Marginwidth/marginheight – the unexpected cross-origin communication channel

On 6th July 2020 I’ve announced a XSS challenge on my Twitter. So far only four people were able to solve it and every single one of them told me that they had never heard about the quirk used in the challenge before. So here’s a writeup explaining this quirk along with some backstory.

The core of the challenge was in the following lines of JavaScript:

The code just iterates over all attributes of the <body> element and evaluates values of all these attributes as JavaScript. Because there was no other sources in the challenge, it meant that solving it requires finding a way to inject arbitrary attribute value into the document.body. So how’s that possible?

It all started when I noticed an interesting snippet in the HTML specification. The 14th section of the spec, called “Rendering”, describes default styles for some elements. For instance it says that <style> or <script> elements are not displayed by default (that is, they have display:none). The interesting bit was how margin of <body> is determined.

The table says that if the <body> has an attribute called marginheight then it maps to the margin-top CSS property of the element. If it doesn’t exist, then topmargin attribute is checked. If it doesn’t exist either, then (and here’s the surprise), if current page is in a nested browser context (so <frame> or <iframe>) browser looks at the marginwidth attribute of the container element. This also works cross-origin, which is directly admitted in the spec:

At first, I thought that this is a historical artifact and that no modern browser actually implements it this way.

Browsers behavior

To test browsers behavior I had a simple code, which lets me check whether the marginwidth attribute is taken into account.

Chromium

In Chromium, the marginwidth attribute is reflected in the <body> element, but it is parsed to integer before. What’s interesting is that Chromium listens to changes of this value, so if you change it dynamically, it is also reflected in the iframe. Here’s an example:

Firefox

In Firefox, the value of <iframe marginwidth> is not reflected in the nested document DOM tree at all. But it is taken into account and could be retrieved via getComputedStyle(). So the example with the slider works exactly the same way as in Chromium.

Safari

In Safari, the value of <iframe marginwidth> is reflected in the nested <body> element without any modification.

Contrary to Firefox and Chromium, Safari doesn’t listen to changes of the attribute, hence the slider example wouldn’t work.

Challenge solution

So, the solution of the challenge is as simple as:

Congratulations to @terjanq, @shafigullin, @BenHayak and @steike for finding the expected solution!

For those who tried to find the solution but didn’t manage to; the hint was in a bullet that said “it might be marginally better to use Safari” 😀.

Marginwidth/marginheight as cross-origin communication channel

An interesting “side-effect” of marginwidth/marginheight is the possibility to use the attributes as cross-origin communication channel. This can be done in every browser:

  • In Safari, just set marginwidth in the parent and check marginwidth of the <body> in the child.
  • In Chrome, set marginwidth byte by byte in the parent, and observe mutation of <body marginwidth> attribute in the child
  • In Firefox, set marginwidth byte by byte in the parent, and check getComputedStyle(document.body).marginLeft in the child.

I implemented it and hosted at https://cdn.sekurak.pl/marginwidth.html:

Summary

I think the main take-away from this article is that HTML spec still has some hidden gems that might be possible in some obscure attacks.

Also I think that marginwidth specifically has some potential for XS-Leaks but I couldn’t find a viable scenario.