* fix: prevent script close tag from being swallowed by a single match
`SCRIPT_CLOSE_REGEXP` matched `<\/script[^>]*>`, whose wildcard could run
from one `</script` to the next `>` anywhere in the function source. When a
second, complete `</script>` fell inside that span, the whole thing collapsed
into one match, and since the plain-code branch neutralizes only the leading
`<`, the swallowed tag was re-emitted verbatim.
A function body reaches that shape whenever `</script` appears in code
position -- `x</script=+/` parses as `x < /script=+/`, a comparison against a
regex literal -- followed by a `</script>` in a later string. The serialized
output then carries a live `</script>`, which terminates the script element
when embedded the way the README documents, so the rest of the payload is
parsed as HTML. Confirmed in headless Chromium: the injected `onerror` runs.
This regressed in v7.1.0. v7.0.7 used the same wildcard but escaped the
entire match, so nothing survived.
Excluding `<` from the character class fixes it: a match can no longer reach
past a second `<`, so every `</script` in the source either starts its own
match or is followed by a non-delimiter -- and the HTML tokenizer only ends
the tag name on TAB, LF, FF, CR, SPACE, `/` or `>`, emitting anything else as
text.
Co-Authored-By: Claude Opus 5 <[email protected]>
* fix: reject spoofed function toString() and make native-code check stateless
Two defects in `serializeFunc`, found while investigating the script close
tag escaping.
`fn.toString()` was trusted to return a string. It is attacker-controlled in
the same way `URL.prototype.toString` and `RegExp.prototype.source` were
before they were hardened. Returning an object with its own `replace()` is
enough to defeat the escaping outright, because `escapeFunctionBody()` is
built entirely from `str.replace(...)` calls -- the object's `replace` simply
returns itself, and the payload reaches the output untouched:
var f = function () {};
f.toString = () => ({
replace: function () { return this; },
toString: () => 'function(){}</script><img src=x onerror=alert(1)>'
});
serialize({ f: f });
// {"f":function(){}</script><img src=x onerror=alert(1)>}
A primitive string from a spoofed `toString()` was already safe; only the
non-string case bypasses escaping. Rejecting it matches how the URL and
RegExp spoofing cases are already handled.
Separately, `IS_NATIVE_CODE_REGEXP` carried a `/g` flag. `.test()` on a
global regexp advances `lastIndex`, so after one rejection the next call
began its scan past the `[native code]` match and returned false, letting
every other native function through:
try { serialize(Math.max); } catch (e) {} // correctly throws
serialize(Math.min); // 'function min() { [native code] }'
That output is a syntax error rather than an injection, so the impact is
limited to an unreliable guard, but the flag serves no purpose here.
Co-Authored-By: Claude Opus 5 <[email protected]>
---------
Co-authored-by: Claude Opus 5 <[email protected]>