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

avatar


image.png

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, ONFOCUS and onfocus are 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.
  • autofocus and onfocus become 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, and onfocus slipped 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 takeaway: Blacklist ≠ security. A filter that lists handlers is an enumeration game you cannot win; the attribute bypass with autofocus + onfocus turns 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).
  • 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.





0
0
0.000
0 comments