Very interesting find!
It seems that the empty src-less script element is in some weird state that accepts content or even a new src and interprets them. (I can’t find the reason for this. I just have a tiny hint :)
It is similar to the behavior of dynamically inserted script elements.
Here is an example / proof of your observation and a few examples are added to illustrate:
script[src]::before, script { display: block; border: 1px solid red; padding: 1em; } script[src]::before, script { content: 'src='attr(src) } button { display: block }
<p>Existing empty script in HTML:</p> <script id="existing"></script> <p>Can be invoked just one of:</p> <button onclick="eval(this.innerText)"> existing.innerHTML='console.log("innerHTML to static")' </button> <button onclick="eval(this.innerText)"> existing.src='data:text/javascript,console.log("src to static")' </button> <p>Dynamically created and inserted script (each creates own, so both work):</p> <button onclick="eval(this.innerText)"> document.body.appendChild(document.createElement('script')).innerHTML='console.log("innerHTML to dynamic")' </button> <button onclick="eval(this.innerText)"> document.body.appendChild(document.createElement('script')).src='data:text/javascript,console.log("src to dynamic")' </button>
You will need to re-run the snippet to see how the “static” cases work.
(There is also an empty script containing the empty space generated by SO, for any reason.)
As Lauranti showed, if the script had some (even white) content (or src="" ), this would not work.
Also, in the "dynamic" examples, note that the value of innerHTML or src changes after the script element has been inserted into the document. Thus, you can have an empty static or create a dynamic script, leave it in the document and install it after that.
Sorry, I didn’t give a complete answer, I just wanted to share my research and help.
(Update removed, Oriol was faster and more accurate. Fu, glad to see that figured it out!)
source share