September 30, 2026
XSS Gym Levels 1–20: A Context-Based Approach to Cross-Site ScriptingLearning XSS by understanding…
Cross-Site Scripting (XSS) is one of the most well-known vulnerabilities in web application security. At first glance, XSS can look like a…

By kermit0X
7 min read
XSS Gym Levels 1–20: A Context-Based Approach to Cross-Site ScriptingLearning XSS by understanding browser parsing contexts instead of memorizing payloads.
Cross-Site Scripting (XSS) is one of the most well-known vulnerabilities in web application security. At first glance, XSS can look like a simple matter of injecting <script>alert(1)</script>. In real applications, however, the challenge is rarely the JavaScript itself.
The real challenge is understanding where the input is reflected, how it is encoded, and how the browser will parse it.
In this write-up, I document my solutions for XSS Gym Levels 1–20.
Rather than treating each payload as an isolated trick, I will focus on the underlying concept:
Identify the context → understand the parser → escape the context → reach an executable context.
All tests described here were performed against an authorized educational lab.
The Core Idea: XSS Is Context-Dependent
Before testing an XSS payload, I try to answer one question:
Where does my input land?
For example, these are completely different contexts:
<h1>USER_INPUT</h1>
<input value="USER_INPUT">
<script>
var name = "USER_INPUT";
</script>
const message = `USER_INPUT`;<h1>USER_INPUT</h1>
<input value="USER_INPUT">
<script>
var name = "USER_INPUT";
</script>
const message = `USER_INPUT`;Although the same attacker-controlled input is being reflected, each context is parsed differently.
Therefore, a payload that works inside an HTML element may be completely useless inside a JavaScript string.
This is the main concept demonstrated throughout XSS Gym.
My XSS Testing Methodology
For each level, I followed roughly the same process:
Find the reflection
↓
Inspect the surrounding source
↓
Identify the context
↓
Identify delimiters / escaping
↓
Determine whether the context can be escaped
↓
Introduce an executable context
↓
Verify JavaScript executionFind the reflection
↓
Inspect the surrounding source
↓
Identify the context
↓
Identify delimiters / escaping
↓
Determine whether the context can be escaped
↓
Introduce an executable context
↓
Verify JavaScript executionThis approach is much more reliable than blindly trying payloads.
Level 1 — Breaking Out of <title>
Payload
</title><img src=x onerror=alert(origin)></title><img src=x onerror=alert(origin)>The input was reflected inside a <title> element.
The first step was escaping the existing context:
</title></title>Once the browser encountered the closing </title> tag, the injected <img> element was interpreted as normal HTML.
The invalid image source caused the onerror handler to execute.
Key takeaway
When input is reflected inside an HTML element, first determine whether you can terminate that element.
Level 2 — Breaking Out of <noscript>
Payload
</noscript><img src=x onerror=alert(origin)></noscript><img src=x onerror=alert(origin)>The reflection occurred inside a <noscript> context.
The payload first closed the existing element:
</noscript></noscript>After escaping that context, the injected HTML became active markup.
Key takeaway
The same general technique from Level 1 applies here, but the closing element changes according to the context.
Level 3 — Breaking Out of <style>
Payload
</style><svg onload=alert(1)></style><svg onload=alert(1)>Here, the input was placed inside a <style> element.
The CSS context was terminated using:
</style></style>The payload then introduced an SVG element with an event handler.
Key takeaway
An HTML document can contain multiple parsing contexts. You need to identify the current one before selecting an escape sequence.
Level 4 — Encoding Matters
Payload
%26apos;-alert(1)-%26apos;%26apos;-alert(1)-%26apos;This level demonstrated an important part of real-world XSS testing: encoding and decoding.
The characters visible in the request are not necessarily the same characters eventually interpreted by the browser.
When a payload appears to be ignored, I check the transformation pipeline:
Input
↓
URL decoding
↓
Server-side processing
↓
HTML / JavaScript encoding
↓
DOM parsingInput
↓
URL decoding
↓
Server-side processing
↓
HTML / JavaScript encoding
↓
DOM parsingKey takeaway
Always think about the final representation of your input, not only what you originally sent.
Level 5 — Breaking Out of an HTML Heading
Payload
</h1><script>alert(origin)</script></h1><script>alert(origin)</script>The input was reflected inside an <h1> element.
The existing element was terminated:
</h1></h1>A new <script> element could then be introduced.
Key takeaway
The important part isn't the <script> tag itself.
The important part is escaping the context that prevents the browser from interpreting the injected content as executable markup.
Level 6 — Double-Quoted Attribute Context
Payload
"><input type="submit" value="Click" onclick="alert(origin)"><input type="submit" value="Click" onclick="alert(origin)This time the input was inside an HTML attribute using double quotes.
The critical character was:
""It terminated the existing attribute value.
The payload then introduced another element containing an event handler.
Conceptually, the goal was to transform the parser's interpretation from:
attribute="USER_INPUT"attribute="USER_INPUT"into something containing an attacker-controlled attribute.
Key takeaway
For attribute-based XSS, inspect the surrounding quote first.
Level 7 — Single-Quoted Attribute Context
Payload
'><input type="submit" value="Click" onclick='alert(origin)'><input type="submit" value="Click" onclick='alert(origin)This level was similar to Level 6, except the attribute used single quotes.
Therefore, the relevant delimiter was:
''The payload escaped the existing attribute and introduced an event handler.
Key takeaway
The surrounding syntax determines the characters that matter.
Level 8 — Injecting an Event Handler
Payload
" onmouseover="alert(origin)" onmouseover="alert(origin)Instead of creating a completely independent HTML element, the payload modified the existing element by injecting an event handler.
The important part was:
onmouseover="alert(origin)"onmouseover="alert(origin)"The browser would execute the handler when the relevant mouse event occurred.
Key takeaway
XSS does not always require <script>.
HTML event-handler attributes can provide an execution context as well.
Level 9 — Single-Quoted Event Handler
Payload
' onmouseover='alert(origin)' onmouseover='alert(origin)This was the single-quoted equivalent of Level 8.
The important difference was the delimiter surrounding the existing attribute value.
Key takeaway
When analyzing an attribute context, determine:
- Which quote is being used?
- Is the input encoded?
- Can the quote be escaped?
- Can another attribute be introduced?
Level 10 — Breaking Out of <textarea>
Payload
</textarea><img/src/onerror=alert(origin)></textarea><img/src/onerror=alert(origin)>The input was reflected inside a <textarea> element.
The textarea was first terminated:
</textarea></textarea>The browser could then parse the injected markup outside the textarea.
Key takeaway
Elements such as <textarea> have their own parsing behavior. Always identify the container before testing the payload.
Levels 11–12 — Escaping a <script> Context
Payload
</script><img src=1 onerror=alert(document.domain)></script><img src=1 onerror=alert(document.domain)>These levels focused on input reflected inside a script-related context.
The payload used:
</script></script>to terminate the existing script element.
After returning to HTML parsing, an image element with an error handler was introduced.
The important concept here is the transition between parsing contexts:
JavaScript context
↓
</script>
↓
HTML context
↓
event handler
↓
JavaScript executionJavaScript context
↓
</script>
↓
HTML context
↓
event handler
↓
JavaScript executionKey takeaway
When direct JavaScript injection is restricted, changing the parser's context can sometimes be the key to understanding the vulnerability.
Levels 13–16 — JavaScript String Escaping
These levels were particularly useful because the input was being processed inside JavaScript strings.
The payloads were:
Level 13
\\';alert(document.domain);//\\';alert(document.domain);//Level 14
\\";alert(document.domain);//\\";alert(document.domain);//Level 15
\\\';alert(document.domain);//\\\';alert(document.domain);//Level 16
\\\";alert(document.domain);//\\\";alert(document.domain);//At this point, simply thinking about HTML tags is no longer enough.
The important characters are:
\
'
"
;
//\
'
"
;
//The backslashes affect how quotes are interpreted, while the comment sequence can prevent the remaining part of the original JavaScript statement from causing a syntax error.
The number of backslashes matters because the application may itself escape the input before the JavaScript parser receives it.
Key takeaway
When testing JavaScript contexts, reason about the transformation chain:
Your input
↓
Application escaping
↓
Resulting JavaScript source
↓
JavaScript parserYour input
↓
Application escaping
↓
Resulting JavaScript source
↓
JavaScript parserA payload that looks correct in the HTTP request may produce completely different JavaScript after escaping.
Level 17 — Closing and Reopening <script>
Payload
</script><script>alert(origin)</script></script><script>alert(origin)</script>This level demonstrated another way of escaping the current script context.
The existing script element was terminated:
</script></script>Then a new script element was introduced.
Key takeaway
Understanding how the browser switches between HTML and JavaScript parsing is more important than memorizing a particular payload.
Level 18 — JavaScript Template Literal
Payload
`;alert(origin)//`;alert(origin)//This level introduced another JavaScript string syntax:
Template literals.
Template literals are delimited by backticks:
``Therefore, the relevant delimiter was no longer ' or ".
The payload closed the template literal and attempted to introduce JavaScript code.
Key takeaway
When testing JavaScript, always consider all possible string delimiters:
' Single quote
" Double quote
` Template literal' Single quote
" Double quote
` Template literalLevel 19 — Template Literal Context
Payload
`-alert(1)//`-alert(1)//This level continued working with JavaScript template literal syntax.
The important part was recognizing that the input was not being processed as ordinary HTML.
The backtick changed the parsing context, while the remaining characters influenced how the resulting JavaScript expression was interpreted.
Key takeaway
Context determines meaning.
The same character can have completely different behavior depending on whether it appears in HTML, JavaScript, CSS, or a template literal.
Level 20 — Template Literal Expression
Payload
${alert(origin)}${alert(origin)}The final level introduced template literal interpolation.
JavaScript template literals support expressions using:
${...}${...}For example:
`${alert(origin)}``${alert(origin)}`The expression inside ${...} is evaluated as JavaScript.
This creates a fundamentally different injection point from the previous levels.
Key takeaway
Template literals don't only represent strings. They can also contain JavaScript expressions.
What These 20 Levels Actually Teach
Looking at the payloads individually can make XSS appear to be a collection of tricks.
Looking at them by context makes the pattern much clearer.
The levels covered several major contexts:
ContextMain Concept<title>Escape the HTML element<noscript>Escape the element<style>Exit the CSS contextHTML textIntroduce executable markupHTML attributesEscape the attribute delimiterEvent handlersInject JavaScript through an attribute<textarea>Escape the text container<script>Switch back to HTML parsingJavaScript stringsUnderstand quote/backslash escapingTemplate literalsWork with backticks and expressions
The payload changes, but the underlying methodology remains almost identical.
The Most Important Lesson: Don't Memorize Payloads
One of the easiest mistakes when learning XSS is building a huge list of payloads and trying them one by one.
That approach can work in simple labs, but it doesn't scale well to real applications.
Instead, I try to answer four questions:
1. Where is my input reflected?
For example:
<div>INPUT</div><div>INPUT</div>or:
<input value="INPUT"><input value="INPUT">or:
const x = "INPUT";const x = "INPUT";2. What parser is handling it?
Is the browser currently parsing:
- HTML?
- An attribute?
- JavaScript?
- CSS?
- A template literal?
3. What prevents me from reaching an executable context?
This could be:
- Quotes
- Backslashes
- HTML encoding
- JavaScript escaping
- Sanitization
- Filtering
- CSP
- Framework-specific escaping
4. Can I change the parsing context?
This is often the key to XSS.
For example:
HTML text
↓
close current element
↓
HTML attribute
↓
event handler
↓
JavaScriptHTML text
↓
close current element
↓
HTML attribute
↓
event handler
↓
JavaScriptor:
JavaScript string
↓
escape string
↓
JavaScript expressionJavaScript string
↓
escape string
↓
JavaScript expressionA Practical XSS Workflow
After completing these levels, my workflow can be summarized as:
Find Reflection
│
▼
Inspect HTML / DOM
│
▼
Identify the Context
│
┌───────────┼───────────┐
▼ ▼ ▼
HTML Attribute JavaScript
│ │ │
▼ ▼ ▼
Identify Identify Identify
closing quote / string
tag delimiter delimiter
│ │ │
└───────────┼───────────┘
▼
Attempt Context Escape
│
▼
Reach Executable Context
│
▼
Verify ExecutionFind Reflection
│
▼
Inspect HTML / DOM
│
▼
Identify the Context
│
┌───────────┼───────────┐
▼ ▼ ▼
HTML Attribute JavaScript
│ │ │
▼ ▼ ▼
Identify Identify Identify
closing quote / string
tag delimiter delimiter
│ │ │
└───────────┼───────────┘
▼
Attempt Context Escape
│
▼
Reach Executable Context
│
▼
Verify ExecutionThis workflow is much more useful than memorizing isolated payloads.
From XSS Gym to Real Bug Bounty Hunting
XSS Gym provides controlled examples, but real applications introduce additional complexity.
In a real target, you may encounter:
- HTML encoding
- Attribute encoding
- JavaScript escaping
- DOM-based sinks
- Sanitizers
- WAFs
- CSP
- Framework-specific escaping
- Client-side transformations
- Multiple decoding stages
For example, an input might travel through:
HTTP Request
↓
Backend
↓
Template Engine
↓
HTML Encoding
↓
Browser
↓
DOM
↓
JavaScriptHTTP Request
↓
Backend
↓
Template Engine
↓
HTML Encoding
↓
Browser
↓
DOM
↓
JavaScriptTherefore, finding the reflection is only the beginning.
The real question is:
What happens to my input between the request and the final sink?
Final Takeaways
After completing Levels 1–20, the most important concepts I took away were:
1. Context matters more than the payload.
A payload that works in HTML may fail completely inside JavaScript.
2. Source-code inspection is essential.
Always inspect where your input actually lands.
3. Understand delimiters.
Quotes, backticks, closing tags, and backslashes can completely change the parsing context.
4. Encoding matters.
The value sent in the request may not be the value interpreted by the browser.
5. XSS is about parser manipulation.
The fundamental goal is to move attacker-controlled data into a context where it can be interpreted as executable code.
Conclusion
XSS Gym Levels 1–20 were a useful exercise in understanding the relationship between input reflection, browser parsing, context escaping, and JavaScript execution.
The biggest lesson wasn't a particular payload.
It was a change in mindset:
Don't ask: "Which XSS payload should I try?"
Ask: "What context is my input in, how is it transformed, and how does the browser parse it?"
Once you start thinking in terms of contexts and parsers, XSS testing becomes much more systematic.
And that's the skill I want to carry from XSS labs into real-world web security and bug bounty hunting.
Disclaimer
All techniques and payloads in this article are presented for authorized security testing, educational labs, and controlled environments. Always obtain permission before testing systems you do not own or have explicit authorization to assess.