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
+20
View File
@@ -100,6 +100,18 @@ export const projectCoverProcedures = {
message: `Gemini error: ${parseGeminiError(err)}`,
});
}
// Provider-generated output is still untrusted — a compromised or
// misconfigured upstream could return a polyglot payload. Run the
// same magic-byte + trailer + marker check we apply to user uploads
// before we persist the data URL to the database.
const providerCheck = validateImageDataUrl(coverImageUrl);
if (!providerCheck.valid) {
throw new TRPCError({
code: "INTERNAL_SERVER_ERROR",
message: `Provider image rejected by validator: ${providerCheck.reason}`,
});
}
} else {
const dalleClient = createDalleClient(runtimeSettings);
const model =
@@ -135,6 +147,14 @@ export const projectCoverProcedures = {
}
coverImageUrl = `data:image/png;base64,${b64}`;
const providerCheck = validateImageDataUrl(coverImageUrl);
if (!providerCheck.valid) {
throw new TRPCError({
code: "INTERNAL_SERVER_ERROR",
message: `Provider image rejected by validator: ${providerCheck.reason}`,
});
}
}
await ctx.db.project.update({