Migrate from xlsx to exceljs to fix security vulnerabilities

- Replace xlsx package (v0.18.5) with exceljs (v4.4.0)
- Remove @types/xlsx dependency (exceljs has built-in TypeScript types)
- Update biaMatchingService.ts to use ExcelJS API:
  - Replace XLSX.read() with workbook.xlsx.load()
  - Replace XLSX.utils.sheet_to_json() with eachRow() iteration
  - Handle 1-based column indexing correctly
- Make loadBIAData() and findBIAMatch() async functions
- Update all callers in applications.ts and claude.ts to use await
- Fix npm audit: 0 vulnerabilities (was 1 high severity)

This migration eliminates the Prototype Pollution and ReDoS vulnerabilities
in the xlsx package while maintaining full functionality.
This commit is contained in:
2026-01-15 09:59:43 +01:00
parent c60fbe8821
commit e276e77fbc
5 changed files with 852 additions and 145 deletions

View File

@@ -1,7 +1,7 @@
{
"name": "zira-backend",
"name": "cmdb-insight-backend",
"version": "1.0.0",
"description": "ZiRA Classificatie Tool Backend",
"description": "CMDB Insight Backend",
"type": "module",
"main": "dist/index.js",
"scripts": {
@@ -29,7 +29,7 @@
"openai": "^6.15.0",
"pg": "^8.13.1",
"winston": "^3.17.0",
"xlsx": "^0.18.5"
"exceljs": "^4.4.0"
},
"devDependencies": {
"@types/better-sqlite3": "^7.6.12",
@@ -38,7 +38,6 @@
"@types/express": "^5.0.0",
"@types/node": "^22.9.0",
"@types/pg": "^8.11.10",
"@types/xlsx": "^0.0.35",
"tsx": "^4.19.2",
"typescript": "^5.6.3"
}

View File

@@ -103,7 +103,7 @@ router.get('/bia-test', async (req: Request, res: Response) => {
if (getQueryString(req, 'clear') === 'true') {
clearBIACache();
}
const biaData = loadBIAData();
const biaData = await loadBIAData();
res.json({
recordCount: biaData.length,
records: biaData.slice(0, 20), // First 20 records
@@ -119,7 +119,7 @@ router.get('/bia-test', async (req: Request, res: Response) => {
router.get('/bia-debug', async (req: Request, res: Response) => {
try {
clearBIACache();
const biaData = loadBIAData();
const biaData = await loadBIAData();
// Get a few sample applications
const searchResult = await dataService.searchApplications({}, 1, 50);
@@ -138,7 +138,7 @@ router.get('/bia-debug', async (req: Request, res: Response) => {
// Test each sample app
for (const app of [...sampleApps, ...testApps]) {
const matchResult = findBIAMatch(app.name, app.searchReference ?? null);
const matchResult = await findBIAMatch(app.name, app.searchReference ?? null);
// Find all potential matches in Excel data for detailed analysis
const normalizedAppName = app.name.toLowerCase().trim();
@@ -207,7 +207,7 @@ router.get('/bia-comparison', async (req: Request, res: Response) => {
clearBIACache();
// Load fresh data
const testBIAData = loadBIAData();
const testBIAData = await loadBIAData();
logger.info(`BIA comparison: Loaded ${testBIAData.length} records from Excel file`);
if (testBIAData.length === 0) {
logger.error('BIA comparison: No Excel data loaded - check if BIA.xlsx exists and is readable');
@@ -251,7 +251,7 @@ router.get('/bia-comparison', async (req: Request, res: Response) => {
for (const app of applications) {
// Find BIA match in Excel
const matchResult = findBIAMatch(app.name, app.searchReference ?? null);
const matchResult = await findBIAMatch(app.name, app.searchReference ?? null);
// Log first few matches for debugging
if (comparisonItems.length < 5) {

View File

@@ -10,7 +10,7 @@ import { readFileSync, existsSync } from 'fs';
import { join } from 'path';
import { dirname } from 'path';
import { fileURLToPath } from 'url';
import * as XLSX from 'xlsx';
import ExcelJS from 'exceljs';
import { logger } from './logger.js';
// Get __dirname equivalent for ES modules
@@ -52,13 +52,13 @@ export function clearBIACache(): void {
/**
* Load BIA data from Excel file
*/
export function loadBIAData(): BIARecord[] {
export async function loadBIAData(): Promise<BIARecord[]> {
const now = Date.now();
// Return cached data if still valid AND has records
// Don't use cache if it's empty (indicates previous load failure)
if (biaDataCache && biaDataCache.length > 0 && (now - biaDataCacheTimestamp) < BIA_CACHE_TTL) {
logger.debug(`Using cached BIA data (${biaDataCache.length} records, cached ${Math.round((now - biaDataCacheTimestamp) / 1000)}s ago)`);
return biaDataCache;
return Promise.resolve(biaDataCache);
}
// Clear cache if it's empty or expired
@@ -96,19 +96,45 @@ export function loadBIAData(): BIARecord[] {
logger.error(`__dirname: ${__dirname}`);
biaDataCache = [];
biaDataCacheTimestamp = now;
return [];
return Promise.resolve([]);
}
logger.info(`Loading BIA data from: ${biaFilePath}`);
try {
// Read file using readFileSync and then parse with XLSX.read
// This works better in ES modules than XLSX.readFile
// Read file using readFileSync and then parse with ExcelJS
const fileBuffer = readFileSync(biaFilePath);
const workbook = XLSX.read(fileBuffer, { type: 'buffer' });
const sheetName = workbook.SheetNames[0];
const worksheet = workbook.Sheets[sheetName];
const data = XLSX.utils.sheet_to_json(worksheet, { header: 1 }) as any[][];
const workbook = new ExcelJS.Workbook();
// ExcelJS accepts Buffer, but TypeScript types may be strict - use type assertion
await workbook.xlsx.load(fileBuffer as Buffer);
const worksheet = workbook.worksheets[0]; // First sheet
// Converteer naar 2D array formaat (zoals xlsx.utils.sheet_to_json met header: 1)
// We need at least column K (index 10), so ensure we read up to column 11 (1-based)
const data: any[][] = [];
const maxColumnNeeded = 11; // Column K is index 10 (0-based), so we need column 11 (1-based)
worksheet.eachRow((row, rowNumber) => {
const rowData: any[] = [];
// Ensure we have at least maxColumnNeeded columns, but also check actual cells
const actualMaxCol = Math.max(maxColumnNeeded, row.actualCellCount || 0);
for (let colNumber = 1; colNumber <= actualMaxCol; colNumber++) {
const cell = row.getCell(colNumber);
// ExcelJS uses 1-based indexing, convert to 0-based for array
// Handle different cell value types: convert to string for consistency
let cellValue: any = cell.value;
if (cellValue === null || cellValue === undefined) {
cellValue = '';
} else if (cellValue instanceof Date) {
cellValue = cellValue.toISOString();
} else if (typeof cellValue === 'object' && 'richText' in cellValue) {
// Handle RichText objects
cellValue = cell.value?.toString() || '';
}
rowData[colNumber - 1] = cellValue;
}
data.push(rowData);
});
logger.info(`Loaded Excel file: ${data.length} rows, first row has ${data[0]?.length || 0} columns`);
if (data.length > 0 && data[0]) {
@@ -236,12 +262,12 @@ export function loadBIAData(): BIARecord[] {
}
biaDataCache = records;
biaDataCacheTimestamp = now;
return records;
return Promise.resolve(records);
} catch (error) {
logger.error('Failed to load BIA data from Excel', error);
biaDataCache = [];
biaDataCacheTimestamp = now;
return [];
return Promise.resolve([]);
}
}
@@ -330,11 +356,11 @@ function wordBasedSimilarity(str1: string, str2: string): number {
* - Confidence/similarity score
* - Length similarity (prefer matches with similar length)
*/
export function findBIAMatch(
export async function findBIAMatch(
applicationName: string,
searchReference: string | null
): BIAMatchResult {
const biaData = loadBIAData();
): Promise<BIAMatchResult> {
const biaData = await loadBIAData();
if (biaData.length === 0) {
logger.warn(`No BIA data available for lookup of "${applicationName}" (biaData.length = 0)`);
return {

View File

@@ -52,7 +52,7 @@ try {
async function findBIAValue(applicationName: string, searchReference?: string | null): Promise<string | null> {
// Use the unified matching service (imported at top of file)
const { findBIAMatch } = await import('./biaMatchingService.js');
const matchResult = findBIAMatch(applicationName, searchReference || null);
const matchResult = await findBIAMatch(applicationName, searchReference || null);
return matchResult.biaValue || null;
}