fix(core): restore IIFE with </script> escaping for composition scripts

The Function constructor (3bb0d1ef) was a security hardening to prevent
</script> injection, but it broke sub-composition DOM proxy scoping.
This restores the inline IIFE (preserving closure scope) while adding
</script> → <\/script> escaping to maintain the injection prevention.

Updates tests to match the new IIFE output shape.

Closes #1074
This commit is contained in:
Miguel Ángel
2026-05-25 15:44:35 -04:00
parent ccee8df4e1
commit 0de17a1f66
3 changed files with 7 additions and 6 deletions
@@ -572,14 +572,15 @@ window.__afterTimeline = window.__timelines.scene;
expect(scoped).toContain('[data-composition-id="chrome-overlay"] .child-element');
});
it("wraps scoped composition script source as a string literal", () => {
it("escapes </script> in scoped composition script source to prevent injection", () => {
const wrapped = wrapScopedCompositionScript(
'window.payload = "</script><script>window.pwned = true;</script>";',
"scene",
);
expect(wrapped).toContain('Function("document", "gsap", "window", "__hyperframes", ');
expect(wrapped).toContain('\\"</script><script>window.pwned = true;</script>\\"');
expect(wrapped).toContain("(function(document, gsap, window, __hyperframes)");
expect(wrapped).not.toContain("</script><script>");
expect(wrapped).toContain("<\\/script>");
});
it("wraps unscoped composition script source as a string literal", () => {
@@ -486,7 +486,7 @@ export function wrapScopedCompositionScript(
var __hfRun = function() {
try {
(function(document, gsap, window, __hyperframes) {
${source}
${source.replace(/<\/(script)/gi, "<\\/$1")}
}).call(window, __hfScopedDocument, __hfScopedGsap, __hfScopedWindow, __hfScopedHyperframes);
} catch (_err) {
console.error(__hfErrorLabel, __hfCompId, _err);
@@ -758,8 +758,8 @@ describe("bundleToSingleHtml", () => {
expect(bundled).toContain('[data-composition-id="scene"] .title { color: red; }');
expect(bundled).toContain("new Proxy(window.document");
expect(bundled).toContain("new Proxy(__hfBaseGsap");
expect(bundled).toContain('Function("document", "gsap", "window", "__hyperframes",');
expect(bundled).toContain("tl.to('.title'");
expect(bundled).toContain("(function(document, gsap, window, __hyperframes)");
expect(bundled).toContain('tl.to(".title"');
});
it("isolates sibling instances of the same external sub-composition", async () => {