Skip to content

fix(playground): warn when malformed HTML swallows the JS script#1705

Draft
caugner wants to merge 3 commits into
mainfrom
1440-playground-detect-swallowed-script
Draft

fix(playground): warn when malformed HTML swallows the JS script#1705
caugner wants to merge 3 commits into
mainfrom
1440-playground-detect-swallowed-script

Conversation

@caugner

@caugner caugner commented Jul 9, 2026

Copy link
Copy Markdown
Contributor

Description

Harden the Playground runner's content assembly (vendor/yari/libs/play/index.js) against malformed input:

  • Malformed HTML swallowing the JS <script>: add an id to the runner's JS <script> and a post-rendering check that verifies a <script> element with that id parsed (matched via querySelector("script#mdn-play-js"), so a user element reusing the id cannot trigger a false warning); when it did not, emit a console.warn pointing the user at the malformed tag.
  • CSS/JS breaking out of their elements: escape literal </style / </script sequences in user CSS/JS (<\/style, <\/script) so they can no longer terminate the <style> / <script> element early.

Motivation

  • An unclosed or malformed tag in the HTML input (e.g. <o) is parsed such that the following <script> becomes attributes of the open tag, so the user's JavaScript renders as visible text instead of executing, with no indication why (When <o characters in Playground HTML input causes entire JavaScript input to show up in HTML preview #1440).
  • User CSS containing </style or JS containing </script (for example inside a string) terminated the corresponding element early, corrupting the page and allowing arbitrary markup to be injected after it.

Additional details

The runner document is assembled in renderHtml, where the raw HTML/CSS/JS are interpolated into the document.

  • The swallow check runs inside the always-reachable "ready" script (the malformed HTML tag closes at the JS script's own >), so it fires reliably. This implements the stop-gap agreed in the issue thread. The malformed HTML (and leaked JS text) still renders, but the user is now told why.
  • The <\/style / <\/script escaping is behavior-preserving within literal contents: \/ resolves to / inside CSS/JS strings, template literals, and regexes, and already-escaped sequences are left untouched. (The one exception is the pathological x</script/ — a < operator immediately followed by a regex literal beginning with script — which is vanishingly rare.) Verified at the DOM level with parse5 that break-out payloads no longer inject stray elements while string values round-trip unchanged.
  • Known limitation: the escaping only neutralizes </script. A <!--<script sequence in user JS still pushes the parser into the script-data-double-escaped state, where the runner's own </script> no longer closes the element (swallowing everything after it, including the swallow warning). No backslash escape fixes this without changing behavior (<\!-- is an invalid escape in /u regexes), so it is documented in escapeScriptText rather than fixed here.
  • Unit tests in test/unit/play/runner.test.js cover the escaping round-trip and break-out prevention, case preservation, the detection tagging, and pin the <!--<script> limitation.

Related issues and pull requests

Fixes #1440.

An unclosed or malformed tag in the HTML input (e.g. `<o`) is parsed such
that the following `<script>` element becomes attributes of the open tag,
leaving the user's JavaScript source rendered as visible text instead of
executing.

Add an `id` to the JS `<script>` and a post-rendering check in the runner
document that verifies it parsed as an `HTMLScriptElement`. When it did
not, emit a `console.warn` pointing the user at the malformed tag.
@github-actions

github-actions Bot commented Jul 9, 2026

Copy link
Copy Markdown
Contributor

ccf9c76 was deployed to: https://fred-pr1705.review.mdn.allizom.net/

caugner and others added 2 commits July 9, 2026 21:28
User CSS containing `</style` or user JS containing `</script` (for example
inside a string) terminated the runner's `<style>` / `<script>` element early,
corrupting the page and allowing arbitrary markup to be injected after it.

Escape those sequences by inserting a backslash (`<\/style`, `<\/script`),
which stops the HTML parser from ending the element while remaining
equivalent inside CSS/JS strings, template literals, and regexes.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

When <o characters in Playground HTML input causes entire JavaScript input to show up in HTML preview

2 participants