From f6daf21983b33d03ed786b73d71ae24024d35d5b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Hartmut=20N=C3=B6renberg?= Date: Mon, 30 Mar 2026 08:02:52 +0200 Subject: [PATCH] feat(import): harden untrusted spreadsheet boundaries --- apps/web/package.json | 1 + .../src/components/admin/BatchSkillImport.tsx | 4 +- .../components/estimates/EstimateWizard.tsx | 4 +- .../estimates/editors/ScopeItemEditor.tsx | 4 +- .../src/components/resources/ImportModal.tsx | 13 +- .../resources/SkillMatrixUpload.tsx | 4 +- apps/web/src/lib/excel.ts | 240 ++++++++++++++++-- apps/web/src/lib/skillMatrixParser.ts | 120 +++++++-- docs/import-hardening.md | 46 ++++ packages/api/src/router/dispo.ts | 27 +- .../src/__tests__/read-workbook.test.ts | 58 +++++ .../use-cases/dispo-import/read-workbook.ts | 113 ++++++++- pnpm-lock.yaml | 3 + 13 files changed, 561 insertions(+), 76 deletions(-) create mode 100644 docs/import-hardening.md create mode 100644 packages/application/src/__tests__/read-workbook.test.ts diff --git a/apps/web/package.json b/apps/web/package.json index 4f330d9..edc6d3d 100644 --- a/apps/web/package.json +++ b/apps/web/package.json @@ -30,6 +30,7 @@ "@trpc/server": "^11.0.0", "clsx": "^2.1.1", "dompurify": "^3.3.3", + "exceljs": "^4.4.0", "framer-motion": "^12.38.0", "next": "^15.1.7", "next-auth": "^5.0.0-beta.25", diff --git a/apps/web/src/components/admin/BatchSkillImport.tsx b/apps/web/src/components/admin/BatchSkillImport.tsx index 6eaf7b3..05a200f 100644 --- a/apps/web/src/components/admin/BatchSkillImport.tsx +++ b/apps/web/src/components/admin/BatchSkillImport.tsx @@ -3,6 +3,7 @@ import { useState, useRef } from "react"; import { trpc } from "~/lib/trpc/client.js"; import { parseSkillMatrixWorkbook, matchRoleName } from "~/lib/skillMatrixParser.js"; +import { assertSpreadsheetFile } from "~/lib/excel.js"; import type { SkillEntry } from "@capakraken/shared"; interface ParsedEntry { @@ -54,6 +55,7 @@ export function BatchSkillImport() { ); try { + assertSpreadsheetFile(file, { allowCsv: false, contextLabel: "skill matrix import" }); const buffer = await file.arrayBuffer(); const result = await parseSkillMatrixWorkbook(buffer); @@ -152,7 +154,7 @@ export function BatchSkillImport() {

Click to select multiple .xlsx files

Name files after resource EID or display name for automatic matching

- + {/* Summary */} diff --git a/apps/web/src/components/estimates/EstimateWizard.tsx b/apps/web/src/components/estimates/EstimateWizard.tsx index 35de50e..e1eb1a6 100644 --- a/apps/web/src/components/estimates/EstimateWizard.tsx +++ b/apps/web/src/components/estimates/EstimateWizard.tsx @@ -269,7 +269,7 @@ export function EstimateWizard({ onClose }: { onClose: () => void }) { event.target.value = ""; if (!isSpreadsheetFile(file)) { - setScopeImportWarnings(["Unsupported file type. Please upload .xlsx, .xls, or .csv."]); + setScopeImportWarnings(["Unsupported file type. Please upload .xlsx or .csv."]); return; } @@ -586,7 +586,7 @@ export function EstimateWizard({ onClose }: { onClose: () => void }) {