Nested Script Element

ID

html.nested_script

Severity

high

Remediation Complexity

trivial

Remediation Risk

low

Remediation Effort

low

Resource

Reliability

Language

Html

Tags

best-practice, reliability

Description

Reports a <script> element opened inside another script’s body, detected at the unmatched </script> it leaves behind.

Rationale

An HTML tokeniser ends a script at the first </script> it reads, with no regard for JavaScript syntax. A closing tag inside a string literal, a comment or a template literal ends the element just the same as one written in the markup.

So a script that opens a second script in its body — or that merely writes the characters </script> into a string — is cut short at that point. Everything after it stops being script and starts being markup: statements render as visible text, and the author’s own </script> has nothing left to close. Nothing about the source looks wrong, which is what makes this expensive to debug.

<!-- The string's closing tag ends the script; the real </script> is then unmatched -->
<script>var closer = "</script>";</script>

<!-- The inner </script> closes the template script; the outer one is left over -->
<script type="text/x-template">
  <script>bootstrap()</script>
</script>

Remediation

Never write </script> inside a script body, and never nest one script element in another. Create the element from script instead, so no markup is involved:

<script>
  const s = document.createElement("script");
  s.src = "legacy.js";
  document.head.appendChild(s);
</script>

When the tag characters really must appear in a string, escape the slash or split the literal — both forms keep the tokeniser out of it, and neither is reported:

<script>
  document.write("<script src=\"legacy.js\"><\/script>");
  document.write("<scr" + "ipt src=\"legacy.js\"><" + "/script>");
</script>

For a client-side template, use <template>, which holds inert markup and needs no escaping at all.

Scope

Detection is exact rather than heuristic, but narrower than the defect’s name suggests, and it is worth knowing why.

A </script> that legitimately closes a script is lexed inside the script body as one token; a </script> lexed in markup state is therefore, by construction, one the parser could not match to any open script. That leftover is the reported evidence.

The nested element itself is not reported, because it cannot be observed: a script body arrives from the parser as a single block of text, exactly as a browser reads it, so the inner <script> is never a node of its own. The consequence is that a nested script is caught through the unmatched close it produces, and that the two correct legacy idioms above — which produce no unmatched close — are never reported. A stray </script> written with no script open at all is reported as well: it is the same defect seen from the other side, and equally a broken document.

© 2026 Xygeni. All rights reserved