fix(ci): unescape node -e inline programs in one pass

validateInlineJs unescaped the shell body with four chained replaces,
so a backslash pair followed by n or t was unescaped twice: the pair
became one backslash, then the new backslash-n became a newline. A
valid hook command such as console.log('\\n') failed validation with
an unterminated string. CodeQL flags the chain as double unescaping.

One regex pass maps each escape exactly once. The regression test
feeds the escaped-backslash program through the validator CLI.
This commit is contained in:
Affaan Mustafa authored and Affaan Mustafa committed 2026-10-19 22:03:57 -04:00
1 parent d3c49a80df
commit 944ea9d050
2 files changed
+23 -1

No files matched your search

+8 -1
View File
@@ -87,6 +87,13 @@ function isStringRecord(value) {
&& Object.values(value).every(item => typeof item === 'string');
}
const INLINE_ESCAPES = { '\\': '\\', '"': '"', n: '\n', t: '\t' };
/** Unescape a shell double-quoted `node -e` body in one pass, so `\\n` stays a literal backslash and n. */
function unescapeInlineJs(source) {
return source.replace(/\\(["\\nt])/g, (match, ch) => INLINE_ESCAPES[ch]);
}
/**
* Compile the inline program of a `node -e "..."` command so a typo in
* hooks.json fails validation instead of every hook invocation.
@@ -96,7 +103,7 @@ function validateInlineJs(command, label) {
const nodeEMatch = command.match(/^node -e "((?:[^"\\]|\\.)*)"(?:\s|$)/s);
if (!nodeEMatch) return false;
try {
new vm.Script(nodeEMatch[1].replace(/\\\\/g, '\\').replace(/\\"/g, '"').replace(/\\n/g, '\n').replace(/\\t/g, '\t'));
new vm.Script(unescapeInlineJs(nodeEMatch[1]));
return false;
} catch (syntaxErr) {
console.error(`ERROR: ${label} has invalid inline JS: ${syntaxErr.message}`);
+15
View File
@@ -1039,6 +1039,21 @@ function runTests() {
cleanupTestDir(testDir);
})) passed++; else failed++;
if (test('unescapes inline JS in one pass so an escaped backslash before n stays literal', () => {
const testDir = createTestDir();
const hooksFile = path.join(testDir, 'hooks.json');
// Shell body `console.log('\\n')` is a valid program; a sequential unescape turned it into a raw newline.
fs.writeFileSync(hooksFile, JSON.stringify({
hooks: {
PreToolUse: [{ id: 'test:escaped-backslash-inline-js', matcher: 'test', hooks: [{ type: 'command', command: 'node -e "console.log(\'\\\\n\')"' }] }]
}
}));
const result = runValidatorWithDir('validate-hooks', 'HOOKS_FILE', hooksFile);
assert.strictEqual(result.code, 0, `Should pass escaped-backslash inline JS, stderr: ${result.stderr}`);
cleanupTestDir(testDir);
})) passed++; else failed++;
if (test('validates array command format', () => {
const testDir = createTestDir();
const hooksFile = path.join(testDir, 'hooks.json');