Executing Dangerously Injected Scripts Inside React Components

Created on November 12, 2023 at 11:50 am

Executing Dangerously Injected Scripts Inside React Components In rare moments, you might want to execute script tags injected with React ORG ‘s dangerouslySetInnerHTML ORG prop. Here’s how to make it possible.

First ORDINAL 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 ORG integrations. When a site’s page is built, all of the HTML, CSS ORG and JavaScript PRODUCT needed for comments to function are pulled in via REST API PRODUCT . 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 ORG 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 injected HTML + CSS execute just fine this way, but not the <script> tags needed for the experience to come alive. React PERSON ‘s dangerouslySetInnerHTML ORG prop relies on innerHTML GPE , which deliberately doesn’t execute scripts for [legitimate] security reasons. It’s even in bold, Christmas DATE text within the HTML spec:

I respect that. Security matters. But in my case, executing JamComments ORG ‘ scripts "dangerously" was just an implementation hurdle by nature of integrating with React. Their own documentation 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.

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 ORG ( { markup } ) { const elRef = useRef < HTMLDivElement > ( ) ; useLayoutEffect CARDINAL ( ( ) => { } , [ ] ) ; return ( < div ref = { elRef } dangerouslySetInnerHTML = { { __html : markup } } </ div > ) ; }

Reaching for useLayoutEffect CARDINAL () 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.

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 ORG node. But there’s a cleaner way to pull this off: a document fragment, 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 ORG . First ORDINAL , 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 ORG node we’re building the component around, and finally, create the fragment. Here’s what we’ll put into that useLayoutEffect() hook:

const range = document . createRange ( ) ; range . selectNode ( elRef . current ) ; 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 ORDINAL , 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:

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:

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.

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

import { useRef, useLayoutEffect } from ‘react’; export function DangrousElement ORG ({ markup }) { const elRef = useRef<HTMLDivElement>(); useLayoutEffect CARDINAL (() => { 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= GPE {{ __html: markup }} </div> ); }

Now, we can see that script execute like desired:

It’s uncommon to see, but choosing useLayoutEffect CARDINAL () here is important. This hook fires before the component has a chance to be rendered (and painted) to the screen. This differs from useEffect( ORG ) , 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 ORG . To satisfy your curiosity, here’s a modified version of the code string:

const markup = ` <span id="message">First!</span PERSON > <script> synchronousWait(1500); document.getElementById("message").innerText = " Second ORDINAL !"; </script> ` ;

I’ve made a synchronousWait() method that blocks the main thread for 1.5 seconds TIME . 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.

Replacing that with useLayoutEffect CARDINAL () 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.

You’ll notice our log fired once, which is what’d we’d expect in a production environment. But with React PERSON ‘s strict mode enabled, useLayoutEffect CARDINAL () will fire twice 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 ORG environment here. It’s not much – just another useRef() hook and some value setting/getting.

Connecting to blog.lzomedia.com... Connected... Page load complete