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:
|
|
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.