One common way to embed a JavaScript application in a web page is using iframe. Assuming the application is accessible from https://example.com/my-app and the embedding page is https://example.com/content, the embedding code in the embedding page would look like this:

<body>
  <!-- Other content... -->
  <iframe src="https://example.com/my-app"><iframe>
  <!-- Other content... -->
</body>

This approach works for an application written in either vanilla JavaScript or any frameworks, like React or Vue. However, this risks two potentially undesired effects:

  • Other websites may embed this application with iframe without permission.
  • The visitor may visit the embedded iframe directly and thus have skipped the content of the parent page.

This post discusses solutions that address these two problems.

For the impatient, feel free to jump to the conclusion.

Prevent Other Websites from Embedding

To prevent another website from embedding the application, we only need to make sure that the domain name of the topmost window, accessible from window.top, is not from any other website. If the topmost window is from a different domain, the application redirects to the embedding page. We now refer to such a website with a different domain that attempts to embed the application as an unauthorized website.

Utilize the Same-Origin Policy

Thanks to the same-origin policy, if the topmost window is from a different origin, accessing many properties in window.top raises DOMException. Then we have the following code:

// Helper function that redirects the embedded app to my site.
function redirectToMySite() {
  window.location.href = "https://example.com/content";
}

function guardEmbedding() {
  const topLocation = window.top?.location;

  // Same-origin policy
  try {
    topLocation.hostname;
  } catch (e) {
    if (e instanceof DOMException) {
      console.error("Access to this app from an unknown host is prohibited.");
      redirectToMySite();
      return;
    }
  }
}

guardEmbedding();

If another website attempts to embed this application, topLocation.hostname will raise DOMException. Once we catch this exception, we know the app is now being embedded from an unauthorized website.

Further Verify the Domain Name

Utilizing the same-origin policy as above prevents the most basic form of embedding, but it may still be insufficient. The unauthorized website can proxy the application from the domain of the unauthorized website, to trick the browser into treating the application as being hosted from the same domain as the unauthorized website. In this case, the code snippet from the previous subsection would not raise DOMException. Hence, we still need to verify the domain from the top window:

function guardEmbedding() {
  const topLocation = window.top?.location;
  // ...
  if (
    topLocation.hostname !== "example.com" &&
    topLocation.hostname !== "localhost" // For local debugging
  ) {
    redirectToMySite();
    return;
  }
}

Prevent Visitors From Directly Visiting

To prevent a visitor from directly visiting the application, one can check whether the topmost window’s path is the same as that of the application:

function guardEmbedding() {
  const topLocation = window.top?.location;
  // ...
  if (topLocation.pathname.startsWith("/my-app")) {
    redirectToMySite();
    return;
  }
}

Extra Hardening

Since window.top may be null under some exceptional circumstances, we can add an extra check:

function guardEmbedding() {
  const topLocation = window.top?.location;
  if (topLocation === undefined) {
    redirectToMySite();
    return;
  }
}

Conclusion

Putting the above code together, we have the following:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
// Helper function that redirects the embedded app to my site.
function redirectToMySite() {
  window.location.href = "https://example.com/content";
}

function guardEmbedding() {
  const topLocation = window.top?.location;
  if (topLocation === undefined) {
    redirectToMySite();
    return;
  }

  // Same-origin policy
  try {
    topLocation.hostname;
  } catch (e) {
    if (e instanceof DOMException) {
      console.error("Access to this app from an unknown host is prohibited.");
      redirectToMySite();
      return;
    }
  }

  // Verify top window domain name
  if (
    topLocation.hostname !== "example.com" &&
    topLocation.hostname !== "localhost" // For local debugging
  ) {
    redirectToMySite();
    return;
  }

  // Prevent visitors from directly visiting
  if (topLocation.pathname.startsWith("/my-app")) {
    redirectToMySite();
    return;
  }
}

guardEmbedding();

Demo

This demo is a simple application that toggles the text of a button once the visitor clicks on it, as embedded in an iframe below:

The embedded application contains an adapted version of the code snippet above. You can visit the application directly by right-clicking on the embedded application and opening the frame in a new tab, or via this link. When you do that, you’ll see your browser redirects to this page. Embedding from another domain will also result in redirection.