(This is related to the 'prototype pollution' attack, although searching that phrase will mostly give you information about the more-dangerous variant where two objects are being merged together with some JS library. If __proto__ is just part of a literal, the behavior is not as dangerous, but still surprising.)
Kaminsky described a very simple and nearly-universal technique to deal with escaping/injection issues. Encode the embedded data as base64 and decode it on the client side. This projects arbitrary data into a fixed, known domain (generally `[a-zA-Z0-9+/]*`) which you can ensure is free from control characters. (You may need to use a particular variant to achieve this, eg for URLs the last characters used are generally `-_` because both + and / are significant in that context.)
After decoding, you can pass it to JSON.parse().
<script language="JavaScript"><!--
// script contents
-->
</script>
That ship sailed several paragraphs ago, when <script> got special treatment by the HTML parser. Too bad we couldn't all agree to parse <![CDATA[...]]> consistently, or, you know, just &-escape the text like we do /everywhere else/ in HTML.
Everything until the tag closer </script> is inside
the script element.
And: In fact, script tags can contain any language (not
necessarily JavaScript) or even arbitrary data. In order to
support this behavior, script tags have special parsing
rules. For the most part, the browser accepts whatever is
inside the script tag until it finds the script close tag
</script>.
Note the sentence fragment "even arbitrary data." This explains the second part of your question as to why nested script tags without HTML comments do not require matching closing tags. Similar compatibility hacks exist for other closing tags (search for Chrome closing tags being optional for a fun ride down a rabbit hole).As to:
why a script tag inside a comment inside a script tag needs
to be closed ...
Well, this again is due to maximizing backward compatibility in order to support broken browsers (thanks IE4, you bastard!). As the article states: When JavaScript was first introduced, many browsers did not
support it. So they would render the content of the script
tag – the JavaScript code itself. The normal way to get
around that was to put the script into a comment ...
HTHI guess people just generally don’t add those?
Still, to help me out, could someone clarify why this was down-voted? I don’t want to mess up again if I did, but I don't understand what that was.
And yeah use URL-safe base64 when you do use it. -_ with no padding.
- escape `<` as `\u003c`
<script id="my-json" type="application/json">{{ escaped_json }}</script>
JSON.parse(document.getElementById('my-json').textContent)
No __proto__ issue, and no dynamic code at all, so you can use a strict CSP.It’s helpful to recognize that the inner script tags are not actual script tags. Yes, once entering a script element, the browser switches parsers and wants to skip everything until a closing script tag appears. The STYLE element, TITLE, TEXTAREA, and a few others do this. Once they chop up the HTML like this they send the contents to the separate inner parser (in this case, the JS engine). SCRIPT is unique due to the legacy behavior^1.
HTML5 specifies these “inner” tags as transitions into escape modes. The entire goal is to allow JavaScript to contain the string “</script>” without it leaking to the outer parser. The early pattern of hiding inside an HTML comment is what determined the escaping mechanism rather than making some special syntax (which today does exist as noted in the post).
The opening script tag inside the comment is actually what triggers the escaping mode, and so it’s less an HTML tag and more some kind of pseudo JS syntax. The inner closing tag is therefore the escaped string value and simultaneously closes the escaped mode.
Consider the use of double quotes inside a string. We have to close the outer quote, but if the inner quote is escaped like `\”` then we don’t have to close it — it’s merely data and not syntax.
There is only one level of nesting, and eight opening tags would still be “closed” by the single closing tag.
^1: (edit) This is one reason HTML and XML (XHTML) are incompatible. The content of SCRIPT and STYLE elements are essentially just bytes. In XML they must be well-formed markup. XML parsers cannot parse HTML.
type
This attribute indicates the type of script represented. The value of this attribute will be one of the following:
[...]
Any other value
The embedded content is treated as a data block, and won't be processed by the browser. Developers must use a valid MIME type that is not a JavaScript MIME type to denote data blocks. All of the other attributes will be ignored, including the src attribute.
Although 'importmap' has specific functionality, as does 'speculationrules', although they operate similarly. My favorite is type="module" which competes with the higher level attribute nomodule="true". Anyways it looks like <script> has taken a lot of abuse over the years:https://developer.mozilla.org/en-US/docs/Web/HTML/Reference/...
Or did they always have two levels of script tag escaping but that behavior only got preserved when inside an HTML comment?
No other JavaScript behavior is different inside an HTML comment, and I’m still missing the connection between the HTML comment and the embedded </script> not closing the tag besides that they were two things that older browsers might have done.
Most people will opt for text to be optional with a link - unless they're showing their own product (Show HN). Because there is an expectation that you will attempt to read an article, before conversing about it.
I think most of the time people dont add a comment to submissions, but if they do its more of the form: I found X interesting because of [insert non obvious reason why X is interesting] or some additional non-obvious context needed.
In any case, i don't think there is any reason to worry too much. There was no ill intent and at the end of the day its all just fake internet points.
<script>console.log("<![CDATA[Hello, this string content in a CDATA section!]]>");</script>
Results in this being output to the console: <![CDATA[Hello, this string content in a CDATA section!]]>
Browsers don't do what you intend if you wrap the whole script in CDATA, either. They treat the "<![CDATA[" sequence as literally part of the script! Which of course throws a syntax error.I tend to use them anyway, as sort of a HTML/XHTML polyglot thing, because deep in my heart I still think HTML should be valid XML:
<script>/* <![CDATA[ */
// my script here, and you *still* need to be careful not
// to include close-script or close-cdata sequences
/* ]]> */</script>
In summary, the 'special parsing rules for script tags' add a great amount of complexity not just to the parsing code, but for anybody who has to emit markup, especially if different parsers disagree on what kind of escaping rules are active within a given section. Yes, the HTML5 spec codified the neurotypical "I would rather make you guess what I mean than just use the proper words to say it clearly" behavior, so at least browsers agree on it, but it's a mess and a pain to deal with because now you have to remember 1000 exceptions to what would have been simple rules.It "conflicts" in the same way noscript[1] and script "conflict" no? They're basically related features, but can't really be made exclusive because the mere act of trying to do so wouldn't work: as the link indicates, executing code in a !module browser reserves the type (requires a specific set of types) so you can't use that as a way to opt in !module browsers.
[1] an other fun element with wonky parsing rules besides
Never was, never will be. Just write XHTML instead.
In this link we can see the expectation that the HTML comment surrounds a call to document.write() which inserts a new SCRIPT element. The tags are balanced.
https://stackoverflow.com/questions/236073/why-split-the-scr...
In this HTML 4.01 spec, it’s noted to use HTML comments to hide the script contents from render, which is where we start to get the notion of using these to hide markup from display.
https://www.w3.org/TR/html401/interact/scripts.html
Some drafts of the HTML standard attempted to escape differently and didn’t have the double escape state.
https://www.w3.org/TR/2016/WD-html52-20161206/semantics-scri...
My guess is that at some point the parsers looked for balanced tags, as evidenced in the note in the last link above, but then practical issues with improperly-generated scripts led to the idea that a single SCRIPT closing tag ends the escaping. Maybe people were attempting to concatenate script contents wrong and getting stacks of opening tags that were never closed. I don’t know, but I suppose it’s recorded somewhere.
Many things in today’s HTML arose because of widespread issues with how people generated the content. The same is true of XML and XHTML by the way. Early XML mailing lists were full of people parsing XML with naive PERL regular expressions and suggesting that when someone wants to “fix” broken markup, that they do it with string-based find-and-replace.
The main difference is that the HTML spec went in the direction of saying, _if we can agree how to handle these errors then in the face of some errors we can display some content_ and we can all do it in the same way. XML is worse in some regards: certain kinds of errors are still ambiguous and up to the parser to determine how to handle, whether they are non-recoverable or recoverable. For those non-recoverable, the presence of a single error destroys the entire document, like being refused a withdrawal at the bank because you didn’t cross a 7.
At least with HTML5, it’s agreed upon what to do when errors are present and all parsers can produce the same output document; XML parsers routinely handle malformed content and do so in different ways (though most at least provide or default to a strict mode). It’s better than the early web, but not that much better.
The advantage of the base64 technique is that it provides fewer degrees of freedom, and so is more robust to unforseen vectors of attack. It's defensive programming. But it comes at a cost of memory/bandwidth.
Bullshit - Navigator and IE didn't have HTTP/2. I'm guessing you didn't use dialup where your external CSS or JavaScript regularly failed to load. You didn't add extra dependencies because IE would only had two concurrent connections to load files.
It's easy to criticize past mistakes from your armchair: but I suggest you try and be a little more fair towards the people that made decisions especially when overall HTML has been a resounding success.
<script nomodule="true" type="module"></script>
Which is a little weird. At the very least I'd expect the type="module" documentation to say that `charset`, `defer` and `nomodule` attributes have no effect."My momentary convenience trumps the man-millenia of effort required to protect billions of people from script injection attacks."
Have you done even a single thing in the markup community?
Yes, and you can write
<noscript>
<script>
...
</script>
</noscript>
Edit: and "effort", please. The spec has a simple and clear note:
> The easiest and safest way to avoid the rather strange restrictions described in this section is to always escape an ASCII case-insensitive match for "<!--" as "\x3C!--", "<script" as "\x3Cscript", and "</script" as "\x3C/script" when these sequences appear in literals in scripts (e.g. in strings, regular expressions, or comments), and to avoid writing code that uses such constructs in expressions. Doing so avoids the pitfalls that the restrictions in this section are prone to triggering.
Backwards compatibility is easily and completely worth this small amount of effort. It's a one-liner in most languages.
Engineers hate bad compromises, and the core of engineering is making good compromises. Creating anything makes you your own critic.
https://html.spec.whatwg.org/multipage/scripting.html#attr-s....
My expectation was that this condition would have been reflected in MDNs documentation where it breaks the conditions for 'charset' and 'defer' out.
[1]: https://uploadcare.com/blog/vulnerability-in-html-design/
Perhaps the spec isn't the right tool for every job? That's why, for me, at least.
<
with \x3C
or \u003C
in JSON strings.json_encode($data, JSON_HEX_TAG | JSON_UNESCAPED_SLASHES)
for safe JSON in <script>
tags.[wp_json_encode](https://developer.wordpress.org/reference/functions/wp_json_encode/)
with the same flags.├─SCRIPT
│ └─#text console.log('
└─#text ')
└─SCRIPT
└─#text {␊
"closeComment": "-->",␊
"closeScript": "<\/script>",␊
"openComment": "<!--",␊
"openScript": "<script>"␊
}</script>␊
<h1>Success! 🎉</h1>
</script
: script data → close<!--
: script data → script data escaped[JSON_HEX_TAG](https://www.php.net/manual/en/json.constants.php#constant.json-hex-tag)
[JSON_UNESCAPED_SLASHES](https://www.php.net/manual/en/json.constants.php#constant.json-unescaped-slashes)
/
.[JSON_UNESCAPED_UNICODE](https://www.php.net/manual/en/json.constants.php#constant.json-unescaped-unicode)
[JSON_UNESCAPED_LINE_TERMINATORS](https://www.php.net/manual/en/json.constants.php#constant.json-unescaped-line-terminators)
[JSON_UNESCAPED_UNICODE](https://www.php.net/manual/en/json.constants.php#constant.json-unescaped-unicode)
is supplied. It uses the same behaviour as it was before PHP 7.1 without this constant. Available as of PHP 7.1.0.├─SCRIPT
│ └─#text {␊
│ "closeComment": "--\u003E",␊
│ "closeScript": "\u003C/script\u003E",␊
│ "openComment": "\u003C!--",␊
│ "openScript": "\u003Cscript\u003E"␊
│ }
├─#text ␊
└─H1
└─#text Success! 🎉
<!--
” transition to<script>
” transition to</script>
as the script close tag. Technically, it’s not strictly </script>
, but a sequence of characters that looks like a script tag closer. For example </SCRIPT/>
closes a script element but </script-no>
does not. ↩︎[JSON_PRETTY_PRINT](https://www.php.net/manual/en/json.constants.php#constant.json-pretty-print)
in the output for legibility. This flag is omitted from the example code. ↩︎<
character is encountered. That is the only transition from the script data state. ↩︎