useIframeBuster React Hook

React Hooks are awesome. I know there’s a lot of criticism levelled against it recently: it’s a bit hard to grok its mental model and so on, but I fell in love with them the day they were introduced. For one simple reason: it’s now possible to compose side-effects the same way you could compose components earlier. The community can build & share these hooks now, and it’s less code that you have to write. So here’s the story of how I extracted out a generic useIframeBuster hook for when you have to bust out of iframes.

Step 1 is to Google

How to bust out of iframes” literally which leads you to this piece of code:


if (top.location!= self.location) {
   top.location = self.location.href;
}

Convert this to a Hook

useEffect comes to the rescue here:


import { useEffect } from "react";

const useIframeBuster = () => {
  useEffect(() => {
    const script = document.createElement("script");
    script.innerHTML = `
      (function () {
        'use strict';
        if (window.location !== window.top.location) {
            window.top.location = window.location;
        }
    })();`;
    script.async = true;

    document.body.appendChild(script);

    return () => {
      document.body.removeChild(script);
    };
  }, []);
};

export default useIframeBuster;

It’s pretty simple code:

  1. useEffect is used with a [] as a parameter which means this will run only on first component render.
  2. And when it does, it executes the code we found via googling! There’s a bit of manual element creation here, but that’s what useEffect is for: it isolates these side-effects from our regular React component model.
  3. To use this effect, a component just needs to import useIframeBuster, and then do a simple useIframeBuster() somewhere in its exported function. Simple, and all the ugly details are wrapped in this easily testable function.

Think of the UX

We can pause here and think this is compete, but let’s think of what happens to a page that has this effect loaded. It’ll render completely once, then bust out and then render again once the script does its job and the bust out is complete. That’s very bad UX. We should add a simple Redirect component:


import React, { useEffect } from "react";
import useIframeBuster from "./effects/useIframeBuster";
import { useLocation } from "react-router-dom";
import isValidUrl from "isValidUrl";

const RedirectWithBustOut = () => {
  useIframeBuster();
  let location = useLocation();
  let path = new URLSearchParams(location.search).get("path") ?? "";

  /**
   * If path is a valid URL, we set path to "/" to avoid
   * Open Redirection Vulnerability, see:
   * https://dzone.com/articles/what-is-an-open-redirection-vulnerability-and-how
   */
  if (isValidUrl(path)) {
    path = "/";
  }

  useEffect(() => {
    setTimeout(() => {
      window.location.href = `${path}`;
    }, 1000);
  }, []);

  return (
    <>
      <p>Loading...</p>
    </>
  );
};

export default RedirectWithBustOut;

We can then invoke this component like so:


<RedirectWithBustOut path="/index" />

& the user will see a Loading... indicator for a while before the page busts out and the path passed in renders. Notice some details:

  1. There is a wrapped setTimeout() that waits a second (you can reduce this time) to make sure the page fully loads before the path is changed. This gives our useIframeBuster enough time to bust out of the page.
  2. There is a possible Open Redirection vulnerability here that we have to mitigate.

As you can see it’s pretty simple code, and we’ve built out an abstraction that you can now reuse hundreds of times. Good stuff!

Leave a Reply

Create a website or blog at WordPress.com