September 9, 2026
Cross-Site Scripting (XSS): When User Input Becomes Code
If you have been learning web hacking for a while, you have probably seen this payload everywhere:

By 4zer7y
12 min read
<scrip>alert(1)</scrip><scrip>alert(1)</scrip>(I misspelled "script" on purpose because it seems Medium has a validation check that prevents XSS from running, haha. )
But, okay, you have probably seen that payload, and maybe your first reaction was:
Okayโฆ so I can make a popup appear. Why should I care?
That is actually a pretty reasonable question.
The popup itself is almost useless. What matters is why the browser executed it in the first place.
If you can make JavaScript controlled by you execute inside somebody else's session on a legitimate website, the alert box is just the proof. The real vulnerability is that the website has accidentally allowed your input to become trusted code.
That is Cross-Site Scripting, or XSS.
And despite being one of the oldest web vulnerabilities around, XSS is definitely not dead.
So, What Exactly Is XSS?
Let's start with the simplest possible definition:
XSS happens when attacker-controlled data reaches a web page in a way that causes the browser to interpret that data as executable code instead of plain text.
Imagine a website has a search page:
https://example.com/search?q=hackinghttps://example.com/search?q=hackingAnd the server generates:
<h2>Search results for: hacking</h2><h2>Search results for: hacking</h2>Nothing strange there.
But imagine the backend is essentially doing something like:
<h2>Search results for: USER_INPUT</h2><h2>Search results for: USER_INPUT</h2>without properly handling that input.
Now someone sends:
<scrip>alert(1)</scrip><scrip>alert(1)</scrip>and the server returns:
<h2>
Search results for:
<scrip>alert(1)</scrip>
</h2><h2>
Search results for:
<scrip>alert(1)</scrip>
</h2>The browser does not know that the <scrip>tag came from an attacker.
It just sees HTML.
So it executes it.
And that is the important part.
The application thought:
This is user data.This is user data.The browser thought:
This is JavaScript.This is JavaScript.That confusion between data and code is the heart of XSS.
The Mental Model Behind XSS
When testing for XSS, I find this mental model much more useful than memorizing dozens of payloads:
ATTACKER INPUT
โ
APPLICATION
โ
HTML / JavaScript / DOM
โ
BROWSER PARSES IT
โ
ATTACKER-CONTROLLED CODE EXECUTESATTACKER INPUT
โ
APPLICATION
โ
HTML / JavaScript / DOM
โ
BROWSER PARSES IT
โ
ATTACKER-CONTROLLED CODE EXECUTESYour job as a tester is basically to figure out:
Can I control some data?
โ
Where does that data go?
โ
How does the browser interpret it?
โ
Can I transform that data into executable code?Can I control some data?
โ
Where does that data go?
โ
How does the browser interpret it?
โ
Can I transform that data into executable code?That's XSS hunting in a nutshell.
Why the Browser's Trust Matters
This is where XSS becomes much more interesting.
Browsers have an important security concept called the Same-Origin Policy.
Very roughly, JavaScript running on:
evil.exampleevil.exampleshould not simply be able to access information from:
bank.examplebank.exampleThose are different origins.
Normally:
evil.example
โ
JavaScript
โ
bank.example
BLOCKEDevil.example
โ
JavaScript
โ
bank.example
BLOCKEDBut XSS changes the situation.
Instead of running JavaScript from the attacker's site, we convince the vulnerable website itself to execute attacker-controlled JavaScript.
Now we have:
bank.example
โ
Injected JavaScript
โ
Victim's browserbank.example
โ
Injected JavaScript
โ
Victim's browserFrom the browser's perspective, that JavaScript is running inside bank.example.
That's the security boundary XSS breaks.
PortSwigger describes the core consequence in similar terms: XSS can bypass the browser's same-origin isolation and potentially allow an attacker to perform actions or access data available to the affected user.
So the vulnerability isn't really I made JavaScript run, actually, It is I made my JavaScript run with the trust and privileges of your application.
Much more interesting.
There Are Three Main Types of XSS
You will usually encounter three big categories:
Reflected XSS
Stored XSS
DOM-Based XSSReflected XSS
Stored XSS
DOM-Based XSSThere are also variations such as blind XSS and self-XSS, which we will talk about later.
Let's go through the important ones.
1. Reflected XSS
Reflected XSS happens when your input arrives in an HTTP request and the server immediately sends it back inside the response without safely encoding it.
Imagine this URL:
https://example.com/search?q=coffeehttps://example.com/search?q=coffeeThe server responds:
<p>You searched for: coffee</p><p>You searched for: coffee</p>Now replace coffee with something interesting:
<scrip>alert(1)</scrip><scrip>alert(1)</scrip>If the application responds with:
<p>You searched for:<scrip>alert(1)</scrip></p><p>You searched for:<scrip>alert(1)</scrip></p>and the browser executes it, we have reflected XSS.
The attack flow looks like this:
Attacker creates malicious URL
โ
Victim clicks URL
โ
Request reaches vulnerable website
โ
Website reflects malicious input
โ
Browser executes itAttacker creates malicious URL
โ
Victim clicks URL
โ
Request reaches vulnerable website
โ
Website reflects malicious input
โ
Browser executes itThe word reflected makes sense now: the payload arrives in the request and gets reflected straight back into the response.
Where should you look?
Typical places include:
Search fields
Error messages
GET parameters
POST parameters
Redirect parameters
Filtering options
Tracking parameters
HTTP headersSearch fields
Error messages
GET parameters
POST parameters
Redirect parameters
Filtering options
Tracking parameters
HTTP headersWhenever you see user-controlled data appearing back inside the HTML response, it deserves attention.
2. Stored XSS
Stored XSS is where things start getting much more serious.
Instead of the payload being immediately reflected, the application stores it somewhere.
Usually a database.
Imagine a comment system.
You submit:
Nice article!Nice article!The server stores it:
Database
โโโ comment = "Nice article!"Database
โโโ comment = "Nice article!"Later, somebody opens the page and receives:
<div class="comment">
Nice article!
</div><div class="comment">
Nice article!
</div>Now imagine the application allows this to be stored:
<scrip>alert(1)</scrip><scrip>alert(1)</scrip>The database now contains:
Database
โโโ comment = "<scrip>alert(1)</scrip>"Database
โโโ comment = "<scrip>alert(1)</scrip>"Every time someone opens that page:
Database
โ
Application
โ
Victim's browser
โ
JavaScript executesDatabase
โ
Application
โ
Victim's browser
โ
JavaScript executesThat is stored XSS.
PortSwigger also calls this persistent or second-order XSS because the malicious data is stored and becomes dangerous when it is later rendered.
And here's why this can be worse than reflected XSS:
With reflected XSS, you often need to convince someone to click a crafted link.
With stored XSS, you might just plant the payload and wait.
Stored XSS Can Become Especially Dangerous
Think about a support ticket system.
A normal user submits:
My account isn't working.My account isn't working.A support administrator later opens the ticket.
Now imagine the message contains malicious HTML or JavaScript and the application renders it unsafely.
The flow becomes:
Low-privileged attacker
โ
Support ticket
โ
Database
โ
Administrator opens ticket
โ
JavaScript executes
โ
Admin privileges become relevantLow-privileged attacker
โ
Support ticket
โ
Database
โ
Administrator opens ticket
โ
JavaScript executes
โ
Admin privileges become relevantSuddenly the impact of the XSS depends on the permissions of the person viewing it.
That can be a very big difference.
3. DOM-Based XSS
DOM XSS is slightly different and, in my opinion, much more interesting once you start reading JavaScript during pentests.
Here the vulnerability can live completely inside client-side JavaScript.
Suppose the page contains:
const search = new URLSearchParams(location.search).get("q");
document.getElementById("result").innerHTML = search;const search = new URLSearchParams(location.search).get("q");
document.getElementById("result").innerHTML = search;You visit:
https://example.com/?q=hellohttps://example.com/?q=helloJavaScript reads:
hellohelloand inserts it into:
innerHTMLinnerHTMLThe important data flow is:
location.search
โ
search
โ
innerHTML
โ
HTML parserlocation.search
โ
search
โ
innerHTML
โ
HTML parserIf attacker-controlled input reaches a dangerous operation like innerHTML, DOM XSS may become possible. PortSwigger describes DOM XSS exactly in terms of attacker-controlled sources reaching dangerous sinks.
This introduces two concepts worth remembering.
Sources and Sinks
A source is where potentially attacker-controlled information enters JavaScript.
Examples:
location.search
location.hash
document.URL
document.referrer
window.name
postMessage()location.search
location.hash
document.URL
document.referrer
window.name
postMessage()A sink is somewhere that data becomes dangerous.
Examples include:
innerHTML
outerHTML
document.write()
eval()innerHTML
outerHTML
document.write()
eval()So during JavaScript analysis, you're basically looking for:
SOURCE
โ
data transformations
โ
SINKSOURCE
โ
data transformations
โ
SINKFor example:
let username = location.hash.substring(1);
profile.innerHTML = username;let username = location.hash.substring(1);
profile.innerHTML = username;Your brain should immediately see:
location.hash
โ
username
โ
innerHTMLlocation.hash
โ
username
โ
innerHTMLInteresting.
Now the question becomes whether we can control that source strongly enough to make the browser interpret our input as code.
XSS Is Context Dependent
This is one of the most important things to understand.
Finding your input reflected in the response doesn't automatically mean XSS.
Where the input lands matters.
For example:
<div>INPUT</div><div>INPUT</div>is different from:
<input value="INPUT"><input value="INPUT">which is different from:
let username = "INPUT";let username = "INPUT";which is different from:
<a href="INPUT">Click here</a><a href="INPUT">Click here</a>Those are completely different parsing contexts.
And different contexts require different approaches.
Example: HTML Context
Suppose your input lands here:
<div>
USER_INPUT
</div><div>
USER_INPUT
</div>Before even thinking about JavaScript, I like using something harmless:
<b>XSS_TEST</b><b>XSS_TEST</b>If the browser shows:
XSS_TEST
in bold, something important just happened.
Your input was not treated purely as text.
The browser interpreted it as HTML.
That does not yet prove JavaScript execution.
But you just learned something extremely useful about the sink.
Example: Attribute Context
Maybe your input appears here:
<input value="USER_INPUT"><input value="USER_INPUT">Now you are inside an HTML attribute.
The interesting question becomes:
Can I escape from this attribute?
Characters such as:
"
'
>
<"
'
>
<become important because they can potentially affect the HTML structure.
If the server safely converts:
""into:
""your attempt to escape the attribute might fail.
This is why blindly pasting:
<scrip>alert(1)</scrip><scrip>alert(1)</scrip>into every field isn't particularly good XSS testing.
Sometimes <scrip> isn't even relevant to the context you're dealing with.
Example: JavaScript Context
Now imagine the server produces:
const username = "USER_INPUT";const username = "USER_INPUT";We're not really dealing with normal HTML anymore.
We're inside a JavaScript string.
Now things such as:
quotes
escaping
backslashes
semicolons
template literalsquotes
escaping
backslashes
semicolons
template literalsbecome much more relevant.
The browser isn't trying to identify if this valid HTML, instead of that is validating if this is valid JavaScript
That change in context completely changes how you analyze the vulnerability.
Why alert(1) Exists Everywhere
Let's clear something up.
This:
alert(1)alert(1)is not the attack.
It is usually just a proof of concept.
You could also use:
alert(document.domain)alert(document.domain)or modify something harmless on the page.
The point is simply to demonstrate:
I control JavaScript execution.I control JavaScript execution.Once that happens, the security question changes from:
Can JavaScript execute? to What can JavaScript do inside this application?
That's where impact analysis begins.
What Can XSS Actually Do?
The exact impact varies a lot.
A tiny reflected XSS on a public page might not be particularly impressive.
A stored XSS that executes inside an administrator's dashboard can be critical.
Potential consequences include:
Performing actions as the victim
Reading sensitive information displayed in the page
Modifying application content
Changing account settings
Capturing information entered into the application
Interacting with internal application APIs
Targeting privileged users
Creating convincing phishing interfaces inside the trusted site
Chaining with other vulnerabilitiesPerforming actions as the victim
Reading sensitive information displayed in the page
Modifying application content
Changing account settings
Capturing information entered into the application
Interacting with internal application APIs
Targeting privileged users
Creating convincing phishing interfaces inside the trusted site
Chaining with other vulnerabilitiesPortSwigger notes that attacker-controlled JavaScript may be able to perform actions available to the victim, read information the victim can access, and modify data available to them.
But What About Stealing Cookies?
You'll hear this constantly when learning XSS:
document.cookiedocument.cookieAnd yes, historically, stealing session cookies was one of the classic demonstrations of XSS.
But modern applications frequently protect important session cookies with:
HttpOnlyHttpOnlywhich prevents JavaScript from directly reading them.
So:
document.cookiedocument.cookiemay not contain the authentication token you're hoping for.
Does that make XSS useless?
Not at all.
This is an important misconception.
If JavaScript is running inside an authenticated user's browser, it may not need to know the session token.
The browser already has it.
Conceptually:
Victim logs in
โ
Browser receives session
โ
XSS executes
โ
Injected JavaScript interacts with application
โ
Browser automatically uses the victim's sessionVictim logs in
โ
Browser receives session
โ
XSS executes
โ
Injected JavaScript interacts with application
โ
Browser automatically uses the victim's sessionSo modern XSS exploitation is often more interesting than simply:
steal cookie โ hijack accountsteal cookie โ hijack accountThe attacker may instead attempt to use the victim's existing session to interact with the application.
Admin XSS Is Where Things Get Interesting
Suppose your XSS only appears on your own profile.
Not very exciting.
Now suppose that same profile is reviewed by an administrator.
Different story.
Imagine:
Attacker
โ
Changes profile field
โ
Stored in database
โ
Administrator opens user profile
โ
Payload executes as administratorAttacker
โ
Changes profile field
โ
Stored in database
โ
Administrator opens user profile
โ
Payload executes as administratorThis can turn a vulnerability accessible to a normal user into something much more serious.
When pentesting, I would pay extra attention to fields eventually displayed in:
Admin panels
Moderation systems
Support dashboards
CRM platforms
Monitoring dashboards
Logs
Analytics interfaces
Back-office applicationsAdmin panels
Moderation systems
Support dashboards
CRM platforms
Monitoring dashboards
Logs
Analytics interfaces
Back-office applicationsBlind XSS
This leads us to Blind XSS.
Blind XSS is basically stored XSS where you cannot directly see the place where your payload eventually executes.
Imagine a contact form:
Name:
Email:
Message:Name:
Email:
Message:You submit data.
The public website simply responds:
Thanks for contacting us.Thanks for contacting us.Nothing happens.
But behind the scenes:
Your input
โ
Database
โ
Internal customer support dashboard
โ
Employee opens the messageYour input
โ
Database
โ
Internal customer support dashboard
โ
Employee opens the messageIf that internal dashboard renders the input unsafely, your payload might execute there.
From your perspective, the execution happened somewhere you could not see.
That's why it's called blind XSS.
And those situations can be particularly interesting because the person triggering your payload may have much higher privileges than you.
Self-XSS
You may also hear the term Self-XSS.
This is when the only person you can make execute the payload is yourself.
For example, imagine you must:
- Open your browser console.
- Paste malicious JavaScript.
- Press Enter.
Wellโฆ
You already control your own browser.
So by itself that isn't really a meaningful security issue.
Self-XSS becomes interesting only when there is some realistic way to convince another user to perform the required action, usually through social engineering or by chaining the behavior with another weakness.
PortSwigger similarly distinguishes self-XSS from normal reflected XSS because the victim normally needs to manually submit the payload themselves.
Is XSS Still Relevant Today?
Absolutely.
But the answer deserves some nuance.
XSS is old.
Browsers are better.
Frameworks are better.
Developers have better libraries.
Security headers exist.
Modern frameworks such as React, Angular, and Vue often escape output automatically in common situations.
Cookies can be protected with HttpOnly.
Content Security Policy can significantly restrict JavaScript execution.
So yes, exploiting XSS in modern applications can sometimes be harder than it was fifteen years ago.
But XSS did not disappear.
As of the OWASP Top 10:2025, XSS remains part of the A05: Injection category. OWASP describes XSS as a high-frequency issue and reports more than 30,000 CVEs associated with it in the dataset used for the ranking.
Even more interestingly, MITRE ranked CWE-79 โ Cross-Site Scripting โ as the #1 weakness in the 2025 CWE Top 25 Most Dangerous Software Weaknesses. MITRE's dataset also associated seven CWE-79 vulnerabilities with CISA's Known Exploited Vulnerabilities catalog.
That gives us a pretty clear answer:
Is XSS old?
Yes.
Is XSS solved?
No.Is XSS old?
Yes.
Is XSS solved?
No.Is XSS Actually Exploited in the Real World?
Yes.
CISA's Known Exploited Vulnerabilities catalog exists specifically to track vulnerabilities with evidence of exploitation in the wild, and XSS-related vulnerabilities have appeared there.
For example, CISA listed CVE-2024โ44309, affecting multiple Apple products, as a known exploited vulnerability involving malicious web content that could lead to XSS.
That doesn't mean every reflected XSS you find is going to be exploited by sophisticated attackers.
Far from it.
XSS severity depends heavily on:
Where the vulnerability exists
Who can trigger it
Who receives the payload
What privileges the victim has
Whether authentication is required
Which browser protections exist
Which CSP policies exist
What the JavaScript can access
Whether the vulnerability can be chainedWhere the vulnerability exists
Who can trigger it
Who receives the payload
What privileges the victim has
Whether authentication is required
Which browser protections exist
Which CSP policies exist
What the JavaScript can access
Whether the vulnerability can be chainedThat's why you'll sometimes see an XSS rated relatively low and another rated high or critical.
Same vulnerability family.
Very different attack scenario.
Why Do We Still Find XSS?
Because modern web applications are incredibly complicated.
Think about what happens in a typical frontend today:
API responses
โ
JavaScript
โ
JSON
โ
Framework components
โ
DOM manipulation
โ
Third-party libraries
โ
User-generated contentAPI responses
โ
JavaScript
โ
JSON
โ
Framework components
โ
DOM manipulation
โ
Third-party libraries
โ
User-generated contentNow add:
Legacy JavaScript
Markdown renderers
Rich-text editors
SVG
File previews
Template engines
Analytics data
postMessage()
WebSockets
Client-side routing
Server-side renderingLegacy JavaScript
Markdown renderers
Rich-text editors
SVG
File previews
Template engines
Analytics data
postMessage()
WebSockets
Client-side routing
Server-side renderingThere are plenty of opportunities for untrusted data to accidentally reach a dangerous context.
The attack surface changed.
XSS didn't disappear with it.
Modern Frontend Frameworks Help, But They're Not Magic
For example, React normally escapes values rendered like this:
<div>{username}</div><div>{username}</div>That makes straightforward HTML injection much harder.
Good.
But applications eventually need features such as:
Rich HTML rendering
Markdown
WYSIWYG editors
Embedded content
Dynamic templates
Legacy components
Third-party widgetsRich HTML rendering
Markdown
WYSIWYG editors
Embedded content
Dynamic templates
Legacy components
Third-party widgetsThen developers start deliberately bypassing those safety mechanisms.
For example, React has:
dangerouslySetInnerHTMLdangerouslySetInnerHTMLAnd the name isn't exactly subtle.
Whenever an application intentionally turns strings back into HTML, the possibility of XSS becomes relevant again.
The same principle applies regardless of framework:
SAFE DATA
โ
someone decides it should become HTML
โ
dangerous parsing boundary appearsSAFE DATA
โ
someone decides it should become HTML
โ
dangerous parsing boundary appearsExample 1 โ Reflected XSS
Imagine:
GET /search?q=hackingGET /search?q=hackingThe application returns:
<h2>Results for: hacking</h2><h2>Results for: hacking</h2>First, I might try something harmless:
<b>XSS_TEST</b><b>XSS_TEST</b>If I receive:
<h2>Results for: <b>XSS_TEST</b></h2><h2>Results for: <b>XSS_TEST</b></h2>and it renders as bold text, I've learned that HTML is being interpreted.
Next, in an authorized test environment, a simple execution proof might be:
<scrip>lert(1)</scrip><scrip>lert(1)</scrip>If the browser executes it:
User input
โ
Server response
โ
HTML parser
โ
JavaScript executionUser input
โ
Server response
โ
HTML parser
โ
JavaScript executionWe have reflected XSS.
Example 2 โ Event Handler XSS
Sometimes <scrip>ags are filtered.
That doesn't necessarily mean XSS is impossible.
JavaScript can also execute through HTML event handlers.
For example:
<img src=x onerror=alert(1)><img src=x onerror=alert(1)>Here the logic is:
Browser creates <img>
โ
Attempts to load "x"
โ
Loading fails
โ
onerror event fires
โ
JavaScript executesBrowser creates <img>
โ
Attempts to load "x"
โ
Loading fails
โ
onerror event fires
โ
JavaScript executesThis is a good example of why:
Block <scrip>Block <scrip>is not a real XSS defense.
The browser provides many ways for HTML and JavaScript to interact.
Example 3 โ Stored XSS
Imagine a vulnerable blog comment:
<div class="comment">
USER_COMMENT
</div><div class="comment">
USER_COMMENT
</div>Submit:
<b>HELLO</b><b>HELLO</b>Reload the page.
If you see bold text, HTML injection has been confirmed.
Now imagine a harmless proof of JavaScript execution is stored instead.
The important difference is persistence:
Attacker submits payload
โ
Application stores payload
โ
Attacker leaves
โ
Victim visits later
โ
Browser loads stored content
โ
Payload executesAttacker submits payload
โ
Application stores payload
โ
Attacker leaves
โ
Victim visits later
โ
Browser loads stored content
โ
Payload executesThat is why stored XSS can have a much larger blast radius.
Example 4 โ DOM XSS
Consider:
const name = location.hash.substring(1);
document.getElementById("welcome").innerHTML = name;const name = location.hash.substring(1);
document.getElementById("welcome").innerHTML = name;Visit:
https://example.com/#Davidhttps://example.com/#DavidThe browser effectively creates:
<div id="welcome">
David
</div><div id="welcome">
David
</div>But the important part is:
location.hash
โ
name
โ
innerHTMLlocation.hash
โ
name
โ
innerHTMLWe have:
Source โ SinkSource โ SinkThe server might never even see the fragment.
Everything happens inside the victim's browser.
That is classic DOM XSS territory.
Example 5 โ Safe vs Unsafe DOM Manipulation
Compare these two lines:
element.innerHTML = userInput;element.innerHTML = userInput;and:
element.textContent = userInput;element.textContent = userInput;They look similar.
Security-wise, they're very different.
With:
innerHTMLinnerHTMLthe browser is essentially told:
Parse this as HTML.Parse this as HTML.With:
textContenttextContentthe browser is told:
Display this as text.Display this as text.So if:
userInput = "<b>Hello</b>";userInput = "<b>Hello</b>";then:
innerHTMLinnerHTMLproduces:
Hello
while:
textContenttextContentproduces:
<b>Hello</b><b>Hello</b>That distinction captures the entire XSS problem surprisingly well.
A Better Way to Hunt for XSS
Instead of randomly throwing payloads everywhere, think like this:
1. Find input you control.
2. Find where that input appears.
3. Identify the context.
4. Check what characters survive.
5. Determine how the browser parses the result.
6. Look for a path to JavaScript execution.
7. Evaluate what that execution actually gives you.1. Find input you control.
2. Find where that input appears.
3. Identify the context.
4. Check what characters survive.
5. Determine how the browser parses the result.
6. Look for a path to JavaScript execution.
7. Evaluate what that execution actually gives you.Or even shorter:
INPUT
โ
CONTEXT
โ
PARSING
โ
EXECUTION
โ
IMPACTINPUT
โ
CONTEXT
โ
PARSING
โ
EXECUTION
โ
IMPACTThat's the workflow worth remembering.
Not:
Paste <scrip>alert(1)</scrip> everywhere and hope.Paste <scrip>alert(1)</scrip> everywhere and hope.Final Thoughts
XSS is one of those vulnerabilities that looks almost trivial when you first learn it.
You inject:
<scrip>alert(1)</scrip><scrip>alert(1)</scrip>A popup appears.
Done.
But once you understand what's happening underneath, it becomes much more interesting.
The real issue is a broken trust boundary:
Attacker-controlled data
โ
Trusted application
โ
Victim's browser
โ
Attacker-controlled codeAttacker-controlled data
โ
Trusted application
โ
Victim's browser
โ
Attacker-controlled codeThe application allowed data to become code.
And once attacker-controlled JavaScript runs inside a trusted origin, the real question is no longer whether you can make an alert appear.
The real question becomes:
What can I do with the privileges of the user who executes it?
That's where XSS goes from a simple browser trick to a real security vulnerability.
And despite decades of research, modern frameworks, browser protections, CSP, sanitization libraries, and secure coding guidance, it continues to appear in real applications.
So if you're studying web pentesting, don't learn XSS as a collection of payloads.
Learn the browser.
Learn HTML parsing.
Learn JavaScript contexts.
Learn sources and sinks.
Learn how user-controlled data travels through an application.
Once you understand that, XSS stops looking like magic.
It becomes a data-flow problem.
And those are much easier to hunt.