Skip to content

Commit 9f04848

Browse files
committed
fix(GHSA-wp5r-2gw5-m7q7): close unicode-escape identifier bypass
The transformer fast-path bailed out when the source contained none of `catch`/`import`/`async`/`with` AND the substring `INTERNAL_STATE_NAME` was absent. Identifiers in JavaScript can use `\uXXXX` / `\u{...}` escapes, so source like `var x = VM2_INTERNAL_STATE_DO_NOT_USE_OR_PROGRAM_WILL_FAIL;` slipped past both checks and re-opened the very leak the GHSA closed (`wrapWith`/`handleException`/`import` exposure). Fix: any source containing `\u` falls through to AST instrumentation, where the walker decodes escapes and inspects the actual identifier name. Adds 3 regression tests covering single-char, code-point, and fully-escaped identifier forms; one negative test confirms legitimate `\u` escapes inside string literals still execute correctly.
1 parent b8ebb4f commit 9f04848

2 files changed

Lines changed: 47 additions & 1 deletion

File tree

lib/transformer.js

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -70,8 +70,21 @@ function transformer(args, body, isAsync, isGenerator, filename) {
7070
// this). Pre-fix the regex.test below silently coerced undefined to
7171
// "undefined" and returned false; the new substring check would
7272
// throw. Coerce to a defined string for both checks.
73+
//
74+
// SECURITY (post-GHSA-wp5r-2gw5-m7q7 hardening): identifiers in
75+
// JavaScript CAN contain `\uXXXX` / `\u{...}` unicode escapes —
76+
// `VM2_INTERNAL_STATE_DO_NOT_USE_OR_PROGRAM_WILL_FAIL` is a
77+
// valid identifier reference for the same global. The `indexOf`
78+
// substring check above only matches the raw form, so bypassed.
79+
// Force fall-through to AST when the source contains any `\u`
80+
// escape; the AST walker decodes escapes and inspects the actual
81+
// identifier name.
7382
const codeStr = typeof code === 'string' ? code : '';
74-
if (!/\b(?:catch|import|async|with)\b/.test(codeStr) && codeStr.indexOf(INTERNAL_STATE_NAME) === -1) {
83+
if (
84+
!/\b(?:catch|import|async|with)\b/.test(codeStr) &&
85+
codeStr.indexOf(INTERNAL_STATE_NAME) === -1 &&
86+
codeStr.indexOf('\\u') === -1
87+
) {
7588
return {__proto__: null, code, hasAsync: false};
7689
}
7790
} else {

test/ghsa/GHSA-wp5r-2gw5-m7q7/repro.js

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -65,4 +65,37 @@ describe('GHSA-wp5r-2gw5-m7q7 (transformer fast-path bypass)', function () {
6565
it('regression: code with catch/import/async still runs unchanged', function () {
6666
assert.strictEqual(new VM().run(`try { throw new Error('x'); } catch(e) { e.message }`), 'x');
6767
});
68+
69+
// SECURITY (post-GHSA-wp5r-2gw5-m7q7 hardening): unicode-escape identifier
70+
// bypass surfaced during pre-tag red-team. Identifiers can contain
71+
// `\uXXXX` escapes; the original fix's substring `indexOf` only matched
72+
// the raw form, so `VM2_INTERNAL_STATE_…` slipped past the fast-path
73+
// and re-opened the same exposure the GHSA was meant to close. The
74+
// fast-path now bails out for any source containing `\u`.
75+
describe('unicode-escape identifier bypass', function () {
76+
it('rejects \\u0056M2_INTERNAL_STATE_… (single-char unicode escape)', function () {
77+
assert.throws(function () {
78+
new VM().run(`var x = \\u0056M2_INTERNAL_STATE_DO_NOT_USE_OR_PROGRAM_WILL_FAIL; x;`);
79+
}, /Use of internal vm2 state variable/);
80+
});
81+
82+
it('rejects \\u{56}M2_INTERNAL_STATE_… (extended unicode code-point escape)', function () {
83+
assert.throws(function () {
84+
new VM().run(`var x = \\u{56}M2_INTERNAL_STATE_DO_NOT_USE_OR_PROGRAM_WILL_FAIL; x;`);
85+
}, /Use of internal vm2 state variable/);
86+
});
87+
88+
it('rejects fully-escaped identifier', function () {
89+
// Every char as \uXXXX: VV -> V, MM -> M, etc. Verify
90+
// that even when no raw VM2_… substring appears, AST still rejects.
91+
assert.throws(function () {
92+
new VM().run(`var x = \\u0056\\u004D2_INTERNAL_STATE_DO_NOT_USE_OR_PROGRAM_WILL_FAIL; x;`);
93+
}, /Use of internal vm2 state variable/);
94+
});
95+
96+
it('regression: legitimate \\u escapes in strings still work', function () {
97+
// Source contains \u but the AST walker doesn't reject string literals.
98+
assert.strictEqual(new VM().run(`'\\u0041\\u0042'`), 'AB');
99+
});
100+
});
68101
});

0 commit comments

Comments
 (0)