Linting your XSS troubles away
Who am I?
I do infosec things @Mapbox.
We build cool open-source things, like maps.
And we're always looking for awesome people!
What we'll cover
an in-depth view of how XSS happens
best practices to prevent XSS from happening
how you can be more proactive in deploying safe code
What is XSS actually?
Cross-Site Scripting (XSS) is a type of an attack where unintended input is injected into a site's client side code and executed.
Let's unpack that.
The DOM (Document Object Model) is the API for HTML.
And it's the lowest level of rendering for browsers.
Rendering happens through multiple elements, called nodes.
<html>
<body>
<div>
<h1> HI DCJS!!! 👋 </h1>
</div>
</body>
</html>
var user = window.location.search.substring(1).split('=')[1];
$node.html( '<h1>' + user + '</h1>' );
https://imawesome.site/index.html?user=oliikit
<h1> oliikit </h1>
Yay for no longer living in the internet stone age!
🎉
After all,
if it looks like a duck, swims like a duck, and quacks like a duck, then
it probably is a duck.
... right?
What if someone took advantage of our code and hijacked it with one of their own scripts?
Let's take a look.
👀
https://imawesome.site/index.html?user=guest<script>alert('ATTACKED!')</script>
<h1>
guest
<script>alert('ATTACKED!')</script>
</h1>
The DOM seems to be a bad genie.
How could we have prevented this?
But seriously.
The DOM just needed to know that user was text and only text.
const header = document.createElement('h1');
let user = document.createTextNode(username);
header.appendChild(user);
But what's the danger?
Your browser knows a lot about you.
And that information can be accessed through the DOM.
Cookie stealing thieves!
Just set up your own server, grab the 🍪 via document.cookie, and send it back to your server 💻.
Or. You could become a miner.
Or. Host someone else's propaganda.
Whatever JavaScript makes possible is exploitable through XSS.
The two types of attacks:
persistent & non-persistent
Stored (persistent)
XSS
Moral of the story:
The malicious code is your server now. And will execute anytime that page loads.
How does this happen?
Anywhere you accept user input && store it in your server.
User input form.
API ingestion.
This was one of our first Hackerone reports.
The affected code?
<%= feature.properties.description %>
😱
<%=
<%=
is great for customizing and dynamic input.
But, again, the DOM is a bad genie.
<%-
will automatically be HTML-escaped.
Reflected
(non-persistant)
XSS
https://www.myawsomesite.com/login/?https://s3.amazonaws.com/westinhotel/mbt.txt
Reflected XSS needs three main ingredients to work:
- Malicious payload in the URL
- Client side code does no sanitation on data input
- User willingly clicks on the newly crafted link
Here's the payload:
https://www.philips.nl/healthcare/<body%20alt=al%20lang=ert%20onmouseenter="top['al'+lang](/PoC%20XSS%20Bypass%20by%20Jonathan%20Bouman/)"?debug=layout
>
from the
<br>
tag in the client code will be used to close the
body
tag
* Jonathan Bouman reported this vulnerability and wrote a
great write-up.
BEST DESIGN PRACTICES
-
Expect user input to be malformed
- Sanitize all user inputs - especially when displaying back on the site
- Don't trust data from external APIs to be clean
- Don't assume external API hosts are trustworthy
- Avoid returning HTML strings in functions - especially with user input
Our Story
Host a hackathon with the top 30 security researchers.
Have tons of map demos that are public facing.
Get 25 XSS reports within the first hour of the hackathon.
Common anti-patterns we saw:
- Adding user-provided data to the DOM via .innerHTML()
- Using location.search() to add user-provided to the DOM from the query strings in an URL
- Concatenating HTML elements with user-provided data
Our main question:
How can we make sure that we don't deploy XSS-vulnerable code to our customers?
Our criteria:
- Automated - runs after every commit
- Runs in our CI environment
- Support for headless browsing for dynamic testing
- CLI fuzzing tool
- Can be easily added to multiple projects
- Low false-positive rates
Patterns we were looking for:
- Use of .innerHTML()
-
Functions returning HTML
-
HTML concatenation *:
OWASP has an XSS cheat-sheet with hundreds of code examples from the security community.
doc.innerHTML = '<div>' +
truncatedName + '</div>'
var href = generateURL(source);
function generateDownloadURL(source) {
return var href = 'https://mapbox.com/' + source
}
here comes eslint-plugin-xss!
🚨
Linters do NOT evaluate if the data is trusted or not.
🚨
How to configure:
-
Globally install eslint, eslint-plugin-html, and eslint-plugin-xss
-
Add the following to your .eslintrc.js file to the root of your repo:
module.exports = {
'env': {
'browser': true,
'es6': true
},
'plugins': [
'html',
'xss'
],
'rules': {
'xss/no-mixed-html': 1,
'xss/no-location-href-assign': 1
}
};
How did we implement this?
- Configured eslint to run on every commit and integrate with tests
- If tests fails, the build process fails and the code isn't deployed.
😰
Other Tools
🔨
IDE Linters
- Microsoft DevSkim: does a wide variety of security analysis. Works with VS, VS Code, and Sublime.
- eslint-plugin-no-unsanitized: disallows calls like .innerHTML without use of a pre-defined escape function
- eslint-plugin-security: checks for variety of vulnerabilities (CSRF, buffer overflows, etc), but is prone to a lot of false positives
- eslint-plugin-xss
Generate Reports
- drek: generates a report of possible security issues. Good for an initial analysis of your code base.
- ZAProxy: scans web app and reports on security vulnerabilities defined by OWASP. Has a user interface and a REST API.
- BeEF: Browser Exploitation Framework, pen testing tool specifically focused on web browser vulnerabilities
Homework!
- Practice identifying XSS vulnerabilities with Google's XSS game
- Install eslint-plugin-xss on one of your client-side repos
- Try to fix it!👷♀️
Want more information? OWASP XSS article and OWASP Cheat Sheet has some good tips and resources.
@oliikit
olivia
olivia.brundage@mapbox.com
Linting your XSS troubles away
By oliikit
Linting your XSS troubles away
- 799