// taint-destructuring.test.mjs — B6 (v7.2.0) — taint-tracer destructuring + spread // // Critical-review §2 B6 finding: extractAssignedVariable handled `const X` // and `X = ...` but missed every modern destructuring pattern. As a result, // taint flows through destructured/spread variables produced false negatives // at the propagation step (same-line sink detection still worked). // // This test pins the B6 fix at the unit-test level. The taint-tracer now // exports `extractAssignedVariable` for direct testing — same pattern as // policy-loader's `_resetCacheForTest`. import { describe, it } from 'node:test'; import assert from 'node:assert/strict'; import { extractAssignedVariable } from '../../scanners/taint-tracer.mjs'; describe('extractAssignedVariable — pre-B6 patterns (regression guard)', () => { it('plain const declaration: const x = req.body', () => { const names = extractAssignedVariable('const x = req.body;'); assert.deepEqual(names, ['x']); }); it('plain let declaration: let y = process.argv[1]', () => { const names = extractAssignedVariable('let y = process.argv[1];'); assert.deepEqual(names, ['y']); }); it('plain var declaration: var z = user_input', () => { const names = extractAssignedVariable('var z = user_input;'); assert.deepEqual(names, ['z']); }); it('plain assignment: tainted = req.body', () => { const names = extractAssignedVariable('tainted = req.body;'); assert.deepEqual(names, ['tainted']); }); it('does not match equality: x == req.body', () => { const names = extractAssignedVariable('if (x == req.body) {}'); assert.equal(names.length, 0); }); }); describe('extractAssignedVariable — B6 destructuring patterns', () => { it('object destructuring: const { x } = req.body', () => { const names = extractAssignedVariable('const { x } = req.body;'); assert.deepEqual(names.sort(), ['x']); }); it('object destructuring multi-key: const { x, y } = req.body', () => { const names = extractAssignedVariable('const { x, y } = req.body;'); assert.deepEqual(names.sort(), ['x', 'y']); }); it('renamed destructuring: const { secret: alias } = req.body', () => { const names = extractAssignedVariable('const { secret: alias } = req.body;'); assert.deepEqual(names.sort(), ['alias']); // The key `secret` is NOT a binding — only `alias` is. assert.ok(!names.includes('secret'), 'key (secret) must not be a binding'); }); it('object rest: const { x, ...spread } = req.body', () => { const names = extractAssignedVariable('const { x, ...spread } = req.body;'); assert.deepEqual(names.sort(), ['spread', 'x']); }); it('nested object destructuring: const { a, b: { c } } = req.body', () => { const names = extractAssignedVariable('const { a, b: { c } } = req.body;'); assert.deepEqual(names.sort(), ['a', 'c']); // `b` is a key — not a binding. assert.ok(!names.includes('b'), 'key (b) must not be a binding'); }); it('array destructuring: const [a, b] = process.argv', () => { const names = extractAssignedVariable('const [a, b] = process.argv;'); assert.deepEqual(names.sort(), ['a', 'b']); }); it('array rest: const [first, ...rest] = process.argv', () => { const names = extractAssignedVariable('const [first, ...rest] = process.argv;'); assert.deepEqual(names.sort(), ['first', 'rest']); }); it('nested array destructuring: const [a, [b, c]] = source', () => { const names = extractAssignedVariable('const [a, [b, c]] = source;'); assert.deepEqual(names.sort(), ['a', 'b', 'c']); }); it('mixed destructuring: const { user: { id }, ...rest } = req.body', () => { const names = extractAssignedVariable('const { user: { id }, ...rest } = req.body;'); assert.deepEqual(names.sort(), ['id', 'rest']); // `user` is a key — not a binding. assert.ok(!names.includes('user'), 'key (user) must not be a binding'); }); it('let with destructuring: let { x } = source', () => { const names = extractAssignedVariable('let { x } = source;'); assert.deepEqual(names.sort(), ['x']); }); it('var with destructuring: var [a] = source', () => { const names = extractAssignedVariable('var [a] = source;'); assert.deepEqual(names.sort(), ['a']); }); it('default value in destructuring: const { x = 5 } = source', () => { // The destructured binding `x` is still bound; the default `= 5` is on // the RHS of `:` if absent or just trails the ident. The walker treats // `=` and any literal that follows as separators. const names = extractAssignedVariable('const { x = 5 } = source;'); assert.ok(names.includes('x'), `expected x in ${JSON.stringify(names)}`); }); }); describe('extractAssignedVariable — non-destructuring pre-B6 regressions', () => { it('does not falsely add destructure-shaped tokens from non-decl lines', () => { // `if ({ x } === ...)` is invalid JS but the regex must not match it // as a binding. Same for object literals on the RHS without `const`. const names = extractAssignedVariable('return { x: 1 };'); assert.equal(names.length, 0, `non-decl produced unexpected names: ${JSON.stringify(names)}`); }); it('does not match arrow function param destructuring without const', () => { // `({x}) => ...` is a destructure but not an assignment — skip. const names = extractAssignedVariable('const fn = ({x}) => x;'); // The plain-decl branch still picks `fn` (the function-binding name). assert.deepEqual(names, ['fn']); }); });