feat(import): harden untrusted spreadsheet boundaries
This commit is contained in:
@@ -1,12 +1,99 @@
|
||||
import type { SkillEntry } from "@capakraken/shared";
|
||||
|
||||
let _xlsx: typeof import("xlsx") | null = null;
|
||||
type ExcelJsModule = typeof import("exceljs");
|
||||
|
||||
async function getXLSX() {
|
||||
if (!_xlsx) {
|
||||
_xlsx = await import("xlsx");
|
||||
let _excelJs: ExcelJsModule | null = null;
|
||||
|
||||
async function getExcelJS() {
|
||||
if (!_excelJs) {
|
||||
_excelJs = await import("exceljs");
|
||||
}
|
||||
return _xlsx;
|
||||
return _excelJs;
|
||||
}
|
||||
|
||||
function normalizeCellString(value: unknown): string {
|
||||
if (value === undefined || value === null) {
|
||||
return "";
|
||||
}
|
||||
|
||||
if (value instanceof Date) {
|
||||
return value.toISOString();
|
||||
}
|
||||
|
||||
if (typeof value === "string" || typeof value === "number" || typeof value === "boolean") {
|
||||
return String(value);
|
||||
}
|
||||
|
||||
if (typeof value === "object") {
|
||||
const record = value as Record<string, unknown>;
|
||||
|
||||
if ("result" in record) {
|
||||
return normalizeCellString(record.result);
|
||||
}
|
||||
|
||||
if ("text" in record && typeof record.text === "string") {
|
||||
return record.text;
|
||||
}
|
||||
|
||||
if ("hyperlink" in record && typeof record.hyperlink === "string") {
|
||||
return record.hyperlink;
|
||||
}
|
||||
|
||||
if ("richText" in record && Array.isArray(record.richText)) {
|
||||
return record.richText
|
||||
.map((part) => {
|
||||
if (part && typeof part === "object" && "text" in part) {
|
||||
const text = (part as { text?: unknown }).text;
|
||||
return typeof text === "string" ? text : "";
|
||||
}
|
||||
return "";
|
||||
})
|
||||
.join("");
|
||||
}
|
||||
}
|
||||
|
||||
return String(value);
|
||||
}
|
||||
|
||||
function worksheetToRowObjects(
|
||||
worksheet: {
|
||||
rowCount: number;
|
||||
getRow: (rowNumber: number) => {
|
||||
cellCount: number;
|
||||
getCell: (columnNumber: number) => { value: unknown };
|
||||
};
|
||||
} | undefined,
|
||||
): Record<string, string>[] {
|
||||
if (!worksheet) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const rows: string[][] = [];
|
||||
for (let rowNumber = 1; rowNumber <= worksheet.rowCount; rowNumber += 1) {
|
||||
const row = worksheet.getRow(rowNumber);
|
||||
const cells: string[] = [];
|
||||
|
||||
for (let columnNumber = 1; columnNumber <= row.cellCount; columnNumber += 1) {
|
||||
cells.push(normalizeCellString(row.getCell(columnNumber).value));
|
||||
}
|
||||
|
||||
rows.push(cells);
|
||||
}
|
||||
|
||||
const headers = (rows[0] ?? []).map((header) => header.trim());
|
||||
if (headers.length === 0) {
|
||||
return [];
|
||||
}
|
||||
|
||||
return rows
|
||||
.slice(1)
|
||||
.filter((row) => row.some((value) => value.trim() !== ""))
|
||||
.map((row) =>
|
||||
headers.reduce<Record<string, string>>((record, header, index) => {
|
||||
record[header] = row[index] ?? "";
|
||||
return record;
|
||||
}, {}),
|
||||
);
|
||||
}
|
||||
|
||||
export interface ParsedEmployeeInfo {
|
||||
@@ -91,24 +178,13 @@ function parseSkillSheet(rows: Record<string, string>[], mainSkillSet: Set<strin
|
||||
* Returns ParsedSkillMatrix with employeeInfo and merged skills array.
|
||||
*/
|
||||
export async function parseSkillMatrixWorkbook(data: ArrayBuffer): Promise<ParsedSkillMatrix> {
|
||||
const XLSX = await getXLSX();
|
||||
const workbook = XLSX.read(new Uint8Array(data), { type: "array" });
|
||||
const ExcelJS = await getExcelJS();
|
||||
const workbook = new ExcelJS.Workbook();
|
||||
await workbook.xlsx.load(data);
|
||||
|
||||
const employeeSheet = workbook.Sheets["Employee Information"];
|
||||
const softwareSheet = workbook.Sheets["Software Skills"];
|
||||
const technicalSheet = workbook.Sheets["Technical Skillset"];
|
||||
|
||||
const employeeRows = employeeSheet
|
||||
? XLSX.utils.sheet_to_json<Record<string, string>>(employeeSheet, { raw: false, defval: "" })
|
||||
: [];
|
||||
|
||||
const softwareRows = softwareSheet
|
||||
? XLSX.utils.sheet_to_json<Record<string, string>>(softwareSheet, { raw: false, defval: "" })
|
||||
: [];
|
||||
|
||||
const technicalRows = technicalSheet
|
||||
? XLSX.utils.sheet_to_json<Record<string, string>>(technicalSheet, { raw: false, defval: "" })
|
||||
: [];
|
||||
const employeeRows = worksheetToRowObjects(workbook.getWorksheet("Employee Information"));
|
||||
const softwareRows = worksheetToRowObjects(workbook.getWorksheet("Software Skills"));
|
||||
const technicalRows = worksheetToRowObjects(workbook.getWorksheet("Technical Skillset"));
|
||||
|
||||
const employeeInfo = parseEmployeeInfo(employeeRows);
|
||||
|
||||
|
||||
Reference in New Issue
Block a user