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>
151 lines
4.6 KiB
Go
151 lines
4.6 KiB
Go
package render
|
|
|
|
import (
|
|
"encoding/binary"
|
|
"fmt"
|
|
"math"
|
|
"strings"
|
|
)
|
|
|
|
const canonicalToPDF = 72.0 / 300.0 // 0.24
|
|
|
|
// maxPointDataSize is the maximum allowed size of point data (4 MB).
|
|
const maxPointDataSize = 4 * 1024 * 1024
|
|
|
|
// PageSizePt returns the page dimensions in PDF points (72 DPI).
|
|
func PageSizePt(pageSize string) (float64, float64) {
|
|
switch pageSize {
|
|
case "LARGE":
|
|
return 3300 * canonicalToPDF, 5100 * canonicalToPDF // 792 x 1224
|
|
default: // REGULAR
|
|
return 2550 * canonicalToPDF, 3300 * canonicalToPDF // 612 x 792
|
|
}
|
|
}
|
|
|
|
type Stroke struct {
|
|
PenSize float32
|
|
Color int32
|
|
Style string
|
|
PointData []byte
|
|
StrokeOrder int
|
|
}
|
|
|
|
// RenderSVG renders a page's strokes as an SVG document.
|
|
func RenderSVG(pageSize string, strokes []Stroke) (string, error) {
|
|
w, h := PageSizePt(pageSize)
|
|
var b strings.Builder
|
|
|
|
fmt.Fprintf(&b, `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 %.0f %.0f" width="%.0f" height="%.0f">`, w, h, w, h)
|
|
b.WriteString("\n")
|
|
// White background
|
|
fmt.Fprintf(&b, `<rect width="%.0f" height="%.0f" fill="white"/>`, w, h)
|
|
b.WriteString("\n")
|
|
|
|
for _, s := range strokes {
|
|
points, err := decodePoints(s.PointData)
|
|
if err != nil {
|
|
continue
|
|
}
|
|
if len(points) < 4 {
|
|
continue
|
|
}
|
|
|
|
penW := float64(s.PenSize) * canonicalToPDF
|
|
color := colorToCSS(s.Color)
|
|
|
|
// Build path data
|
|
var path strings.Builder
|
|
fmt.Fprintf(&path, "M %.2f %.2f", points[0]*canonicalToPDF, points[1]*canonicalToPDF)
|
|
for i := 2; i < len(points)-1; i += 2 {
|
|
fmt.Fprintf(&path, " L %.2f %.2f", points[i]*canonicalToPDF, points[i+1]*canonicalToPDF)
|
|
}
|
|
|
|
attrs := fmt.Sprintf(`stroke="%s" stroke-width="%.2f" stroke-linecap="round" stroke-linejoin="round" fill="none"`, color, penW)
|
|
if s.Style == "dashed" {
|
|
attrs += fmt.Sprintf(` stroke-dasharray="%.1f %.1f"`, 7.2, 4.8)
|
|
}
|
|
|
|
fmt.Fprintf(&b, `<path d="%s" %s/>`, path.String(), attrs)
|
|
b.WriteString("\n")
|
|
|
|
// Arrow heads
|
|
if (s.Style == "arrow" || s.Style == "double_arrow") && len(points) >= 4 {
|
|
x1, y1 := points[0]*canonicalToPDF, points[1]*canonicalToPDF
|
|
x2, y2 := points[2]*canonicalToPDF, points[3]*canonicalToPDF
|
|
b.WriteString(renderArrowHeads(x1, y1, x2, y2, s.Style, penW, color))
|
|
}
|
|
}
|
|
|
|
b.WriteString("</svg>")
|
|
return b.String(), nil
|
|
}
|
|
|
|
func renderArrowHeads(x1, y1, x2, y2 float64, style string, penW float64, color string) string {
|
|
arrowLen := 40.0 * canonicalToPDF
|
|
arrowAngle := 25.0 * math.Pi / 180.0
|
|
dx := x2 - x1
|
|
dy := y2 - y1
|
|
angle := math.Atan2(dy, dx)
|
|
|
|
var b strings.Builder
|
|
attrs := fmt.Sprintf(`stroke="%s" stroke-width="%.2f" stroke-linecap="round"`, color, penW)
|
|
|
|
// End arrow
|
|
ax1 := x2 - arrowLen*math.Cos(angle-arrowAngle)
|
|
ay1 := y2 - arrowLen*math.Sin(angle-arrowAngle)
|
|
ax2 := x2 - arrowLen*math.Cos(angle+arrowAngle)
|
|
ay2 := y2 - arrowLen*math.Sin(angle+arrowAngle)
|
|
fmt.Fprintf(&b, `<line x1="%.2f" y1="%.2f" x2="%.2f" y2="%.2f" %s/>`, x2, y2, ax1, ay1, attrs)
|
|
b.WriteString("\n")
|
|
fmt.Fprintf(&b, `<line x1="%.2f" y1="%.2f" x2="%.2f" y2="%.2f" %s/>`, x2, y2, ax2, ay2, attrs)
|
|
b.WriteString("\n")
|
|
|
|
if style == "double_arrow" {
|
|
revAngle := angle + math.Pi
|
|
bx1 := x1 - arrowLen*math.Cos(revAngle-arrowAngle)
|
|
by1 := y1 - arrowLen*math.Sin(revAngle-arrowAngle)
|
|
bx2 := x1 - arrowLen*math.Cos(revAngle+arrowAngle)
|
|
by2 := y1 - arrowLen*math.Sin(revAngle+arrowAngle)
|
|
fmt.Fprintf(&b, `<line x1="%.2f" y1="%.2f" x2="%.2f" y2="%.2f" %s/>`, x1, y1, bx1, by1, attrs)
|
|
b.WriteString("\n")
|
|
fmt.Fprintf(&b, `<line x1="%.2f" y1="%.2f" x2="%.2f" y2="%.2f" %s/>`, x1, y1, bx2, by2, attrs)
|
|
b.WriteString("\n")
|
|
}
|
|
|
|
return b.String()
|
|
}
|
|
|
|
// decodePoints decodes a byte slice of little-endian float32 values into float64.
|
|
// Returns an error if the data length is not a multiple of 4, exceeds maxPointDataSize,
|
|
// or contains NaN/Inf values.
|
|
func decodePoints(data []byte) ([]float64, error) {
|
|
if len(data)%4 != 0 {
|
|
return nil, fmt.Errorf("point data length %d is not a multiple of 4", len(data))
|
|
}
|
|
if len(data) > maxPointDataSize {
|
|
return nil, fmt.Errorf("point data size %d exceeds maximum %d", len(data), maxPointDataSize)
|
|
}
|
|
|
|
count := len(data) / 4
|
|
points := make([]float64, 0, count)
|
|
for i := 0; i < count; i++ {
|
|
bits := binary.LittleEndian.Uint32(data[i*4 : (i+1)*4])
|
|
v := float64(math.Float32frombits(bits))
|
|
if math.IsNaN(v) || math.IsInf(v, 0) {
|
|
return nil, fmt.Errorf("point data contains NaN or Inf at index %d", i)
|
|
}
|
|
points = append(points, v)
|
|
}
|
|
return points, nil
|
|
}
|
|
|
|
func colorToCSS(argb int32) string {
|
|
r := (argb >> 16) & 0xFF
|
|
g := (argb >> 8) & 0xFF
|
|
b := argb & 0xFF
|
|
if r == 0 && g == 0 && b == 0 {
|
|
return "black"
|
|
}
|
|
return fmt.Sprintf("rgb(%d,%d,%d)", r, g, b)
|
|
}
|