How to sneak in a XSS exploit in 4 steps or how to detect said attempt

Ronald Chen
Battlefy
Published in
4 min readApr 11, 2022

Cross-site scripting (XSS) attacks are very serious. When fully exploited, it gives one full control of user account on websites. Imagine somebody gaining access to your bank account and transfering all the money out. From the bank’s perspective, this would be a perfectly fine legitimate operation from the account owner.

Let’s consider a series of steps, each of which seem reasonable, but in the end leads to a XSS attack. The story begins with a large single page application and we’re implementing a new feature, user profile.

Step 1. Argue new feature should be its own microapp

Surely we don’t want new users on our landing pages to have to load the user profile? Just a few hundred milliseconds will cost us millions of dollars.

Let’s separate the user profile into its own microapp. We don’t need to be bogged down by our main app.

Aside: There is a lot of “micro-frontend” lingo floating around and there is little agreement on what it even is. For the purposes of this story, microapp is an independent single page application. It has its own index.html and JavaScript bundle. In Webpack terms, it is another entry point. In Vite terms, it is a multi-page app. Demo of a microapp.

On face value, step 1 is very reasonable. Microapps reduce deployment risk being an independent single-page application. The increased delay with a real page load acceptable when we’re switching context as such with user profile.

Step 2. Argue microapp is so small it doesn’t need a framework

Surely we don’t need to load a heavy framework like React for a simple user profile page? Our JavaScript bundle would be way smaller and avoids supply-chain attacks if we just wrote this feature in Vanilla JavaScript. Surely we don’t want our users to suffer slow load times or have their security compromised.

The rhetoric laid on thick here because this argument is insidious. Frameworks exist beyond to speed up development, but also to allow everybody to benefit from security expertise they may not have.

One doesn’t even need to think about XSS in React as long as they avoid dangerouslySetInnerHTML. The reason why a real attacker needs to advocate against frameworks like React is because they have done such a good job with APIs like dangerouslySetInnerHTML. These scary looking APIs are easy to spot in pull requests or automatically with linters.

Step 3. Implement XSS mitigation with design flaw

No, no! Don’t worry about XSS. We’ll implement our own mitigation! Look, we’ve property implemented escaping of user input when modifying the DOM.

const escape = (userInput) => String(userInput)
.replaceAll('&', '&')
.replaceAll('<', '&lt;')
.replaceAll('>', '&gt;')
.replaceAll('"', '&quot;')
.replaceAll("'", '&apos;');
function createElement(tag, props, children) {
const attributes = Object.entries(props ?? {})
.map(([attribute, value]) =>
`${attribute}="${escape(value)}"`
)
.join(' ');
return `
<${tag} ${attributes}>
${escape(children)}
</${tag}>
`;
}

At first glance, the code is very reasonable. But there are 2 possible XSS vectors, do you see them?

The escape function is fine. It is essentially what setting textContent would be. While attribute value and children have been properly escaped, the tag and attribute name have not been. How would an attacker exploit this?

Attacking the tag is unlikely, as there is little real-world reason on why HTML tags would be dynamic, however the attribute name can be abused with data attributes.

Step 4. Exploit design flaw in poor XSS mitigation

Surely, we would want user data to be visible as data attributes. This would speed up development as it would be trivial to see the state of the app by simply looking at the the DOM tree in dev tools! We wouldn’t need to implement a custom API to extract data for end to end test automation. It could just read the data directly off data attributes. It could even handle custom field for the user profile.

...
<body>
<div id="root">
<div
id="profile"
data-name="Alice"
data-fields="tagline,fav-food"
data-tagline="No risk, no reward"
data-fav-food="Cake"
>
</div>
...

This may seem harmless, but now that custom fields is user input that can end up in attribute name, we have a XSS vector. Now with a custom field name that closes attribute and div, it can insert an img with an onload exploit.

Aside: innerHTML injected script tags are not run by design. This is why we need to use this awkward img onload method.

...
<body>
<div id="root">
<div
id="profile"
data-name="Alice"
data-fields="field=&quot;&quot;><img src=&quot;https://via.placeholder.com/1&quot;
onload=&quot;alert(document.domain)&quot;
/>"
data-field=""
>
<img
src="https://via.placeholder.com/1"
onload="alert(document.domain)"
>
="hacked"&gt;
</div>
...

Aside: alert(1) considered harmful.

See full source and demo.

In the real-world, the author of step 3 may not be the same person as the author of step 4. Steps 1 through 3 could be completely innocent with good intentions. This is why preventing XSS requires vigilance beyond “just use a framework lol”. We need to understand how frameworks protect us and why we should shutdown the rockstar developer who wants to forgo the framework.

Do you want to prevent XSS attacks? You’re in luck, Battlefy is hiring.

--

--