Skip to content

Commit 2864b3c

Browse files
redonkulusclaude
andauthored
fix: escape </script at the end of a serialized function body (#235)
`escapeFunctionBody()` escapes each function body on its own, so a body ending in a bare `</script` never saw the delimiter that the surrounding output supplies next (e.g. the newline from the `space` option). The sequence was emitted raw, letting a later value close the tag and inject markup. Also match the bare form at end of string. Fixes PSECBUGS-118415. Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
1 parent 6a73fa1 commit 2864b3c

2 files changed

Lines changed: 30 additions & 2 deletions

File tree

‎index.js‎

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,11 @@ var UNSAFE_CHARS_REGEXP = /[<>\/\u2028\u2029]/g;
2626
// stays in script data, so only these need neutralizing. The bare-prefix form
2727
// matters because the matching `>` could be supplied by a different
2828
// serialized value later in the output, so escaping stops there without
29-
// waiting for a closing `>`. A trailing backslash is not a delimiter here
29+
// waiting for a closing `>`. For the same reason the bare form also matches
30+
// at the very end of the string: each function body is escaped on its own, so
31+
// a body ending in `</script` can't see the delimiter (e.g. the newline added
32+
// by the `space` option) that the surrounding output supplies next
33+
// (PSECBUGS-118415). A trailing backslash is not a delimiter here
3034
// (that's a JS-level concern, not an HTML one), so this doesn't affect tagged
3135
// template literals like `String.raw`.
3236
//
@@ -38,7 +42,7 @@ var UNSAFE_CHARS_REGEXP = /[<>\/\u2028\u2029]/g;
3842
// longer contain a second `<`, every `</script` in the source is either the
3943
// start of its own match or is followed by a non-delimiter — and the latter
4044
// the tokenizer never treats as an end tag.
41-
var SCRIPT_CLOSE_REGEXP = /<\/script[^<>]*>|<\/script(?=[\t\n\f\r \/>])/gi;
45+
var SCRIPT_CLOSE_REGEXP = /<\/script[^<>]*>|<\/script(?=$|[\t\n\f\r \/>])/gi;
4246

4347
var RESERVED_SYMBOLS = ['*', 'async'];
4448

‎test/unit/serialize.js‎

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -664,6 +664,30 @@ describe('serialize( obj )', function () {
664664
strictEqual(serialized.includes('\\u003C\\u002Fscript\\u003E'), true);
665665
});
666666

667+
it('should escape a script close tag at the very end of a function body (PSECBUGS-118415)', function () {
668+
// Each body is escaped on its own, so one ending in a bare
669+
// `</script` can't see the newline the `space` option emits right
670+
// after it, and a later value can then supply the closing `>`.
671+
var f = function () {};
672+
f.toString = function () { return 'function f(){ return 1 }//</script'; };
673+
var g = function () {};
674+
g.toString = function () { return 'function g(x){ return x } > <img src=x onerror=alert(1)>'; };
675+
var serialized = serialize({ a: { f: f }, g: g }, { space: 2 });
676+
677+
strictEqual(CLOSES_SCRIPT.test(serialized), false);
678+
// Inside the line comment, so it unicode-escapes.
679+
strictEqual(serialized.includes('//\\u003C\\u002Fscript\n'), true);
680+
});
681+
682+
it('should neutralize a script close tag ending a function body in code position', function () {
683+
var fn = function () {};
684+
fn.toString = function () { return 'function f(x){ return x }</script'; };
685+
var serialized = serialize([fn], { space: 2 });
686+
687+
strictEqual(CLOSES_SCRIPT.test(serialized), false);
688+
strictEqual(serialized.includes('}< /script\n'), true);
689+
});
690+
667691
it('should escape every script close tag when several appear in one function', function () {
668692
var src = "function f(x){ return x</script=+/ + '</script>' + '</script>' + '</script >' }";
669693
var fn = new Function('return ' + src)();

0 commit comments

Comments
 (0)