Files
eng-pad-server/internal/render/jpg.go
Kyle Isom ea9375b6ae Security hardening: fix critical, high, and medium issues from audit
CRITICAL:
- A-001: SQL injection in snapshot — escape single quotes in backup path
- A-002: Timing attack — always verify against dummy hash when user not
  found, preventing username enumeration
- A-003: Notebook ownership — all authenticated endpoints now verify
  user_id before loading notebook data
- A-004: Point data bounds — decodePoints returns error on misaligned
  data, >4MB payloads, and NaN/Inf values

HIGH:
- A-005: Error messages — generic errors in HTTP responses, no err.Error()
- A-006: Share link authz — RevokeShareLink verifies notebook ownership
- A-007: Scan errors — return 500 instead of silently continuing

MEDIUM:
- A-008: Web server TLS — optional TLS support (HTTPS when configured)
- A-009: Input validation — page_size, stroke count, point_data alignment
  checked in SyncNotebook RPC
- A-010: Graceful shutdown — 30s drain on SIGINT/SIGTERM, all servers
  shut down properly

Added AUDIT.md with all 17 findings, status, and rationale for
accepted risks.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-24 20:16:26 -07:00

102 lines
2.1 KiB
Go

package render
import (
"bytes"
"image"
"image/color"
"image/jpeg"
"math"
)
// RenderJPG rasterizes a page's strokes at 300 DPI as JPEG.
func RenderJPG(pageSize string, strokes []Stroke, quality int) ([]byte, error) {
w, h := pageSizeCanonical(pageSize)
img := image.NewRGBA(image.Rect(0, 0, w, h))
// White background
for y := 0; y < h; y++ {
for x := 0; x < w; x++ {
img.Set(x, y, color.White)
}
}
// Draw strokes
for _, s := range strokes {
points, err := decodePoints(s.PointData)
if err != nil {
continue
}
if len(points) < 4 {
continue
}
c := argbToColor(s.Color)
penRadius := float64(s.PenSize) / 2.0
for i := 0; i < len(points)-3; i += 2 {
x0, y0 := points[i], points[i+1]
x1, y1 := points[i+2], points[i+3]
drawLine(img, x0, y0, x1, y1, penRadius, c)
}
}
var buf bytes.Buffer
if err := jpeg.Encode(&buf, img, &jpeg.Options{Quality: quality}); err != nil {
return nil, err
}
return buf.Bytes(), nil
}
func pageSizeCanonical(pageSize string) (int, int) {
switch pageSize {
case "LARGE":
return 3300, 5100
default:
return 2550, 3300
}
}
func argbToColor(argb int32) color.RGBA {
return color.RGBA{
R: uint8((argb >> 16) & 0xFF),
G: uint8((argb >> 8) & 0xFF),
B: uint8(argb & 0xFF),
A: uint8((argb >> 24) & 0xFF),
}
}
// drawLine draws a line with the given pen radius using Bresenham's.
func drawLine(img *image.RGBA, x0, y0, x1, y1, radius float64, c color.RGBA) {
dx := x1 - x0
dy := y1 - y0
dist := math.Sqrt(dx*dx + dy*dy)
if dist < 1 {
fillCircle(img, int(x0), int(y0), int(radius), c)
return
}
steps := int(dist) + 1
for i := 0; i <= steps; i++ {
t := float64(i) / float64(steps)
x := x0 + t*dx
y := y0 + t*dy
fillCircle(img, int(x), int(y), int(radius), c)
}
}
func fillCircle(img *image.RGBA, cx, cy, r int, c color.RGBA) {
if r < 1 {
r = 1
}
bounds := img.Bounds()
for dy := -r; dy <= r; dy++ {
for dx := -r; dx <= r; dx++ {
if dx*dx+dy*dy <= r*r {
px, py := cx+dx, cy+dy
if px >= bounds.Min.X && px < bounds.Max.X && py >= bounds.Min.Y && py < bounds.Max.Y {
img.Set(px, py, c)
}
}
}
}
}