Executing Dangerously Injected Scripts Inside React Components
Tags: react, synchronous, xss
Published: 2023-11-11 05:09:32

---
title: Executing Dangerously Injected Scripts Inside React Components
published: true
date: 2023-11-11 05:06:31 UTC
tags: react, synchronous, xss,
canonical_url: https://macarthur.me/posts/script-tags-in-react
---

First off, I hereby declare myself not responsible for you shooting yourself in the foot after reading this.

I'm doing something a little weird with [JamComments](https://jamcomments.com/?ref=alex-macarthur) integrations. When a site's page is built, all of the HTML, CSS and JavaScript needed for comments to function are pulled in via REST API. I like this pattern. It makes for fewer dependencies, quicker iteration on the product, and it just feels tidier.

For the React-based integrations (Next, Gatsby, or Remix), I'm using the [`dangerouslySetInnerHTML`](https://react.dev/reference/react-dom/components/common?ref=alex-macarthur#dangerously-setting-the-inner-html) prop to inject all of that code content into a `<JamComments />` component. Using that prop is necessary because I want the code to run _as code_ when the page is rendered.

## The Problem: Script Tags Don't Run

The injected HTML + CSS execute just fine this way, but not the `<script>` tags needed for the experience to come alive. React's `dangerouslySetInnerHTML` prop [relies on `innerHTML`](https://github.com/facebook/react/blob/432b9f1d9729aaea010730d546bda89b9842eaa1/packages/react-dom-bindings/src/client/ReactDOMComponent.js?ref=alex-macarthur#L600), which deliberately doesn't execute scripts for [legitimate] security reasons. It's even in bold, Christmas text within the [HTML spec](https://www.w3.org/TR/2008/WD-html5-20080610/dom.html?ref=alex-macarthur#innerhtml0):

