fix: remove vulnerable excel runtime dependency

This commit is contained in:
Codex
2026-06-30 06:55:26 +08:00
parent 7ab702f471
commit e66623158d
8 changed files with 153 additions and 613 deletions

View File

@@ -12,9 +12,9 @@
"dependencies": {
"@supabase/storage-js": "^2.108.2",
"ali-oss": "^6.23.0",
"exceljs": "^4.4.0",
"jose": "^6.2.3",
"pg": "^8.16.3"
"pg": "^8.16.3",
"read-excel-file": "^9.2.0"
},
"devDependencies": {
"@types/node": "^24.0.4",

View File

@@ -1,5 +1,5 @@
import { Buffer } from 'node:buffer';
import ExcelJS from 'exceljs';
import readXlsxFile, { readSheet } from 'read-excel-file/node';
import { HttpError } from '../../core/http.js';
type JsonObject = Record<string, unknown>;
@@ -678,18 +678,17 @@ function parseCsvBody(body: JsonObject, importType: SpreadsheetImportType): Pars
};
}
function worksheetRows(worksheet: ExcelJS.Worksheet, importType: SpreadsheetImportType, overrides: FieldMappingOverrides) {
const matrix: string[][] = [];
worksheet.eachRow({ includeEmpty: false }, row => {
const values: string[] = [];
const max = Math.min(row.cellCount, MAX_SPREADSHEET_COLUMNS);
for (let col = 1; col <= max; col += 1) {
const value = primitiveCellValue(row.getCell(col).value);
values.push(value === undefined || value === null ? '' : String(value));
}
if (values.some(value => value.trim())) matrix.push(values);
});
return rowsFromMatrix(matrix, importType, overrides);
function worksheetRows(matrix: unknown[][], importType: SpreadsheetImportType, overrides: FieldMappingOverrides) {
const rows = matrix.map(line => {
const values = line
.slice(0, MAX_SPREADSHEET_COLUMNS)
.map(value => {
const primitive = primitiveCellValue(value);
return primitive === undefined || primitive === null ? '' : String(primitive);
});
return values;
}).filter(values => values.some(value => value.trim()));
return rowsFromMatrix(rows, importType, overrides);
}
async function parseExcelBody(body: JsonObject, importType: SpreadsheetImportType): Promise<ParsedSpreadsheet> {
@@ -697,19 +696,20 @@ async function parseExcelBody(body: JsonObject, importType: SpreadsheetImportTyp
if (!buffer) {
throw new HttpError(400, 'Excel import requires fileBase64', 'EXCEL_FILE_REQUIRED');
}
const workbook = new ExcelJS.Workbook();
let workbookSheets: Array<{ sheet: string; data: unknown[][] }>;
try {
await workbook.xlsx.load(buffer as unknown as Parameters<typeof workbook.xlsx.load>[0]);
workbookSheets = await readXlsxFile(buffer);
} catch {
throw new HttpError(400, 'Excel file could not be parsed as .xlsx', 'EXCEL_PARSE_FAILED');
}
const sheetName = stringValue(body.sheetName);
const sheetIndex = Number(body.sheetIndex ?? 1);
let selected: ExcelJS.Worksheet | undefined;
if (sheetName) selected = workbook.getWorksheet(sheetName);
else if (Number.isFinite(sheetIndex) && sheetIndex > 0) selected = workbook.getWorksheet(Math.trunc(sheetIndex));
selected = selected || workbook.worksheets.find(sheet => sheet.actualRowCount > 0);
let selected = sheetName
? workbookSheets.find(sheet => sheet.sheet === sheetName)
: workbookSheets[Math.max(0, Math.trunc(Number.isFinite(sheetIndex) && sheetIndex > 0 ? sheetIndex : 1) - 1)];
selected = selected || workbookSheets.find(sheet => sheet.data.some(row => row.some(value => value !== null && value !== undefined && String(value).trim())));
if (!selected) {
throw new HttpError(400, 'Excel file has no non-empty worksheet', 'EXCEL_SHEET_EMPTY');
}
@@ -717,22 +717,31 @@ async function parseExcelBody(body: JsonObject, importType: SpreadsheetImportTyp
const sheets: Record<string, JsonObject[]> = {};
let totalRows = 0;
const overrides = fieldMappingOverrides(body, importType);
for (const sheet of workbook.worksheets) {
if (sheet.actualRowCount === 0) continue;
const rows = worksheetRows(sheet, importType, overrides);
sheets[normalizedKey(sheet.name)] = rows;
for (const sheet of workbookSheets) {
if (!sheet.data.some(row => row.some(value => value !== null && value !== undefined && String(value).trim()))) continue;
const rows = worksheetRows(sheet.data, importType, overrides);
sheets[normalizedKey(sheet.sheet)] = rows;
totalRows += rows.length;
}
assertRowLimit(totalRows);
const selectedRows = sheets[normalizedKey(selected.name)] || [];
if (!sheets[normalizedKey(selected.sheet)]) {
try {
const selectedData = await readSheet(buffer, selected.sheet);
sheets[normalizedKey(selected.sheet)] = worksheetRows(selectedData, importType, overrides);
} catch {
throw new HttpError(400, 'Excel file could not be parsed as .xlsx', 'EXCEL_PARSE_FAILED');
}
}
const selectedRows = sheets[normalizedKey(selected.sheet)] || [];
return {
rows: selectedRows,
sheets,
metadata: {
parser: 'exceljs',
workbookSheetCount: workbook.worksheets.length,
selectedSheet: selected.name,
parser: 'read-excel-file',
workbookSheetCount: workbookSheets.length,
selectedSheet: selected.sheet,
rowCount: selectedRows.length,
totalParsedRows: totalRows,
fieldMappingOverrides: overrides.size,