fix(hardening): handle regex literals after else/do

This commit is contained in:
Tam Nhu Tran
2026-02-12 15:20:22 +07:00
parent bb9d846a54
commit 65a1d8ae2c
2 changed files with 86 additions and 4 deletions
+50 -4
View File
@@ -66,6 +66,22 @@ const SYNC_CALL_CAPTURE_REGEX = new RegExp(
); );
const LEGACY_MARKER_REGEX = const LEGACY_MARKER_REGEX =
/(?:\blegacy\b|\bshim\b|backward compatibility|backwards compatibility|compatibility layer|deprecated.*re-export|re-export.*compatibility)/i; /(?:\blegacy\b|\bshim\b|backward compatibility|backwards compatibility|compatibility layer|deprecated.*re-export|re-export.*compatibility)/i;
const REGEX_LITERAL_KEYWORDS = new Set([
'return',
'throw',
'case',
'else',
'do',
'delete',
'void',
'typeof',
'instanceof',
'in',
'of',
'yield',
'await',
'new',
]);
function toPosixPath(filePath) { function toPosixPath(filePath) {
return filePath.split(path.sep).join('/'); return filePath.split(path.sep).join('/');
@@ -124,9 +140,11 @@ function summarize(items, limit = 10) {
})); }));
} }
function isRegexLiteralStart(previousSignificantChar) { function isRegexLiteralStart(previousSignificantChar, previousIdentifier) {
return ( return (
previousSignificantChar === '' || '([{:;,=!?+-*%^&|~<>'.includes(previousSignificantChar) previousSignificantChar === '' ||
'([{:;,=!?+-*%^&|~<>'.includes(previousSignificantChar) ||
REGEX_LITERAL_KEYWORDS.has(previousIdentifier)
); );
} }
@@ -141,6 +159,7 @@ function stripComments(sourceText) {
let inRegexLiteral = false; let inRegexLiteral = false;
let inRegexCharClass = false; let inRegexCharClass = false;
let previousSignificantChar = ''; let previousSignificantChar = '';
let previousIdentifier = '';
while (index < sourceText.length) { while (index < sourceText.length) {
const current = sourceText[index]; const current = sourceText[index];
@@ -207,6 +226,7 @@ function stripComments(sourceText) {
} }
inRegexLiteral = false; inRegexLiteral = false;
previousSignificantChar = 'r'; previousSignificantChar = 'r';
previousIdentifier = '';
continue; continue;
} }
@@ -224,6 +244,7 @@ function stripComments(sourceText) {
if (current === "'") { if (current === "'") {
inSingleQuote = false; inSingleQuote = false;
previousSignificantChar = 's'; previousSignificantChar = 's';
previousIdentifier = '';
} }
index += 1; index += 1;
continue; continue;
@@ -239,6 +260,7 @@ function stripComments(sourceText) {
if (current === '"') { if (current === '"') {
inDoubleQuote = false; inDoubleQuote = false;
previousSignificantChar = 's'; previousSignificantChar = 's';
previousIdentifier = '';
} }
index += 1; index += 1;
continue; continue;
@@ -254,6 +276,7 @@ function stripComments(sourceText) {
if (current === '`') { if (current === '`') {
inTemplateLiteral = false; inTemplateLiteral = false;
previousSignificantChar = 's'; previousSignificantChar = 's';
previousIdentifier = '';
} }
index += 1; index += 1;
continue; continue;
@@ -273,7 +296,7 @@ function stripComments(sourceText) {
continue; continue;
} }
if (current === '/' && isRegexLiteralStart(previousSignificantChar)) { if (current === '/' && isRegexLiteralStart(previousSignificantChar, previousIdentifier)) {
output += ' '; output += ' ';
index += 1; index += 1;
inRegexLiteral = true; inRegexLiteral = true;
@@ -281,6 +304,19 @@ function stripComments(sourceText) {
continue; continue;
} }
if (/[A-Za-z_$]/.test(current)) {
let tokenEnd = index + 1;
while (tokenEnd < sourceText.length && /[A-Za-z0-9_$]/.test(sourceText[tokenEnd])) {
tokenEnd += 1;
}
const token = sourceText.slice(index, tokenEnd);
output += token;
previousSignificantChar = 'i';
previousIdentifier = token;
index = tokenEnd;
continue;
}
if (current === "'") { if (current === "'") {
inSingleQuote = true; inSingleQuote = true;
output += ' '; output += ' ';
@@ -305,6 +341,7 @@ function stripComments(sourceText) {
output += current; output += current;
if (!/\s/.test(current)) { if (!/\s/.test(current)) {
previousSignificantChar = current; previousSignificantChar = current;
previousIdentifier = '';
} }
index += 1; index += 1;
} }
@@ -477,4 +514,13 @@ function main() {
console.log(`[hardening-inventory] wrote ${relMd}`); console.log(`[hardening-inventory] wrote ${relMd}`);
} }
main(); if (require.main === module) {
main();
}
module.exports = {
buildReport,
collectSyncCallSites,
renderMarkdown,
stripComments,
};
@@ -0,0 +1,36 @@
import { describe, expect, test } from 'bun:test';
const { collectSyncCallSites } = require('../../../scripts/hardening-inventory.js');
describe('hardening-inventory sync call scanning', () => {
test('ignores sync-call names inside regex literals after else', () => {
const source = [
'if (enabled) {',
' run();',
'} else /fs\\.readFileSync\\(/.test("pattern");',
].join('\n');
const result = collectSyncCallSites(source);
expect(result.count).toBe(0);
});
test('ignores sync-call names inside regex literals after do', () => {
const source = 'do /fs\\.writeFileSync\\(/.test("pattern"); while (false);';
const result = collectSyncCallSites(source);
expect(result.count).toBe(0);
});
test('still counts real sync fs call sites', () => {
const source = [
'if (enabled) {',
' run();',
'} else /fs\\.readFileSync\\(/.test("pattern");',
'fs.readFileSync("file.txt", "utf8");',
].join('\n');
const result = collectSyncCallSites(source);
expect(result.count).toBe(1);
expect(result.calls).toEqual(['readFileSync']);
});
});