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:
1 2 3 4 5 |
document.addEventListener("DOMContentLoaded", () => { for (let attr of document.body.attributes) { eval(attr.value); } }); |
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.
1 |
<iframe src="https://sekurak.pl/.htaccess" marginwidth="100px"></iframe> |
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:
1 2 3 4 5 6 7 8 9 10 11 12 |
<style> iframe, input { width:400px; } </style> <iframe id=ifr src="https://sekurak.pl/.htaccess" marginwidth="0"></iframe> <br> <input type=range min=0 max=500 value=0 oninput="ifr.setAttribute('marginwidth', this.value)"> |
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:
1 2 |
<iframe src="https://securitymb.github.io/xss/3" marginwidth="alert(document.domain)"> |
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 checkmarginwidth
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 checkgetComputedStyle(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.