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
@@ -58,22 +58,22 @@ describe("assistant dispo import batch delegation tools", () => {
const result = await executeTool(
"stage_dispo_import_batch",
JSON.stringify({
chargeabilityWorkbookPath: "/imports/chargeability.xlsx",
planningWorkbookPath: "/imports/planning.xlsx",
referenceWorkbookPath: "/imports/reference.xlsx",
costWorkbookPath: "/imports/cost.xlsx",
rosterWorkbookPath: "/imports/roster.xlsx",
chargeabilityWorkbookPath: "chargeability.xlsx",
planningWorkbookPath: "planning.xlsx",
referenceWorkbookPath: "reference.xlsx",
costWorkbookPath: "cost.xlsx",
rosterWorkbookPath: "roster.xlsx",
notes: "March import",
}),
ctx,
);
expect(stageDispoImportBatch).toHaveBeenCalledWith(ctx.db, {
chargeabilityWorkbookPath: "/imports/chargeability.xlsx",
planningWorkbookPath: "/imports/planning.xlsx",
referenceWorkbookPath: "/imports/reference.xlsx",
costWorkbookPath: "/imports/cost.xlsx",
rosterWorkbookPath: "/imports/roster.xlsx",
chargeabilityWorkbookPath: "chargeability.xlsx",
planningWorkbookPath: "planning.xlsx",
referenceWorkbookPath: "reference.xlsx",
costWorkbookPath: "cost.xlsx",
rosterWorkbookPath: "roster.xlsx",
notes: "March import",
});
expect(JSON.parse(result.content)).toEqual({
@@ -92,18 +92,18 @@ describe("assistant dispo import batch delegation tools", () => {
const result = await executeTool(
"validate_dispo_import_batch",
JSON.stringify({
chargeabilityWorkbookPath: "/imports/chargeability.xlsx",
planningWorkbookPath: "/imports/planning.xlsx",
referenceWorkbookPath: "/imports/reference.xlsx",
chargeabilityWorkbookPath: "chargeability.xlsx",
planningWorkbookPath: "planning.xlsx",
referenceWorkbookPath: "reference.xlsx",
importBatchId: "batch_1",
}),
ctx,
);
expect(assessDispoImportReadiness).toHaveBeenCalledWith({
chargeabilityWorkbookPath: "/imports/chargeability.xlsx",
planningWorkbookPath: "/imports/planning.xlsx",
referenceWorkbookPath: "/imports/reference.xlsx",
chargeabilityWorkbookPath: "chargeability.xlsx",
planningWorkbookPath: "planning.xlsx",
referenceWorkbookPath: "reference.xlsx",
importBatchId: "batch_1",
});
expect(JSON.parse(result.content)).toEqual({