August 26, 2026
Cloudflare WAF Bypass → DOM-Based XSS via Unsanitized iframe.src
Introduction
By redhunter01
9 min read
Introduction
One of the most useful lessons in bug bounty hunting is that a vulnerability is not always obvious from the initial request.
Sometimes an application appears to safely handle a parameter, the WAF blocks the obvious payload, and everything looks secure — until you trace what happens to that parameter after server-side rendering, URL decoding, client-side hydration, and DOM manipulation.
This finding came from exactly that kind of investigation.
The vulnerable functionality involved a parameter named whatsAppReturn on the /girls endpoint. The value eventually reached an iframe's src attribute without protocol validation:
iframe.setAttribute('src', decodeURIComponent(whatsAppReturn))iframe.setAttribute('src', decodeURIComponent(whatsAppReturn))That created a JavaScript URL execution primitive.
The interesting part was not simply finding the XSS sink. The more important discovery was that a basic payload was filtered, while an equivalent representation using a tab character survived the filtering layer and still became executable in the browser.
The final result was arbitrary JavaScript execution in the target's origin.
The impact initially appeared to suggest account takeover, but deeper testing showed that the primary authentication cookie was protected with HttpOnly. After validating the actual browser behavior, the impact was correctly reduced to the XSS itself and the realistic consequences demonstrated by it.
That distinction ended up being an important lesson in responsible vulnerability validation.
1. The Starting Point
The first interesting endpoint was:
/girls/girlswith:
isWhatsApp=1
whatsAppReturn=<value>isWhatsApp=1
whatsAppReturn=<value>The parameter immediately caught my attention because its name suggested that it might contain a URL used for redirecting or embedding a WhatsApp-related destination.
Whenever I see a parameter that sounds like:
returnredirecturlnextcallbacktargetcontinuedestination
I usually ask one question:
Where does this value eventually end up?
A parameter that is harmless in one context can become dangerous if it reaches:
location =
window.open(...)
iframe.src =
script.src =
element.innerHTML =
element.setAttribute(...)location =
window.open(...)
iframe.src =
script.src =
element.innerHTML =
element.setAttribute(...)So instead of focusing only on the HTTP response, I followed the parameter through the application's JavaScript.
2. Tracing the Data Flow
The important discovery was inside the production JavaScript bundle.
The application eventually performed:
iframe.setAttribute('src', decodeURIComponent(whatsAppReturn))iframe.setAttribute('src', decodeURIComponent(whatsAppReturn))This immediately raised several questions.
Question 1 — Is the value controlled by the attacker?
Yes.
The parameter is supplied directly through the URL.
Question 2 — Is it decoded before reaching the sink?
Yes.
decodeURIComponent(...)decodeURIComponent(...)Question 3 — Is there protocol validation?
No validation was visible before the value reached the iframe.
Question 4 — Can an iframe src contain a dangerous URI scheme?
Yes.
That makes the following data flow particularly interesting:
Attacker-controlled URL
↓
whatsAppReturn
↓
decodeURIComponent()
↓
iframe.setAttribute("src", ...)
↓
browser interprets URI
↓
JavaScript executionAttacker-controlled URL
↓
whatsAppReturn
↓
decodeURIComponent()
↓
iframe.setAttribute("src", ...)
↓
browser interprets URI
↓
JavaScript executionAt this point I had a potential XSS sink.
But finding a sink is only the beginning.
3. Testing the Obvious Payload
The first thing I tested was the obvious JavaScript URI conceptually:
javascript:<payload>javascript:<payload>The application/WAF did not simply allow the straightforward representation.
This is where I changed my mindset.
Instead of asking:
"Why doesn't my XSS payload work?"
I asked:
"Which layer is stopping it?"
That distinction is extremely important.
There are usually several independent layers involved:
Request
↓
CDN / WAF
↓
Server
↓
SSR state
↓
Client-side JavaScript
↓
DOM
↓
Browser parserRequest
↓
CDN / WAF
↓
Server
↓
SSR state
↓
Client-side JavaScript
↓
DOM
↓
Browser parserA payload can be rejected by one layer while remaining completely valid to another.
4. Looking for Parser Differential
The key breakthrough came from considering how browsers parse whitespace and how security filters detect dangerous protocols.
The application was ultimately interested in the decoded URI.
Instead of relying on a literal:
javascript:javascript:I tested an encoded control character between the protocol name and the colon.
The working proof used a tab character:
java%09script:java%09script:The resulting PoC demonstrated that the browser ultimately interpreted the value as an executable JavaScript URI.
The important lesson here is broader than this particular payload.
WAFs and browsers may normalize input differently.
For example:
Attacker input
↓
WAF normalization
↓
Application decoding
↓
DOM assignment
↓
Browser URL parsingAttacker input
↓
WAF normalization
↓
Application decoding
↓
DOM assignment
↓
Browser URL parsingIf those components disagree about normalization, security controls can sometimes be bypassed.
This is why testing only the canonical payload is often insufficient.
5. The Working Proof of Concept
A minimal proof of execution was used rather than immediately attempting credential theft.
The PoC concept was:
/girls?isWhatsApp=1&whatsAppReturn=<encoded-JavaScript-URI>/girls?isWhatsApp=1&whatsAppReturn=<encoded-JavaScript-URI>The JavaScript changed a harmless browser-controlled property so execution could be verified without accessing or transmitting sensitive information.
For example, the proof demonstrated execution by assigning a value to a property on the top-level window.
The observed result was equivalent to:
top.__SC_WA_CF_PWN_535 = 1top.__SC_WA_CF_PWN_535 = 1Then:
window.__SC_WA_CF_PWN_535window.__SC_WA_CF_PWN_535returned:
11That gave me a clean execution primitive.
6. Why This Was Definitely XSS
There were several independent pieces of evidence.
1. Server-side reflection
The supplied value appeared inside the server-rendered application state.
2. Client-side hydration
The SPA consumed that state during hydration.
3. DOM sink
The value was assigned to an iframe:
iframe.setAttribute('src', ...)iframe.setAttribute('src', ...)4. Browser execution
The iframe interpreted the resulting URI as JavaScript.
5. Same-origin context
The resulting JavaScript executed in the target origin rather than an attacker-controlled origin.
That last point is especially important.
This wasn't simply:
"My URL caused JavaScript somewhere to execute."
The important security boundary was:
Attacker-controlled input
↓
Target application
↓
Target-origin JavaScript executionAttacker-controlled input
↓
Target application
↓
Target-origin JavaScript executionThat is what makes it a meaningful XSS vulnerability.
7. The Reproduction Methodology
The cleanest way to reproduce the issue is to start with a harmless execution marker.
Step 1 — Open the vulnerable endpoint
Use:
/girls?isWhatsApp=1&whatsAppReturn=<PoC>/girls?isWhatsApp=1&whatsAppReturn=<PoC>Step 2 — Open DevTools
Use:
F12 → ConsoleF12 → ConsoleStep 3 — Inspect the hydrated page
Look for the iframe generated by the application.
The important observation is that the attacker-controlled value reaches the iframe's src.
Step 4 — Wait for hydration
The vulnerable DOM operation occurs as the client application initializes.
Step 5 — Verify execution
Check the harmless marker created by the PoC.
If the marker exists in the top-level window, JavaScript execution has been demonstrated.
8. The Interesting Part: Cloudflare Wasn't the Actual Fix
One of the biggest lessons from this investigation was not to confuse:
WAF blocked payloadWAF blocked payloadwith:
application is secureapplication is secureA WAF generally sees a request.
The browser ultimately executes a parsed URL.
Those are two different environments.
The successful path looked conceptually like:
Encoded attacker input
↓
WAF sees unusual representation
↓
Request accepted
↓
Application decodes value
↓
iframe receives decoded value
↓
Browser parses URI
↓
JavaScript executesEncoded attacker input
↓
WAF sees unusual representation
↓
Request accepted
↓
Application decodes value
↓
iframe receives decoded value
↓
Browser parses URI
↓
JavaScript executesThis is a classic example of why parser differentials are worth testing.
Whenever a WAF blocks:
javascript:javascript:don't immediately conclude that the functionality is safe.
Instead investigate:
- URL encoding
- double encoding
- whitespace
- tabs
- newlines
- mixed case
- Unicode normalization
- HTML entity decoding
- application-specific decoding
- browser URL normalization
The key is to understand which transformation happens at which layer.
9. Testing the Real Impact
After confirming XSS, the next question was:
"What can this actually do?"
This is where I initially made an important mistake.
The first assumption was that JavaScript execution might allow direct session theft and therefore account takeover.
I tested the browser storage and cookie environment.
The application exposed several interesting client-side values, including identifiers and application/analytics tokens.
However, the primary authentication cookie was protected using HttpOnly.
That meant:
document.cookiedocument.cookiecould not retrieve the primary session cookie.
Therefore:
XSS ≠ automatically Account TakeoverXSS ≠ automatically Account TakeoverThis is a critical bug bounty lesson.
10. Don't Overclaim Your Impact
Initially, the vulnerability appeared to be an account-takeover issue.
After testing the actual authentication mechanism, that conclusion was not supported.
The correct conclusion was:
Confirmed:
Arbitrary JavaScript execution
Same-origin XSS
DOM manipulation
User interaction abuse potential
Not confirmed:
Direct theft of the primary HttpOnly session cookie
Full account takeoverConfirmed:
Arbitrary JavaScript execution
Same-origin XSS
DOM manipulation
User interaction abuse potential
Not confirmed:
Direct theft of the primary HttpOnly session cookie
Full account takeoverThis distinction matters enormously when writing reports.
A strong report says:
"I confirmed X."
A weak report says:
"I think this could probably lead to X."
The second statement can reduce confidence in the entire report.
In this case, the final report was corrected to reflect the actual evidence.
11. What the XSS Can Realistically Enable
Even without access to the primary authentication cookie, same-origin XSS remains security-relevant.
Depending on the application's functionality and user interaction, an attacker-controlled script could potentially:
Modify the page
DOM manipulation
UI replacement
content injectionDOM manipulation
UI replacement
content injectionRedirect the victim
An attacker could replace or manipulate the page content and navigation flow.
Perform actions in the victim's browser
JavaScript executes with the application's origin, meaning application functionality exposed to that browser context may become accessible.
Interact with forms
If sensitive information is entered into a page controlled by the application, malicious JavaScript may be able to observe or manipulate that DOM state.
Access non-HttpOnly browser storage
Depending on the application's implementation:
localStorage
sessionStorage
non-HttpOnly cookieslocalStorage
sessionStorage
non-HttpOnly cookiesmay contain useful application data.
The exact impact must always be tested rather than assumed.
12. A Mistake That Taught Me a Lot
During testing I experimented with multiple impact scenarios.
Some included:
- token collection
- keyboard-event monitoring
- DOM modification
- form interaction
- phishing-style UI replacement
The important realization was that demonstrating a capability is not the same as proving a practical exploit chain.
For example:
Can JavaScript register a key handler?Can JavaScript register a key handler?is different from:
Can an attacker reliably capture a victim's password during normal usage?Can an attacker reliably capture a victim's password during normal usage?The second requires realistic victim interaction and browser behavior.
The program ultimately determined that the demonstrated keylogging scenario required unrealistic interaction and was not a sufficient reason to increase severity.
That was a fair assessment.
13. The Better Bug Bounty Mindset
This finding reinforced several habits that I now consider extremely important.
Don't stop at reflection
Reflection alone isn't the vulnerability.
Trace:
Input
↓
Transformation
↓
Sink
↓
Parser
↓
ExecutionInput
↓
Transformation
↓
Sink
↓
Parser
↓
ExecutionDon't stop when the WAF blocks you
A blocked payload is a signal to investigate the parsing chain.
Ask:
"Does the application and browser interpret this input the same way the WAF does?"
Understand every decoding step
Here, this line was particularly important:
decodeURIComponent(whatsAppReturn)decodeURIComponent(whatsAppReturn)If you see:
encodeURIComponent
decodeURIComponent
atob
decodeURI
JSON.parse
DOMParser
innerHTML
setAttributeencodeURIComponent
decodeURIComponent
atob
decodeURI
JSON.parse
DOMParser
innerHTML
setAttributeslow down and investigate the transformations.
Every transformation is a potential place for assumptions between security controls to break.
14. A Practical XSS Hunting Workflow
When hunting modern JavaScript applications, my workflow is roughly:
1. Discover interesting parameters
↓
2. Identify URL/redirect/callback parameters
↓
3. Search JavaScript bundles
↓
4. Trace attacker-controlled data
↓
5. Identify DOM sinks
↓
6. Determine every decoding/normalization step
↓
7. Test harmless payloads
↓
8. Identify filtering/WAF behavior
↓
9. Test parser differentials
↓
10. Prove same-origin execution
↓
11. Determine realistic impact
↓
12. Remove unsupported claims
↓
13. Submit minimal reproducible PoC1. Discover interesting parameters
↓
2. Identify URL/redirect/callback parameters
↓
3. Search JavaScript bundles
↓
4. Trace attacker-controlled data
↓
5. Identify DOM sinks
↓
6. Determine every decoding/normalization step
↓
7. Test harmless payloads
↓
8. Identify filtering/WAF behavior
↓
9. Test parser differentials
↓
10. Prove same-origin execution
↓
11. Determine realistic impact
↓
12. Remove unsupported claims
↓
13. Submit minimal reproducible PoCThis workflow is much more reliable than simply throwing hundreds of XSS payloads at parameters.
15. What I Would Test First on Similar Targets
If I encounter a parameter resembling:
return
redirect
next
url
target
callbackreturn
redirect
next
url
target
callbackI immediately investigate:
Client-side sinks
location =
window.open(...)
iframe.src =
script.src =
element.href =
element.setAttribute(...)
element.innerHTML =location =
window.open(...)
iframe.src =
script.src =
element.href =
element.setAttribute(...)
element.innerHTML =Transformations
decodeURIComponent()
decodeURI()
atob()
JSON.parse()decodeURIComponent()
decodeURI()
atob()
JSON.parse()URI schemes
Conceptually test whether the application accepts dangerous protocols such as:
javascript:
data:javascript:
data:while also checking whether the browser, framework, or WAF normalizes unusual representations.
Context
Determine whether the final sink is:
HTML
attribute
JavaScript
CSS
URL
DOM APIHTML
attribute
JavaScript
CSS
URL
DOM APIThe correct payload strategy depends heavily on the context.
16. Why iframe.src Deserves Attention
Developers sometimes treat iframe URLs as inherently safe because they are "just URLs."
That assumption is dangerous.
A URL-valued DOM property is still interpreted by a browser parser.
Therefore this:
iframe.src = userInput;iframe.src = userInput;should never automatically be considered safe.
A safer design validates the URL scheme and destination explicitly.
For example, an application expecting HTTPS URLs should enforce an allowlist such as:
https:https:rather than accepting arbitrary URI schemes.
17. Recommended Fix
The primary fix is to avoid assigning attacker-controlled values directly to a URL-valued DOM sink.
Instead:
1. Validate the protocol
Only permit expected schemes.
For example:
https:https:2. Prefer an allowlist
If the application expects a specific set of domains, validate both:
scheme
+
hostnamescheme
+
hostname3. Reject dangerous schemes after decoding
Validation must happen after all relevant decoding steps.
Otherwise:
encoded input
↓
validation
↓
decode
↓
dangerous URIencoded input
↓
validation
↓
decode
↓
dangerous URIcan still bypass the protection.
4. Avoid unnecessary dynamic iframe URLs
If the functionality does not require arbitrary URLs, don't allow arbitrary URLs.
5. Defense in depth
CSP should complement input validation rather than being relied upon as the only defense.
18. The Most Important Lesson
The most valuable part of this vulnerability wasn't the final payload.
It was the process.
A parameter that initially looked like a normal redirect value eventually became:
HTTP parameter
↓
SSR state
↓
client hydration
↓
decodeURIComponent()
↓
iframe.src
↓
browser URL parser
↓
same-origin JavaScriptHTTP parameter
↓
SSR state
↓
client hydration
↓
decodeURIComponent()
↓
iframe.src
↓
browser URL parser
↓
same-origin JavaScriptAnd the security boundary failed because different layers interpreted the input differently.
That is exactly the type of chain I look for when hunting modern web applications.
19. Final Takeaways for Bug Hunters
If I had to reduce the entire investigation to a few rules:
1. Trace data, don't just fuzz parameters.
The interesting bug was found by following the value into the JavaScript bundle.
2. Learn browser parsing behavior.
WAFs, frameworks, servers, and browsers don't necessarily normalize data identically.
3. Look for decoding functions.
A value that looks harmless before decoding may become dangerous afterward.
4. Prove execution with the smallest possible payload.
A harmless execution marker is better than immediately attempting credential theft.
5. Verify the authentication architecture.
Never automatically claim ATO because document.cookie returns something.
Determine:
Which cookie?
HttpOnly?
Secure?
SameSite?
Actually required for authentication?Which cookie?
HttpOnly?
Secure?
SameSite?
Actually required for authentication?6. Separate confirmed impact from theoretical impact.
This dramatically improves report quality.
7. Don't be afraid to downgrade your own finding.
Discovering that an assumed ATO chain doesn't work is not failure.
It's good security research.
A report that accurately says:
"This is confirmed XSS, but the primary session cookie is HttpOnly and direct ATO was not demonstrated."
is much stronger than an exaggerated report claiming:
"Critical account takeover"
without evidence.
Conclusion
The vulnerability was ultimately a combination of:
Attacker-controlled URL parameter
+
insufficient URL validation
+
client-side decoding
+
unsafe iframe URL assignment
+
browser URI parsing
+
WAF/parser differentialAttacker-controlled URL parameter
+
insufficient URL validation
+
client-side decoding
+
unsafe iframe URL assignment
+
browser URI parsing
+
WAF/parser differentialThe result was a reliable same-origin XSS primitive.
The most important lesson wasn't "find a clever XSS payload."
It was learning to follow an input through the entire application stack and understand how each layer interprets it.
That's the mindset that turns random payload testing into systematic bug hunting.
Disclosure Outcome
The vulnerability was accepted after the impact was narrowed to the demonstrated XSS capabilities.
The program confirmed the XSS, but noted that the primary authentication cookies were protected and that the demonstrated keylogging scenario required unrealistic interaction. The final severity was therefore reduced from High to Low.
Bounty received: $200.