BattlefyBlogHistoryOpen menu
Close menuHistory

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

Ronald ChenApril 11th 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 transferring 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 is 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 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%20src=&quot;https://via.placeholder.com/1&quot;%20onload=&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 shut down the rock star developer who wants to forgo the framework.

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

2022

Powered by
BATTLEFY