I have read in several places that scripts are not executed when we use React dangerouslySetInnerHTML
to insert a html fragment.
However I just tried to insert this:
<img src= "img.png" onload="alert('picture loaded')" alt="script test">
And the alert was triggered.
That doesn't really surprise me (that's why I did the test in the first place), but I'd like to better understand what we mean by "scripts don't execute".
My questions:
[Edit] I am using dangerouslySetInnerHTML
in a function component:
const htmlString = '<img src="img.png" onload="alert('picture loaded')" alt="script test">'
In the return statement (JSX):
return <div dangerouslySetInnerHTML={{__html: htmlString}} />
The scripts that "will not execute" are script tags, like <script>
. See here for an example:
const html = `
<script>console.log('this does NOT run');<\/script>
<img src onerror="console.log('but this will')">
`;
const App = () => {
return <div dangerouslySetInnerHTML={{__html: html}} />;
};
ReactDOM.render(<App />, document.querySelector('.react'));
<script crossorigin src="https://unpkg.com/react@16/umd/react.development.js"></script>
<script crossorigin src="https://unpkg.com/react-dom@16/umd/react-dom.development.js"></script>
<div class="react"></div>
Inline handlers, not being <script>
tags, can run, if the event they're attached to fires. (Above, an <img>
tag with a src
property but no valid path throws an error, so its onerror
inline handler runs)
There are no other categories of scripts that will run in combination with dangerouslySetInnerHTML
(unless an inline handler itself injects a <script>
by other means, like with document.createElement('script')
).
is there a way to fully prevent script execution, including those embedded in html event handlers like in my example?
You'll need to remove the on-
attributes. If all on-
attributes are removed, no events which may be fired will ever result in unexpected scripts running. You could sanitize the input by sending it through DOMParser first:
const sanitize = (input) => {
const doc = new DOMParser().parseFromString(input, 'text/html');
for (const elm of doc.querySelectorAll('*')) {
for (const attrib of elm.attributes) {
if (attrib.name.startsWith('on')) {
elm.removeAttribute(attrib.name);
}
}
}
return doc.body.innerHTML;
};
const html = `
<div>
<script>console.log('this does NOT run');<\/script>
<img src onerror="console.log('but this will')">
more content
<style type="text/css"> img {float: left; margin: 5px}</style>
</div>
`;
const App = () => {
return <div dangerouslySetInnerHTML={{__html: sanitize(html)}} />;
};
ReactDOM.render(<App />, document.querySelector('.react'));
<script crossorigin src="https://unpkg.com/react@16/umd/react.development.js"></script>
<script crossorigin src="https://unpkg.com/react-dom@16/umd/react-dom.development.js"></script>
<div class="react"></div>
<img src="https://www.gravatar.com/avatar/f117a950c689d3d6ec459885a908166e?s=32&d=identicon&r=PG">
if a script tag defines a function, will that function still be loaded and be callable later?
No, because the <script>
tag will not have run at all, so nothing that goes on inside of it will do anything; functions defined inside will not become visible. For such a thing to occur, you'd have to deliberately reload the newly injected <script>
tag somehow, in a manner that gets it to run.