Notes /

React spread operator is dangerous

Written today (July 6, 2025)

Using spread operator within react with untrusted user input is a recipe for XSS; embedding raw HTML into website.

React supports embedding raw HTML via an attribute called dangerouslySetInnerHTML.__html.

return (
    <p dangerouslySetInnerHTML={{__html:"<b>hello world</b>"}} />
)

Which is different from how for example svelte handles it - by using a different tag / template (the equivalent would be <p>{@html "..."}</p>).

JSX supports spread operators just like any JS object, as it creates one under the hood anyway. Which is nice for passing props to another component:

return (
    <Component {...props} />
)

But this could be coupled to make a mostly hidden bug with no ’danger’ in it’s sources being explicitly present:

import { useSearchParams } from 'next/navigation';
import { parse } from 'qs';

function useQs() {
    const searchParams = useSearchParams();
    const search = searchParams.toString();

    return parse(search, {
        ignoreQueryPrefix: true,
        decoder: (str) => {
            str = decodeURIComponent(str);
            return str
        }
    });
}
function Page() {
    // load query string, i.e. parses http://tld/path?key=val into { key: "val" }
    const qs = useQs();

    return (<Component props={{ }} {...qs} />)
}

function Component({ props, key }) {
    return (
        <div {...props}>
            Your key is {key || "empty"}
        </div>
    )
}

The above app is vulnerable, because the latter spread operator can override props, which is then spreaded div.
This would be safe - if buggy - if all we could do is set props to a string value.

But qs also parses objects. So you’re just one ?props[dangerouslySetInnerHTML][__html]="<b>pwned</b>" away from danger. Check your vibe coded apps, and perhaps choose technologies with sane & safe defaults.


Daniel Bulant - Blog posts CC-BY-SA (unless otherwise specified)