🛡️ Bug Bounty Log #01 | XSS via blacklist filter bypass (autofocus + onfocus)

Turn a reflected XSS behind a blacklist filter into 0-click execution using attribute breakout, autofocus and onfocus. NDA-safe bug bounty log.
🎯 The plan
- Focus: Passive recon → manual testing of a legacy login → reflected XSS
- Time Spent: ~2 hours (OSINT + low-volume manual probing)
- Tools Used: subfinder / assetfinder / certspotter / waybackurls / gau (passive), curl + browser (manual)
NDA Disclaimer: The program is not disclosed, so this log is generic: no company name, no real domains, no endpoints, and no PoCs with live URLs. Everything technical is reproducible in an abstract form.
🧠 The handler they forgot
What do you miss when your "possible attacks" filter blocks onclick and onmouseover? Exactly what an attacker needed: a handler you forgot. This session proved it on a legacy login: a reflected XSS that only requires the victim to open a link — no hover, no click, no interaction at all.
📝 Why blacklists always lose
Blacklist filtering is the classic defense that never quite wins. The idea sounds reasonable: block <script>, onclick, onmouseover... but the problem is that you must enumerate the past, while the attacker only needs one variant of the future.
- HTML is case-insensitive:
OnFocus,ONFOCUSandonfocusare the same thing to the browser. - There are over 100 event handlers; blocking a few leaves dozens open (
onfocus,onerror,onfocusin,onpointerenter,ontoggle,oninput...). - The filter reads the raw request; the browser parses the decoded DOM.
- New attributes and behaviors ship with every browser release.
The right fix is not filtering better: it is contextual output encoding (OWASP). The filter is only one layer of defense-in-depth, never the primary control.
⚙️ The discovery: attribute breakout, no angle brackets
The parameter was reflected inside an attribute of an <input>:
<input type="text" value="USER_INPUT_GOES_HERE">
For this context you don't need <script> or < >. Just break out of the attribute and add your own to the tag that is already open:
" autofocus onfocus=alert(1) x="
The browser renders it as:
<input type="text" value="" autofocus onfocus=alert(1) x="">
- The opening quote closes the
value. autofocusandonfocusbecome attributes of the already-open<input>→ no angle brackets needed.- The trailing
x="swallows the leftover quote from the tag closing.
This is the canonical technique from PortSwigger's "Reflected XSS into attribute with angle brackets HTML-encoded" lab.
🔓 The bypass: onfocus + autofocus = 0-click
The app's filter blocked onmouseover and onclick (handlers that require interaction) by returning a "possible malicious script" message. But it did not block onfocus.
On its own, onfocus still requires the user to click or tab into the field. That's where autofocus comes in: on page load, the browser focuses the element automatically, firing onfocus immediately.
Result: JavaScript execution with zero interaction, from a single malicious link.
Payload: " autofocus onfocus=alert(document.domain) x="
Delivery: the victim clicks the link → the payload fires on load.
Useful auto-trigger variants from the same family: onerror on <img src=x>, onload on <body>/<iframe>/<svg>, ontoggle on <details>, onanimationstart... PortSwigger's XSS cheat sheet has a curated "event handlers that do not require user interaction" section.
🛡️ The CSP that saved nothing
The response included a Content-Security-Policy... but with the classic escape:
Content-Security-Policy: default-src 'none'; script-src 'self' 'unsafe-inline'; ...
script-src governs all JavaScript, including inline event handlers (onfocus, onclick...), not just <script> tags. The 'unsafe-inline' token re-enables inline handlers — it undoes exactly what the CSP was trying to protect.
- A strict CSP with nonces/hashes would have blocked this vector.
- A CSP with
'unsafe-inline'(very common in legacy apps with inline scripts) does nothing against this. - The lesson: CSP is a second line of defense, it does not fix missing output encoding.
💡 What worked, what didn't
- What paid off: Applying the generic attribute breakout instead of obsessing over
<script>. I enumerated the filter's handler family (by observing which ones it blocked) rather than guessing, andonfocusslipped through. - What tripped me up:
- The filter is inconsistent across contexts: in the login-submit POST flow it ended up "stripping" the handlers (the PoC stayed as
value="x""), while the GET vector reflected fully 3/3 times. Note: document reproducibility, don't oversell the bypass. - A call-stack leak on the filter's error page: out of scope for the program (stack traces/verbose errors). I left it out of the report.
- The filter is inconsistent across contexts: in the login-submit POST flow it ended up "stripping" the handlers (the PoC stayed as
- The takeaway: Blacklist ≠ security. A filter that lists handlers is an enumeration game you cannot win; the attribute bypass with
autofocus + onfocusturns an XSS that "requires interaction" into a 0-click one, and sets it apart from a classic<script>PoC (often the difference between Low and Medium).
🔒 From alert(1) to a real impact story
A pre-auth reflected XSS with HttpOnly cookie + CSP tends to land as Low/Informative or duplicate. To raise its level:
- Credential phishing on the login page: overlay/fake form on the same origin → harvest of username+password. Valid even with HttpOnly and CSP.
- Same-origin actions / ATO chain: read CSRF tokens from the DOM, change email → password reset → account takeover.
- UI redressing: spoofed warnings, MFA code capture.
- 0-click is the multiplier: "the victim only clicks a link and the payload fires with no interaction" — above any hover/click PoC.
✅ Final takeaways
- Takeaways (3):
- Blacklist filters are beaten with attribute breakout and an off-list handler (
autofocus+onfocus= 0-click). 'unsafe-inline'in CSP re-enables inline handlers — CSP does not save broken encoding.- A strong report narrates real impact (login phishing, ATO, 0-click), not just
alert(1).
- Blacklist filters are beaten with attribute breakout and an off-list handler (
- Your move: If you're testing a legacy target, don't dismiss the login as "boring": it's where people are conditioned to type credentials. Test the attribute, not the tag.
Note: For this post, I used AI tools to format the text, add emoticons and make corrections.
