September 3, 2026
Attribute Breakouts: Winning When Angle Brackets Are Dead
Hello, I am Nitin.

By Nitin yadav
7 min read
Day three of XSS Month. Today we take the single most common dead end in XSS hunting and turn it into a bug.
The scenario: you found a reflection, you fired an img-onerror payload, and the response came back with the angle brackets HTML-encoded. Most hunters mark it dead and move on.
They are wrong roughly half the time, because there is an entire class of XSS that never needs an angle bracket. If your input landed inside an HTML attribute, you are not trying to create a new element. You are trying to create a new attribute on an element that already exists. That is a completely different game with completely different requirements.
The core idea
Look at this response:
<input type="text" name="q" value="nitn4041x"><input type="text" name="q" value="nitn4041x">The browser's HTML parser is inside an attribute value. It is looking for the closing quote. When it finds one, the attribute ends and the parser starts reading the next attribute name.
So if the double quote survives your filter, you can write:
" onmouseover=alert(document.domain) x="" onmouseover=alert(document.domain) x="Which produces:
<input type="text" name="q" value="" onmouseover=alert(document.domain) x=""><input type="text" name="q" value="" onmouseover=alert(document.domain) x="">No angle brackets. No new element. A perfectly valid input element that now carries an event handler. The trailing x=" exists purely to swallow the application's own closing quote so the markup stays valid โ a broken tag can cause the parser to behave in ways that eat your handler.
That is the whole technique. Everything else is variations for when the obvious version is blocked.
Step one: identify the exact quoting
Read the raw response. There are three cases and they need three different terminators.
Double-quoted needs a double quote. Single-quoted needs a single quote โ worth checking specifically, because a huge number of filters were written by someone thinking only about double quotes. Unquoted needs nothing but a space:
x onmouseover=alert(document.domain)x onmouseover=alert(document.domain)Unquoted attributes turn up in older server-rendered templates and in string-concatenated HTML. Always check the bytes on the wire, because the DOM inspector renders normalised markup and will show you quotes the server never sent. Use curl or Burp's raw response view.
Step two: pick an event that fires without a click
Getting a handler in is half the job. Getting it to fire is the other half, and it directly changes how your report is triaged. A payload requiring a mouseover is a "user interaction required" finding. A payload firing on page load is not.
Ranked by how little the victim has to do:
autofocus plus onfocus. The strongest general-purpose option on focusable elements. autofocus forces the browser to focus the element on load, which fires onfocus immediately.
" autofocus onfocus=alert(document.domain) x="" autofocus onfocus=alert(document.domain) x="onload on elements that load something. Works on img, iframe, svg, body.
onerror on a broken resource. If your injection point is an image source attribute, breaking the src is enough:
x" onerror=alert(document.domain) x="x" onerror=alert(document.domain) x="ontoggle on an open details element. Fires on load, and less commonly blocklisted than img/onerror.
onanimationstart, onanimationend, ontransitionend. These are the ones that keep working when everything else is blocked, because they are rarely in blocklists. PortSwigger's research team has published a set of these that fire without needing a separate style block, by leaning on browser default styles. The trick worth knowing is that Chrome applies a default focus outline to focusable elements, which means a transition on the outline property fires without you defining any keyframes at all:
<xss style="display:block;transition:outline 1s;" ontransitionend=alert(1) id=x tabindex=1>test</xss><xss style="display:block;transition:outline 1s;" ontransitionend=alert(1) id=x tabindex=1>test</xss>That vector is from PortSwigger Research, and it matters for attribute breakouts because you can often set style and a transition handler in the same breakout, giving you an auto-firing payload on elements that are not otherwise focusable.
onmouseover or onmouseenter. Fine as a last resort and fine for proving the bug exists. Triage will discount it slightly. If you can also set a style that makes the element cover the viewport in the same breakout, any mouse movement fires it, which effectively removes the interaction requirement. That is a legitimate escalation argument to make in the report.
Step three: when the quote is encoded
This is where most of the depth is. Your probe comes back and the quote is HTML-encoded. Work through this list in order.
Check the other quote
Encoders are frequently asymmetric. If the double quote is encoded and the attribute is double-quoted, check what happens to the single quote, and check whether the same parameter reflects elsewhere on the page inside a single-quoted attribute.
Check for double-decoding
If the application decodes twice, or decodes after sanitising, double-encoded forms can smuggle a quote past a filter that only inspects the first decode. Test the plain percent-encoded form, the double-encoded form, and the numeric and hex HTML entity forms, and see which one the app is.
Look for an unquoted sibling attribute
A page will often reflect the same value into several attributes. One may be quoted, another not. That pattern is real and common, and the unquoted one is your way in.
Break out of a JavaScript event attribute
If your reflection is already inside an event handler attribute, you are in a JavaScript context nested inside an HTML attribute context, and HTML entity decoding happens first.
<div onclick="showResult('nitn4041x')"><div onclick="showResult('nitn4041x')">Here the numeric entity for a single quote will be decoded by the HTML parser into a real quote before the JavaScript engine ever sees the string:
'-alert(document.domain)-''-alert(document.domain)-'This is one of the most reliably overlooked bypasses in the whole class. The filter sees an HTML entity and thinks it is safe. The parser turns it into a quote. Hunt this deliberately โ grep responses for event-handler attributes and check whether any of them contain user input.
srcdoc, and other attributes that reopen HTML parsing
If you can inject into an iframe's attributes, srcdoc gives you a fresh HTML document, and entities inside it are decoded. The outer parser sees only entities; the inner document sees real markup.
Step four: when the space is filtered
Some filters strip spaces to stop you adding attributes. Attribute separators in the HTML spec are more generous than just the space character:
- Tab,
%09 - Line feed,
%0a - Form feed,
%0c - Carriage return,
%0d - Slash โ
<img/src=x/onerror=alert(1)>is valid
So:
"%09onmouseover=alert(document.domain)%09x="
"/onmouseover=alert(document.domain)/x=""%09onmouseover=alert(document.domain)%09x="
"/onmouseover=alert(document.domain)/x="The same principle applies to the equals sign in an event handler: some parsers accept whitespace around it. And attribute values can be double-quoted, single-quoted, or unquoted independently of how the rest of the tag is written.
Step five: when alert, parentheses, or dots are filtered
Once your handler is in, the filter may still block the payload body. Options in rough order of how often they work:
- Different function.
print(),confirm(),prompt().print()in particular is unusual enough to slip past keyword blocklists and it visibly proves execution. - Backticks instead of parentheses. Tagged template syntax calls the function: alert
document.domain - Property access without dots.
alert(document['domain'])orwindow['ale'+'rt'](1) - String reconstruction from regex literals.
top[/al/.source+/ert/.source](document.cookie)โ the blocklist looks for the function name, and that name never appears in the payload. - HTML entity encoding of the handler body. Inside an attribute, the HTML parser decodes entities before JavaScript runs, so a fully entity-encoded function call still executes.
- Case variation. HTML attribute names are case-insensitive, so a mixed-case handler name is valid. Case-sensitive blocklists still exist in 2026 and still fall to this.
A note on discipline: use these to prove execution, then stop. The point of a bypass is to demonstrate the filter is ineffective, not to build the most obfuscated payload in the world. A report with a clean minimal proof of concept and one sentence explaining which filter it defeats reads far better than a wall of encoded characters.
Where attribute reflections hide
Prioritise your search here, because these are the spots where attribute-context reflections are common and under-tested:
- Search forms that re-populate the input value with the previous query
- Pagination and sort links carrying parameters into href attributes
- Hidden inputs carrying state such as return URLs
data-*attributes feeding JavaScript components, which are almost never sanitised because "it is just data"- Meta tags โ description and Open Graph tags reflecting page parameters
- Form actions
- Language and locale switchers writing into lang or hreflang attributes
- Analytics and tag manager blobs where campaign parameters get written into attributes verbatim
Hidden inputs deserve special mention. They are invisible in the rendered page, so people assume they cannot matter, and they are frequently unescaped because the developer thought the same thing. PortSwigger have published research specifically on exploiting XSS in hidden input fields โ the short version is that even when you cannot get an auto-firing event on a hidden element, accesskey plus a click handler gives you a keyboard-triggered path, and in many cases the value is also reflected somewhere visible.
Impact ladder
- Informational โ quote encoded everywhere, no sibling reflection, no double-context. Genuinely dead for now. Note the endpoint and re-check after their next release.
- Low โ breakout works but requires unusual interaction, such as a specific hover on a small element or an access key.
- Medium โ breakout with a click or hover requirement on a prominent element. Standard reflected XSS.
- High โ breakout with autofocus, onload, or an animation event that fires on page load, on an authenticated origin. No interaction beyond visiting your link.
- Critical โ the same, but stored, so it fires for every user who views the page, or fires in a privileged context.
The jump from medium to high here is almost entirely about which event you chose. That is why step two matters as much as step one.
Using document.domain tells the security team which origin runs your code, which is the fact that determines severity.
Do not use the escalation techniques in this post to build something that runs against real users. A full-viewport transparent overlay with an event handler is a legitimate argument in a report about why interaction requirements are weak. It is not something to deploy on a live page other customers will visit.
If your breakout works in a hidden input on a page other users share, remove the payload after your screenshots and note it in the report.
Keep bypasses proportionate. If your minimal payload works, report the minimal payload. Escalating obfuscation past what is needed does not increase the bounty and does make the report harder to triage.
Conclusion โ steal this checklist
- Encoded angle brackets do not kill a reflection. If you are inside an attribute, you never needed them.
- Read the raw bytes to find the real quoting: double, single, or unquoted. DevTools lies to you here.
- Unquoted attributes need only a space. Check for them on every reflection.
- Close the application's own quote with a trailing dummy attribute so the tag stays valid.
- Choose the event deliberately: autofocus with onfocus, and animation or transition events, fire on load and raise severity.
- If the double quote is encoded, check the single quote, check double-decoding, check for an unquoted sibling reflection.
- Reflections inside an existing event handler attribute are a nested JS context โ HTML entities get decoded before JavaScript runs.
- The iframe srcdoc attribute reopens HTML parsing and decodes entities.
- Space filtered? Use tab, newline, form feed, carriage return, or a slash as the attribute separator.
- Function name blocked? Backticks, bracket property access, string reconstruction from regex sources, entity encoding, case variation.
- Hunt hidden inputs, data attributes, meta tags, form actions, and pagination links. These are the under-tested attribute reflections.
Tomorrow: inside the script block โ JavaScript string breakouts, template literals, and the JSON hydration blobs every modern framework ships.
If you Love reading my blogs. Check my Youtube Channel too.