August 29, 2026
How I Turned Self-XSS into Reflected XSS (and Bypassed the WAF)
Learn how I turned a restricted Self-XSS into a Reflected XSS vulnerability by bypassing WAF limits using CSRF.

By Muhammad Nur Ibnu Hubab
6 min read
ุจุณู ุงููู ุงูุฑุญู ู ุงูุฑุญูู
DISCLAIMER:_ _This write-up is shared strictly for educational and informational purposes only.
During one of my recent bug bounty hunts on a private program at Bugcrowd, I had the opportunity to analyze an e-commerce platform ([REDACTED]). While looking for potential security flaws, I stumbled upon a fascinating behavior in the search feature that allowed me to chain a Self-XSS into a fully functional Reflected XSS using CSRF.
In this write-up, I will walk you through how the vulnerability works, how I bypassed the WAF, and how it can be remediated.
Phase 1: Discovering the Search Behavior
As is standard with almost any e-commerce application, the search functionality was one of the first areas I targeted. I wanted to see how user input was handled by the application.
I started by injecting a simple HTML injection payload, <h1>test</h1>, into the search bar and executed a search query.
- URL Generated: https://[REDACTED]/search/?q=%3Ch1%3Etest%3C%2Fh1%3E
- Behavior: The input was reflected directly onto the page, but rendered safely as raw plain text:
At this point, it looked like standard output encoding. However, I decided to explore the application's behavior further across different viewports. I noticed a distinct UI difference between desktop and mobile layouts:
- Desktop view: Displays a full search input bar directly.
- Resized View (< 1280px): When the browser window is resized to a width smaller than 1280px, the desktop search bar is replaced by a search (magnifying glass) icon. Clicking this icon opens an interactive search interface and preview box containing search suggestions.
Curious about how input was handled in this specific UI state, I tested the <h1>test</h1> payload again while the window was resized below 1280px. Surprisingly, inside the search suggestion preview, it was rendered as actual HTML rather than plain text!
This was my turning point. It was time to hunt for an XSS payload.
Phase 2: Finding the XSS Payload
Now that I had confirmed that the search suggestion component was actually rendering my input as HTML, I had a much more interesting question:
Could I turn this HTML injection into JavaScript execution?
I started testing different XSS payloads. Most of the common payloads were immediately blocked by the application's Web Application Firewall (WAF) or filtered before they reached the vulnerable rendering point.
So instead of repeatedly throwing the same payloads at the application, I started experimenting with different HTML syntax, casing, and event-handler structures.
Eventually, I found a payload that successfully bypassed the filtering:
<Img Src=OnXSS onerror=alert(document.cookie)><Img Src=OnXSS onerror=alert(document.cookie)>And this time, something interesting happened.
The payload was accepted and rendered by the search suggestion component, and the JavaScript executed successfully.
XSS confirmed.
But there was still a problem.
A fairly important one.
The Problem: Was This Really Reflected XSS?
At this point, I could execute JavaScript.
But I wasn't satisfied.
I asked myself:
"Who controls the payload?"
The answer wasโฆ me.
I had to manually enter the payload into the search field myself.
That meant I couldn't simply send someone a URL containing the payload and expect their browser to execute it automatically.
In other words, I had demonstrated JavaScript execution, but the attack still depended on the victim manually injecting the payload.
That made the finding much closer to a Self-XSS than a conventional Reflected XSS.
And this became the next challenge.
If the XSS payload works, how can I make the attacker control the input instead of the victim?
I didn't want to stop at "Self-XSS confirmed."
I started looking at the entire request flow again.
The vulnerable value was coming from the q parameter:
/search/?q=PAYLOAD/search/?q=PAYLOADSo I tried taking the exact payload that had worked in the search interface and putting it directly into the URL:
https://[REDACTED]/search/?q=<Img Src=OnXSS onerror=alert(document.cookie)>https://[REDACTED]/search/?q=<Img Src=OnXSS onerror=alert(document.cookie)>But the result was different.
The WAF immediately detected and blocked the request.
Instead of allowing the complete payload through, the response was reduced to something like:
This was actually a useful clue.
The issue wasn't that the application couldn't render the payload.
I already knew it could.
The problem was that the payload was being filtered when delivered through the q parameter.
So I started thinking about the problem from another angle.
Instead of asking:
"How do I make the WAF accept this URL?"
I asked:
"Can I make the victim's browser send the request for me?"
That question led me to revisit the request itself.
If I could cause another user's browser to submit a request containing my attacker-controlled q parameter, I wouldn't need the victim to manually type anything into the search box.
The vulnerable component was already there.
The payload already worked.
I just needed to solve the delivery mechanism.
And that's where the next phase began.
Phase 3: Turning the Vulnerability into an Attacker-Controlled XSS
At this point, I had three separate pieces:
- The search suggestion component rendered HTML.
- I had a payload capable of executing JavaScript.
- The vulnerable input ultimately came from the
qparameter.
The missing piece was getting an attacker-controlled value into that parameter without requiring the victim to type it themselves.
I started looking at whether the search endpoint could be triggered through a cross-origin request.
Using Burp Suite, I captured a normal search request and examined how the application processed the q parameter.
From there, I generated a simple CSRF Proof of Concept.
Here is small piece of the code:
The important difference was that the victim no longer had to type the payload themselves.
The attacker's page could cause the victim's browser to request:
https://[REDACTED]/search/?q=PAYLOADhttps://[REDACTED]/search/?q=PAYLOADThe vulnerable search functionality would then receive the attacker-controlled value.
At this point, the attack chain looked like this:
Attacker-controlled page -> CSRF request ->/search/?q=ATTACKER_PAYLOAD -> Search suggestion component -> JavaScript execution
There was one more important detail.
The vulnerable rendering behavior only appeared in the responsive search interface, so the victim's browser needed to be in the affected UI state for the payload to reach the vulnerable rendering path.
Once that condition was met, the payload executed without the victim having to manually enter it.
That was the point where the original Self-XSS primitive became much more interesting.
What initially looked like:
Victim manually enters payload -> XSS executes
could now be demonstrated as:
Attacker-controlled request ->Victim's browser -> Vulnerable q parameter -> HTML-rendering search component -> JavaScript execution
Self-XSS was no longer the end of the investigation. It was the starting point for finding an attacker-controlled delivery path.
The interesting lesson here wasn't simply "I found an XSS payload."
It was understanding who controls the input, how that input reaches the vulnerable sink, and whether an attacker can influence that flow remotely.
Reporting and Outcome
I documented the full reproduction steps, recorded a PoC video, and submitted the report to the Bugcrowd program team.
Unfortunately, the report was marked as a Duplicate of an existing submission :)
While it stings a bit not to get the bounty, the learning experience, the thrill of chaining vulnerabilities, and the deep understanding gained throughout this process were entirely worth it.
Key Takeaways & Remediation
Here are a few valuable lessons I learned from this finding:
- Test Different Viewports and UI States: Never assume input is handled the same way across all layouts. Responsive components (like desktop vs. resized views, popups, or search suggestion boxes) often use different DOM-rendering mechanisms or frontend frameworks that might introduce sanitization gaps.
- Don't Stop at Self-XSS: If you find a Self-XSS, treat it as a stepping stone rather than a dead end. Look into chaining techniques โ such as CSRF, Clickjacking, or open redirection โ to elevate its impact into a high-severity Reflected or Stored XSS.
- Experiment with WAF Bypasses: WAFs and filters rely heavily on signatures. When standard payloads like
<img src=x onerror=alert(1)>are blocked, experiment with alternative casing, different tags, or semantically equivalent attributes to test how strict the input validation truly is.
To prevent this kind of vulnerability, developers should ensure:
- Context-Aware Output Encoding: All user input, especially inside search suggestions and dynamic UI components, must be strictly HTML-encoded.
- Robust CSRF Defenses: Implement anti-CSRF tokens on state-changing and search-related parameters where applicable.
- Defense-in-Depth: Never rely solely on WAF filtering to block XSS, as alternate syntax and casing can often bypass signature-based rules. If you are actively hunting or looking to level up your bug bounty game, here are a few valuable lessons I learned from this finding:
Thank you for reading! Keep hacking and never stop learning.
discover me!
Website: https://nuribnuu.vercel.app/
LinkedIn: https://www.linkedin.com/in/nuribnuu/