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.