Cross-Site Scripting XSS
What It Is
Cross-Site Scripting, also called XSS, happens when an attacker injects malicious JavaScript into a website and makes the browser execute it.
The injected script can come from places like URLs, forms, input fields, or any user-controlled data that is inserted into the page unsafely.
XSS = Attacker-controlled script runs inside your website
Why It Matters
XSS is dangerous because the malicious script runs inside the user's browser as part of the trusted website.
This can allow attackers to perform unintended actions such as:
- stealing session data
- manipulating page content
- performing unauthorized actions
- capturing typed input
- stealing sensitive page information
- showing phishing forms
If user input becomes executable JavaScript, the page becomes unsafe.
Common Injection Sources
Malicious scripts can enter the app through user-controlled inputs.
Common sources:
- URL query parameters
- forms
- input fields
- browser events
- dynamic HTML content
- data rendered directly into the DOM
Never trust user input.
Any data that comes from outside the application should be treated as unsafe until validated, sanitized, and rendered safely.
Vulnerable Pattern
A common XSS issue happens when user input is inserted into the DOM using innerHTML.
<div>Welcome, <span id="username"></span>!</div>
<script>
const params = new URLSearchParams(window.location.search);
const name = params.get("name");
document.getElementById("username").innerHTML = name;
</script>
Problem:
URL input -> innerHTML -> browser treats input as HTML
If the value contains script-like HTML, the browser may execute it.
This is why directly placing user input into innerHTML is unsafe.
Safe Rendering
Use textContent or innerText when rendering plain text.
<div>Welcome, <span id="username"></span>!</div>
<script>
const params = new URLSearchParams(window.location.search);
const name = params.get("name");
document.getElementById("username").textContent = name;
</script>
This treats the value as text, not executable HTML.
innerHTML -> parses HTML
textContent -> renders plain text
Vulnerability 1: Session Hijacking
Session hijacking happens when an attacker tries to steal session-related data such as cookies.
If a malicious script can access cookies, it may send them to an attacker-controlled system.
Possible impact:
- stolen user session
- account takeover risk
- unauthorized access
- user impersonation
XSS + readable cookies = session theft risk
This is why sensitive session cookies should not be accessible through JavaScript.
Vulnerability 2: Unauthorized Activities
XSS can make the browser perform actions the user did not intend.
Example idea:
User is logged in
Injected script runs in the page
Script triggers an action using the user's active session
Possible impact:
- unwanted post creation
- unwanted messages
- unwanted account changes
- actions sent with the user's credentials
The browser may send valid cookies with the unwanted request.
This makes XSS especially risky in authenticated applications.
Vulnerability 3: Capturing Keystrokes
An injected script can listen to keyboard events and capture what the user types.
Possible impact:
- stolen usernames
- stolen passwords
- captured search text
- captured form input
- privacy loss
XSS can turn the page into a keylogger.
This is why script injection must be prevented before user input reaches the DOM.
Vulnerability 4: Stealing Critical Information
A malicious script can read page content and send sensitive information elsewhere.
Possible targets:
- DOM content
- HTML content
- visible account details
- transaction information
- bank-related page content
- critical page data
If the script runs in the page, it can read what the page can read.
Avoid putting sensitive secrets directly into frontend HTML or JavaScript.
Vulnerability 5: Phishing
XSS can inject fake UI into a trusted website.
Example idea:
Injected script displays a fake login form
User trusts the website
User submits credentials to attacker-controlled location
Possible impact:
- stolen username
- stolen password
- fake login prompt
- fake payment form
- user deception
XSS can make a trusted page show untrusted UI.
Dangerous Pattern: eval
eval() executes a string as JavaScript code.
If user input reaches eval(), attackers may execute arbitrary JavaScript.
<input type="text" id="jsCode" />
<button onclick="executeCode()">Execute Code</button>
<script>
function executeCode() {
const jsCode = document.getElementById("jsCode").value;
eval(jsCode);
}
</script>
This is unsafe because the input becomes executable code.
User input + eval = code execution risk
Avoid eval() in frontend applications.
Mitigation 1: Identify All User Inputs
First, list all possible places where user input enters the app.
Common sources:
- URL parameters
- forms
- input fields
- textareas
- uploaded content
- data from APIs
- browser events
Security starts by knowing where input enters the system.
The rule is simple:
Trust your ex, but never trust user input.
Mitigation 2: Avoid innerHTML
Do not use innerHTML with user-controlled data.
Unsafe:
document.getElementById("username").innerHTML = name;
Safer:
document.getElementById("username").textContent = name;
Use:
textContentinnerText
Avoid:
innerHTML- unsafe HTML injection
- direct rendering of untrusted HTML
Mitigation 3: Escape User Input
Escaping means representing special characters in a safe way so they are not interpreted as code or HTML.
Example idea:
< becomes safe text
" becomes escaped text
special characters do not become executable syntax
Escaping helps prevent user input from changing the meaning of the page.
Escape input before it can be interpreted as HTML or JavaScript.
Mitigation 4: Use Framework Safety
Libraries like React help reduce XSS risk because they escape values before rendering them into the DOM.
Example:
function Welcome({ name }) {
return <h1>Hello {name}</h1>;
}
React treats name as text by default.
Important rule:
Do not bypass framework safety by dangerously injecting HTML.
Avoid unsafe patterns such as directly injecting untrusted HTML into the DOM.
Mitigation 5: Sanitize HTML
If the application must render HTML from users or external sources, sanitize it first.
DOMPurify is an example of a sanitization library.
const cleanHTML = DOMPurify.sanitize(userInput);
document.getElementById("content").innerHTML = cleanHTML;
Sanitization removes unsafe parts from HTML before it reaches the DOM.
Sanitize before rendering user-controlled HTML.
Mitigation 6: Avoid eval
Avoid using eval() because it executes strings as code.
Unsafe:
eval(userInput);
Better approach:
Do not execute user-controlled strings as JavaScript.
Use normal functions and controlled logic instead of dynamic code execution.
Mitigation 7: Content Security Policy
Content Security Policy, also called CSP, is a browser security mechanism controlled through response headers.
CSP can decide:
- what resources can load
- where scripts can come from
- which scripts are allowed to run
- whether inline scripts are blocked
- where violations can be reported
CSP = Browser rulebook for allowed scripts and resources
CSP helps reduce damage if an injection bug exists.
CSP: default-src
default-src 'self' means resources should load only from the same origin by default.
app.use((req, res, next) => {
res.setHeader("Content-Security-Policy", "default-src 'self';");
next();
});
This blocks resources from unapproved external locations unless other directives allow them.
CSP: script-src
script-src controls where scripts can load from.
app.use((req, res, next) => {
res.setHeader(
"Content-Security-Policy",
"default-src 'self';" + "script-src 'self' http://unsecure.com;",
);
next();
});
This means scripts can load from:
- same origin
http://unsecure.com
script-src = allowed script locations
Only allow script sources that the application actually trusts.
CSP for Images, Styles, and Frames
CSP can also define policies for other resource types.
Examples:
- scripts
- styles
- images
- iframes
- fonts
- media
CSP can control different resource types separately.
This gives the application better control over what the browser is allowed to load.
Inline Scripts and unsafe-inline
Inline scripts are blocked if CSP does not allow them.
To allow inline scripts, CSP can use:
'unsafe-inline'
But this weakens CSP.
app.use((req, res, next) => {
res.setHeader(
"Content-Security-Policy",
"default-src 'self';" + "script-src 'self' 'unsafe-inline';",
);
next();
});
Avoid using 'unsafe-inline' unless there is no better option.
unsafe-inline reduces the security benefit of CSP.
CSP Nonce
A nonce helps allow only specific trusted inline scripts.
HTML example:
<script nonce="randomKey">
console.log("My trusted code!");
</script>
<script>
console.log("My non-trusted code!");
</script>
CSP header example:
app.use((req, res, next) => {
res.setHeader(
"Content-Security-Policy",
"default-src 'self';" + "script-src 'self' 'nonce-randomKey';",
);
next();
});
Only the script with the matching nonce is allowed to run.
Nonce = Allow selected inline scripts without allowing all inline scripts
Rules for CSP Nonce
A nonce should be:
- generated randomly
- unique per request
- hard to guess
- not reused carelessly
The PDF also notes that the nonce is not visible in the DOM element tab.
Nonce is used to distinguish trusted scripts from untrusted scripts.
CSP Report-Only Mode
Report-only mode lets teams test CSP without blocking resources immediately.
If CSP errors happen, they can be reported to a configured endpoint.
Common reporting directives:
report-to default;
report-uri URL;
This is useful when introducing CSP gradually.
Report-only mode = observe CSP violations before enforcing blocking behavior
XSS Mitigation Table
| Problem | Safer Practice |
|---|---|
User input in innerHTML | Use textContent or innerText |
| Untrusted HTML | Sanitize with a library like DOMPurify |
| Dynamic code execution | Avoid eval() |
| Special characters in input | Escape user input |
| Inline scripts | Avoid them or use CSP nonce |
| Unknown script sources | Restrict with CSP |
| Sensitive cookies | Avoid JavaScript-readable session data |
| Unsafe user input | Validate and sanitize before use |
Basic Checklist
List all places where user input enters the app
Never trust URL params, forms, or input fields directly
Avoid using innerHTML with user-controlled data
Use textContent or innerText for plain text
Escape user input before rendering
Use framework rendering safety properly
Avoid dangerously injecting HTML into the DOM
Sanitize HTML with DOMPurify when HTML rendering is required
Avoid eval completely
Use CSP headers to control script and resource loading
Prefer CSP nonces over unsafe-inline
Use report-only mode before enforcing strict CSP
Avoid storing sensitive data where JavaScript can read it
Interview Style Answer
Cross-Site Scripting, or XSS, happens when an attacker injects malicious JavaScript into a web application and the browser executes it as part of the trusted page. The script can enter through URLs, forms, input fields, or any user-controlled data rendered unsafely into the DOM. XSS can lead to session hijacking, unauthorized activities, keystroke capture, stealing critical page information, and phishing attacks. A common vulnerable pattern is placing query parameter data into innerHTML. To prevent XSS, identify all user inputs, avoid innerHTML, use textContent or innerText, escape and sanitize data, use libraries like React safely, sanitize HTML with DOMPurify when needed, avoid eval(), and configure CSP headers with allowed sources, script nonces, and report-only mode.
One-Line Summary
XSS = Malicious JavaScript injection caused by unsafe user input rendering and prevented with safe rendering, sanitization, escaping, no eval, and CSP.
Final Mental Model
Input enters app -> treat as unsafe
Render text -> use textContent
Render HTML -> sanitize first
Execute code -> never from user input
Load scripts -> control with CSP
Inline trusted script -> use nonce
Unknown script -> block it