security: workbook path allowlist + stronger image polyglot validation (#54)

- dispo workbook imports are pinned to DISPO_IMPORT_DIR (default ./imports):
  tRPC input rejects absolute paths and .. segments, runtime reader
  re-validates containment via path.relative. Closes a path-traversal
  class that reached ExcelJS CVEs through admin/compromised tokens.
- image validator now checks the full 8-byte PNG magic, enforces PNG IEND
  and JPEG EOI trailers, scans the decoded buffer for markup polyglot
  markers (<script, <svg, <iframe, javascript:, onerror=, ...), and
  explicitly rejects SVG. Provider-generated covers (DALL-E, Gemini) run
  through the same validator before persistence — an untrusted upstream
  cannot smuggle a stored-XSS payload past us.
- added image-validation.test.ts and tightened documentation.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
2026-04-17 15:26:29 +02:00
parent 3392297791
commit c4b01c1bfc
11 changed files with 394 additions and 65 deletions
+5 -2
View File
@@ -102,9 +102,12 @@ publicProcedure
- Strict TypeScript (`strict: true`, `exactOptionalPropertyTypes: true`)
- Blueprint dynamic fields validated at runtime against stored Zod schema definitions
- File uploads validated by:
- MIME type whitelist (`image/png`, `image/jpeg`, `image/webp`, `image/tiff`, `image/bmp`)
- MIME type whitelist (`image/png`, `image/jpeg`, `image/webp`, `image/tiff`, `image/bmp`). SVG is explicitly rejected — XML markup could carry `<script>`.
- Size limit (10 MB client-side, 4 MB server-side after compression)
- Magic byte verification (actual file content matched against declared MIME)
- Full magic-byte verification: declared MIME must match actual content. PNG uses the full 8-byte signature, not a short prefix that would accept polyglots.
- Trailer check: PNG must end with an `IEND` chunk, JPEG with the `FFD9` EOI marker. Any bytes appended after the trailer are rejected.
- Polyglot-marker scan: the decoded buffer is searched (latin1, lowercased) for markup fragments (`<script`, `<svg`, `<iframe`, `javascript:`, `onerror=`, …) and rejected if any appear. Provider-generated images (DALL-E, Gemini) run through the same validator before persistence — an untrusted upstream cannot smuggle a stored-XSS payload past us by virtue of being "our" API.
- Dispo workbook imports must live under the `DISPO_IMPORT_DIR` directory (defaults to `./imports`). The tRPC input schema accepts only relative paths (no `..` segments, no absolute paths), and the runtime workbook reader re-validates that the resolved absolute path stays inside `DISPO_IMPORT_DIR`. This closes a path-traversal class that would have let an admin (or compromised admin token) point the ExcelJS parser at arbitrary files on disk, keeping known ExcelJS CVEs from being reachable through our own API.
### Prompt-Injection Guard (defense-in-depth only)