![screenshot from the HTML spec indicating script tags aren't executed by innerHTML](https://cms.macarthur.me/content/images/2023/11/image-4.png)

I respect that. Security matters. But in my case, executing JamComments' scripts "dangerously" was just an implementation hurdle by nature of integrating with React. [Their own documentation](https://react.dev/reference/react-dom/components/common?ref=alex-macarthur#dangerously-setting-the-inner-html) even makes the caveat that using it might be appropriate, as long as the injected code is coming from an "extremely trusted source." I know exactly what's being injected here, so I feel like I deserve a hall pass.

## Executing Injected Scripts 

Fortunately, there's a straightforward way to make this happen if you ever need it. The TL;DR: after the component mounts, scoop up the stringified code and re-run it in a document fragment.

Let's build that out. Here's the shell of what we'll start with:

```
import { useRef, useLayoutEffect } from 'react';

export function DangrousElement({ markup }) {
	const elRef = useRef<HTMLDivElement>();

	useLayoutEffect(() => {
		// Magic goes here.
	}, []);

	return (
		<div 
			ref={elRef} 
			dangerouslySetInnerHTML={{ __html: markup }}
		</div>
    );
}
```

Reaching for `useLayoutEffect()` is very intentional, by the way. More on that later. For now, let's continue fleshing this out with the following snippet code-as-a-string being passed to the component:

```
const markup = `
	<span>Dangerous markup!</span>
    
	<script>console.log("Script has executed!");</script>
`;

<DangrousElement markup={markup} />

```

If we do this right, we'll see that `console.log` fire after our component mounts in our application.

### Make a Little Document Fragment

If we wanted, we _could_ query that `<div>` for the `<script>` tags ourselves, build clones of them, and then execute them by appending them to an existing DOM node. But there's a cleaner way to pull this off: a [document fragment](https://developer.mozilla.org/en-US/docs/Web/API/DocumentFragment?ref=alex-macarthur), or a little version of an HTML document we can use to contain (and execute) code.

We're going to reach for a specific piece of that API. First, we'll create a document "range" to hold our fragment. Think of this as a slice of the document we'll use to "hold" everything. After that, we'll set the context of that range to the specific DOM node we're building the component around, and finally, create the fragment. Here's what we'll put into that `useLayoutEffect()` hook:

```
// Create a range/slice of the document. 
const range = document.createRange();

// Set the context to our containing node. 
range.selectNode(elRef.current);

// Create a new fragment within that range. 
const documentFragment = range.createContextualFragment(markup);
```

The benefit to this approach is that the code will execute _in the context of that node_. We can verify this by creating a couple of empty ranges. First, we'll make one with no "selected" node.

```
const openRange = document.createRange();
```

When we access the children of the `startContainer` property, it's wrapped around the entire document itself:

![openRange has context of entire document](https://cms.macarthur.me/content/images/2023/11/image-8.png)

But that changes if we target a specific node:

```
const contextualRange = document.createRange();
contextualRange.selectNode(document.getElementById("app"));
```

This time, the range is wrapped around the node we selected, giving it a little more specific context:

![](https://cms.macarthur.me/content/images/2023/11/image-7.png)

Depending on the script you're running, this might not make for anything practically meaningful, but it's nice to know that the code will run as if it were always in that spot, and maybe prevent some unexpected context-related bugs at the same time.

### Re-Run the Code

The last part's simple. Wipe the already-rendered code and replace it with our fragment:

```
import { useRef, useLayoutEffect } from 'react';

export function DangrousElement({ markup }) {
	const elRef = useRef<HTMLDivElement>();

	useLayoutEffect(() => {
		const range = document.createRange();
		range.selectNode(elRef.current);
		const documentFragment = range.createContextualFragment(markup);
        
+ // Inject the markup, triggering a re-run! 
+ elRef.current.innerHTML = '';
+ elRef.current.append(documentFragment);
	}, []);

	return (
		<div 
			ref={elRef} 
			dangerouslySetInnerHTML={{ __html: markup }}
		</div>
    );
}
```

Now, we can see that script execute like desired:

![screenshot showing console contains log](https://cms.macarthur.me/content/images/2023/11/image-9.png)

### Reaching for `useLayoutEffect()` vs `useEffect()`

It's uncommon to see, but choosing `useLayoutEffect()` here is important. This hook fires [_before_ the component has a chance to be rendered](https://macarthur.me/posts/when-i-needed-uselayouteffect-in-react?ref=alex-macarthur) (and painted) to the screen. This differs from `useEffect()`, which runs _after_ render. If we want our code to execute as if it were there from the beginning, our work needs to come before any possible paint, which could cause an unwelcome "flicker" for scripts that mess with the DOM. To satisfy your curiosity, here's a modified version of the code string:

```
const markup = `
	<span id="message">First!</span>
  
	<script>
		synchronousWait(1500);

		document.getElementById("message").innerText = "Second!";
	</script>
`;
```

I've made a `synchronousWait()` method that blocks the main thread for 1.5 seconds. After that, the text is updated in the injected `<span>`. Because React gives the component a chance to do an initial render + paint with our code, mounting it allows us to see the initial text before it's swapped out.

![GIF showing flash caused by useEffect()](https://cms.macarthur.me/content/images/2023/11/effect-flash.gif)

Replacing that with `useLayoutEffect()` guarantees execution before paint, preventing this flash. Instead, you just get a blank, 1.5s delay – just like you'd get if the snippet were to run in a fresh HTML document.

The trade-off should be obvious, though: because the hook executes synchronously, you're at risk for introducing some annoying performance issues. So, be wary about what the injected script is doing, and be sure to lean into asynchronous patterns as needed.

### By the Way! "Strict" Mode Disabled for Simplicity

You'll notice our log fired once, which is what'd we'd expect in a production environment. But with React's strict mode enabled, `useLayoutEffect()` [will fire twice](https://github.com/facebook/react/issues/24502?ref=alex-macarthur) in development mode, meaning our log would also fire twice unless extra steps are made to avoid it. I've intentionally disabled strict mode here, just for the sake of conceptual simplicity.

If you'd like to see a version that works regardless of strict mode, you can see the [StackBlitz environment here](https://stackblitz.com/edit/stackblitz-starters-wd1tmd?description=React+++TypeScript+starter+project&file=src%2FApp.tsx%2Csrc%2Findex.tsx%2Cpublic%2Findex.html&title=React+Starter&ref=alex-macarthur). It's not much – just another `useRef()` hook and some value setting/getting.

## Don't Be Stupid

I think the `dangerouslySetInnerHTML` is appropriately named, even if it doesn't execute `<script>` tags by default. So, I highly advise you to use it with even more caution if you do so in such a way described here. Despite that risk, I think knowing how to navigate around "limitations" like this, for the right reasons, is valuable, and even powerful. Just don't be an idiot when wielding it. We don't need another [Samy](https://en.wikipedia.org/wiki/Samy_(computer_worm)?ref=alex-macarthur).