security-no-xlsx-dependency.test.mjs
108 lines 4.1 KB
Raw
sha256:41d741fb345c4abdb640838aa3d847de02ccffd7a39fce04894e743e683b50d0 fix(security): pin patched transitive deps to clear Dependa… Human minor ⚠ breaking 8 hours ago
1 /**
2 * Security regression guard: the vulnerable SheetJS `xlsx` package must never
3 * be (re)introduced into any npm lockfile, and the Excel import path must keep
4 * using the maintained `exceljs` parser.
5 *
6 * Background
7 * ----------
8 * Dependabot alerts #29/#30 flagged `xlsx` (SheetJS community build) for
9 * Prototype Pollution and ReDoS with *no upstream patch available*. Knowtation
10 * resolved this structurally by migrating `lib/importers/excel-xlsx.mjs` to
11 * `exceljs` and removing the `xlsx` dependency entirely. These tests prove that
12 * remediation stays in force, so a future transitive bump cannot silently pull
13 * the unpatched parser back in and expose the Excel import surface (which
14 * accepts user-supplied .xlsx files) to those CVEs.
15 *
16 * This is the "Option C" guarantee from the security audit: instead of pinning
17 * an unpatched package, we assert the unsafe code path cannot exist.
18 */
19
20 import assert from 'assert';
21 import { test } from 'node:test';
22 import fs from 'fs';
23 import path from 'path';
24 import { fileURLToPath } from 'url';
25
26 const __dirname = path.dirname(fileURLToPath(import.meta.url));
27 const repoRoot = path.resolve(__dirname, '..');
28
29 const LOCKFILES = [
30 'package-lock.json',
31 'hub/package-lock.json',
32 'hub/bridge/package-lock.json',
33 'hub/gateway/package-lock.json',
34 ];
35
36 /**
37 * Collect every package name referenced by an npm v3 lockfile, drawn from both
38 * the `packages` map (keyed by `node_modules/...` install path) and the legacy
39 * `dependencies` tree, so a match cannot slip through a single representation.
40 *
41 * @param {string} lockPath absolute path to a package-lock.json
42 * @returns {Set<string>} the set of dependency names present in the lockfile
43 */
44 function collectLockfilePackageNames(lockPath) {
45 const raw = fs.readFileSync(lockPath, 'utf8');
46 /** @type {{ packages?: Record<string, unknown>, dependencies?: Record<string, unknown> }} */
47 const lock = JSON.parse(raw);
48 const names = new Set();
49 for (const key of Object.keys(lock.packages ?? {})) {
50 if (key === '') continue;
51 const idx = key.lastIndexOf('node_modules/');
52 const name = idx === -1 ? key : key.slice(idx + 'node_modules/'.length);
53 if (name) names.add(name);
54 }
55 for (const name of Object.keys(lock.dependencies ?? {})) {
56 names.add(name);
57 }
58 return names;
59 }
60
61 test('no npm lockfile contains the vulnerable SheetJS `xlsx` package', () => {
62 for (const rel of LOCKFILES) {
63 const lockPath = path.join(repoRoot, rel);
64 assert.ok(fs.existsSync(lockPath), `expected lockfile to exist: ${rel}`);
65 const names = collectLockfilePackageNames(lockPath);
66 assert.ok(
67 !names.has('xlsx'),
68 `${rel} must not contain the unpatched 'xlsx' (SheetJS) package — use exceljs instead`,
69 );
70 // Defend against a maliciously aliased reintroduction (npm:xlsx@...).
71 const rawText = fs.readFileSync(lockPath, 'utf8');
72 assert.ok(
73 !/"resolved":\s*"[^"]*\/xlsx\/-\//.test(rawText),
74 `${rel} resolves a tarball for 'xlsx' — the vulnerable parser must not be installed`,
75 );
76 }
77 });
78
79 test('Excel importer uses exceljs and never imports the `xlsx` package', () => {
80 const importerPath = path.join(repoRoot, 'lib/importers/excel-xlsx.mjs');
81 assert.ok(fs.existsSync(importerPath), 'lib/importers/excel-xlsx.mjs must exist');
82 const src = fs.readFileSync(importerPath, 'utf8');
83
84 assert.match(
85 src,
86 /import\s+ExcelJS\s+from\s+['"]exceljs['"]/,
87 'excel importer must parse via exceljs',
88 );
89
90 // A bare `from 'xlsx'` / `require('xlsx')` would reintroduce the CVE surface.
91 assert.doesNotMatch(
92 src,
93 /from\s+['"]xlsx['"]|require\(\s*['"]xlsx['"]\s*\)/,
94 'excel importer must not import the vulnerable xlsx package',
95 );
96 });
97
98 test('root package.json declares no direct dependency on `xlsx`', () => {
99 const pkg = JSON.parse(fs.readFileSync(path.join(repoRoot, 'package.json'), 'utf8'));
100 const buckets = ['dependencies', 'devDependencies', 'optionalDependencies', 'peerDependencies'];
101 for (const bucket of buckets) {
102 const deps = pkg[bucket] ?? {};
103 assert.ok(
104 !Object.prototype.hasOwnProperty.call(deps, 'xlsx'),
105 `package.json#${bucket} must not declare 'xlsx'`,
106 );
107 }
108 });
File History 1 commit
sha256:41d741fb345c4abdb640838aa3d847de02ccffd7a39fce04894e743e683b50d0 fix(security): pin patched transitive deps to clear Dependa… Human minor 8 hours ago