Expected Behavior
Obfuscating valid input should always produce syntactically valid output (or throw). Parentheses around an in expression inside a for statement's init are grammatically required (the "NoIn" restriction) and must be preserved.
Current Behavior
The obfuscator silently emits unparsable JavaScript. It removes the parentheses around an in expression located in a for init, so the bare in is parsed as a for-in head and the code throws at parse time:
- V8 / native:
SyntaxError: Invalid left-hand side in for-loop
- acorn:
Assigning to rvalue
No exception is raised by obfuscate() — the invalid code is returned as-is, which can ship and break the application at load.
Steps to Reproduce
Input (valid JS):
for(a=(t,e)=>(t in e)?0:1;;){}
import O from 'javascript-obfuscator'
const out = O.obfuscate(`for(a=(t,e)=>(t in e)?0:1;;){}`, { compact: true }).getObfuscatedCode()
console.log(out)
// for(a=(_0x14502c,_0xcad55a)=>_0x14502c in _0xcad55a?0x0:0x1;;){}
// ^^^^^^^^^^^^^^^^^^^^^^^ parentheses around `(t in e)` dropped
new Function(out) // throws: Invalid left-hand side in for-loop
The protective parentheses present in the input ((t in e)) are absent from the output.
Reproduces regardless of options
Same broken output with default options, with { compact: true } only, and even with { simplify: false } — so this is in the code generator / parenthesization logic, not a specific transform:
| Options |
Result |
{} (defaults) |
❌ Invalid left-hand side in for-loop |
{ compact: true } |
❌ |
{ compact: true, simplify: false } |
❌ |
Root cause
Inside a for statement's init, an unparenthesized in operator is interpreted as introducing a for-in loop (the ECMAScript "NoIn" grammar production). The input parenthesizes the in expression to opt out of that interpretation; the obfuscator's output omits those parentheses, so the parser consumes everything before in as the for-in binding target — which is not a valid assignment target — hence the parse error.
For comparison, the same in expression outside a for init (e.g. var a = (b in c) ? 1 : 2;) is correctly emitted without parentheses and stays valid — confirming the parentheses are only required in the for-init "NoIn" context, which the generator does not account for.
Real-world impact
Hit in a production bundle: obfuscating an app bundle that includes @sentry/rrweb code (a base64 char-code table built via for (h = (e,t,n) => (t in e) ? define(e,t,…) : e[t]=n, i=0; i<s.length; i++) …) produced an unparsable bundle. On a single-file (inlined) native bundle this bricks the app at load — the JS never parses. Minimal reduction of that bundle yields the one-line repro above.
Your Environment
javascript-obfuscator: 5.4.3 (latest)
- Node.js: 22.x
- OS: Linux
Related
Expected Behavior
Obfuscating valid input should always produce syntactically valid output (or throw). Parentheses around an
inexpression inside aforstatement's init are grammatically required (the "NoIn" restriction) and must be preserved.Current Behavior
The obfuscator silently emits unparsable JavaScript. It removes the parentheses around an
inexpression located in aforinit, so the bareinis parsed as afor-inhead and the code throws at parse time:SyntaxError: Invalid left-hand side in for-loopAssigning to rvalueNo exception is raised by
obfuscate()— the invalid code is returned as-is, which can ship and break the application at load.Steps to Reproduce
Input (valid JS):
The protective parentheses present in the input (
(t in e)) are absent from the output.Reproduces regardless of options
Same broken output with default options, with
{ compact: true }only, and even with{ simplify: false }— so this is in the code generator / parenthesization logic, not a specific transform:{}(defaults){ compact: true }{ compact: true, simplify: false }Root cause
Inside a
forstatement's init, an unparenthesizedinoperator is interpreted as introducing afor-inloop (the ECMAScript "NoIn" grammar production). The input parenthesizes theinexpression to opt out of that interpretation; the obfuscator's output omits those parentheses, so the parser consumes everything beforeinas thefor-inbinding target — which is not a valid assignment target — hence the parse error.For comparison, the same
inexpression outside aforinit (e.g.var a = (b in c) ? 1 : 2;) is correctly emitted without parentheses and stays valid — confirming the parentheses are only required in thefor-init "NoIn" context, which the generator does not account for.Real-world impact
Hit in a production bundle: obfuscating an app bundle that includes
@sentry/rrwebcode (a base64 char-code table built viafor (h = (e,t,n) => (t in e) ? define(e,t,…) : e[t]=n, i=0; i<s.length; i++) …) produced an unparsable bundle. On a single-file (inlined) native bundle this bricks the app at load — the JS never parses. Minimal reduction of that bundle yields the one-line repro above.Your Environment
javascript-obfuscator: 5.4.3 (latest)Related
({a} = obj)(closed 2020). Same family (assignment-target handling) but those were runtime-incorrect output; this one is parse-invalid output, specific to thefor-init NoIn context.