feat(import): harden untrusted spreadsheet boundaries
This commit is contained in:
@@ -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() {
|
||||
</svg>
|
||||
<p className="text-sm font-medium text-gray-700 dark:text-gray-300">Click to select multiple .xlsx files</p>
|
||||
<p className="text-xs text-gray-400 dark:text-gray-500 mt-1">Name files after resource EID or display name for automatic matching</p>
|
||||
<input ref={fileRef} type="file" accept=".xlsx,.xls" multiple className="hidden" onChange={handleFiles} />
|
||||
<input ref={fileRef} type="file" accept=".xlsx" multiple className="hidden" onChange={handleFiles} />
|
||||
</div>
|
||||
|
||||
{/* Summary */}
|
||||
|
||||
@@ -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 }) {
|
||||
<div className="flex gap-2">
|
||||
<label className="cursor-pointer rounded-xl border border-gray-200 px-3 py-2 text-sm text-gray-600 transition hover:border-gray-300 hover:text-gray-900">
|
||||
Import XLSX
|
||||
<input type="file" accept=".xlsx,.xls,.csv" onChange={handleScopeImport} className="hidden" />
|
||||
<input type="file" accept=".xlsx,.csv" onChange={handleScopeImport} className="hidden" />
|
||||
</label>
|
||||
<button type="button" onClick={() => setScopeItems((current) => [...current, makeScope(current.length + 1)])} className="rounded-xl border border-gray-200 px-3 py-2 text-sm text-gray-600 transition hover:border-gray-300 hover:text-gray-900">
|
||||
Add scope row
|
||||
|
||||
@@ -67,8 +67,8 @@ export function ScopeItemEditor({
|
||||
<div className="space-y-4">
|
||||
<div className="flex flex-wrap items-center gap-3">
|
||||
<label className="cursor-pointer rounded-2xl border border-gray-300 bg-white px-4 py-2 text-sm font-medium text-gray-700 transition hover:bg-gray-50">
|
||||
Import scope from XLSX
|
||||
<input type="file" accept=".xlsx,.xls,.csv" className="hidden" onChange={(event) => void handleScopeImport(event)} />
|
||||
Import scope from spreadsheet
|
||||
<input type="file" accept=".xlsx,.csv" className="hidden" onChange={(event) => void handleScopeImport(event)} />
|
||||
</label>
|
||||
{scopeImportWarnings.length > 0 && (
|
||||
<div className="text-xs text-amber-700">
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
import { useState, useRef } from "react";
|
||||
import { trpc } from "~/lib/trpc/client.js";
|
||||
import { parseSpreadsheet, isSpreadsheetFile } from "~/lib/excel.js";
|
||||
import { assertSpreadsheetFile, parseSpreadsheet, isSpreadsheetFile } from "~/lib/excel.js";
|
||||
|
||||
type ImportStage = "idle" | "preview" | "importing" | "done";
|
||||
|
||||
@@ -48,13 +48,14 @@ export function ImportModal({ onClose }: Props) {
|
||||
setResult(null);
|
||||
|
||||
if (!isSpreadsheetFile(file)) {
|
||||
setFileError("Unsupported file type. Please upload an Excel (.xlsx, .xls) or CSV file.");
|
||||
setFileError("Unsupported file type. Please upload a .xlsx or .csv file.");
|
||||
return;
|
||||
}
|
||||
|
||||
setFileName(file.name);
|
||||
|
||||
try {
|
||||
assertSpreadsheetFile(file, { contextLabel: "resource import" });
|
||||
const parsed = await parseSpreadsheet(file);
|
||||
setRows(parsed);
|
||||
setStage("preview");
|
||||
@@ -111,7 +112,7 @@ export function ImportModal({ onClose }: Props) {
|
||||
{stage === "idle" && (
|
||||
<div className="space-y-4">
|
||||
<p className="text-sm text-gray-600">
|
||||
Upload an Excel or CSV file to import resources. The first row must contain column headers
|
||||
Upload a `.xlsx` or CSV file to import resources. The first row must contain column headers
|
||||
matching the resource fields (e.g.{" "}
|
||||
<code className="px-1 py-0.5 bg-gray-100 rounded text-xs font-mono">
|
||||
eid, displayName, email, chapter, lcrCents
|
||||
@@ -127,13 +128,13 @@ export function ImportModal({ onClose }: Props) {
|
||||
<svg className="w-10 h-10 text-gray-400 mb-2" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={1.5}>
|
||||
<path strokeLinecap="round" strokeLinejoin="round" d="M9 13h6m-3-3v6m5 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z" />
|
||||
</svg>
|
||||
<p className="text-sm text-gray-500">Click to select Excel or CSV</p>
|
||||
<p className="text-xs text-gray-400 mt-1">.xlsx, .xls, .csv supported</p>
|
||||
<p className="text-sm text-gray-500">Click to select `.xlsx` or CSV</p>
|
||||
<p className="text-xs text-gray-400 mt-1">.xlsx, .csv supported</p>
|
||||
</div>
|
||||
<input
|
||||
ref={fileInputRef}
|
||||
type="file"
|
||||
accept=".xlsx,.xls,.csv"
|
||||
accept=".xlsx,.csv"
|
||||
className="hidden"
|
||||
onChange={handleFileChange}
|
||||
/>
|
||||
|
||||
@@ -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 Props {
|
||||
@@ -46,6 +47,7 @@ export function SkillMatrixUpload({ resourceId, isOwner, onClose, onSuccess }: P
|
||||
setPreview(null);
|
||||
|
||||
try {
|
||||
assertSpreadsheetFile(file, { allowCsv: false, contextLabel: "skill matrix import" });
|
||||
const buffer = await file.arrayBuffer();
|
||||
const parsed = await parseSkillMatrixWorkbook(buffer);
|
||||
|
||||
@@ -127,7 +129,7 @@ export function SkillMatrixUpload({ resourceId, isOwner, onClose, onSuccess }: P
|
||||
<input
|
||||
ref={fileRef}
|
||||
type="file"
|
||||
accept=".xlsx,.xls"
|
||||
accept=".xlsx"
|
||||
className="hidden"
|
||||
onChange={handleFile}
|
||||
/>
|
||||
|
||||
Reference in New Issue
Block a user