Insights
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:
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.
To test browsers behavior I had a simple code, which lets me check whether the marginwidth
attribute is taken into account.
<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:
<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.
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.
So, the solution of the challenge is as simple as:
<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" 😀.
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:
marginwidth
in the parent and check marginwidth
of the <body>
in the child.marginwidth
byte by byte in the parent, and observe mutation of <body marginwidth>
attribute in the childmarginwidth
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:
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.
Within last year I shared a a few writeups of my bypasses of HTML sanitizers, including: > Write-up of DOMPurify 2.0.0 bypass using mutation XSS > Mutation XSS via namespace confusion – DOMPurify < 2.0.17 bypass While breaking sanitizers is fun and I thoroughly enjoy doing it, I reached a point where I began to think whether I can contribute even more and propose a fix that will kill an entire class of bypasses.
A few days ago, the Anaconda project announced the PyScript framework, which allows Python code to be executed directly in the browser. Additionally, it also covers its integration with HTML and JS code. An execution of the Python code in the browser is not new; the pyodide project has allowed this for a long time...
Summary: During my research on other bug bounty program I've found Cross-Site Scripting vulnerability in cmp3p.js file, which allows attacker to execute arbitrary javascript code in context of domain that include mentioned script. Below you can find the way of finding bug bounty vulnerabilities from the beginning to the ...