Cybersecurity

Cross-Site Scripting Explained for Web Developers

JavaScript RAG AI Agents Encryption Web Security Authentication OAuth JWT SQL Injection XSS Databases SQL HTML CSS React Vue.js Angular Java Rust Vector Search Hashing TLS HTTPS
1,428 words Includes Code

Cross-Site Scripting Explained for Web Developers

How malicious scripts hide inside the content your users trust — and the encoding patterns that stop them.

KEY TAKEAWAY
Cross-Site Scripting (XSS) happens when untrusted user input is included in web pages without proper encoding. The browser cannot tell the difference between your HTML and an attacker's injected script — unless you encode the output. The three types — reflected, stored, and DOM-based — differ in how the payload reaches the browser, but the core defense is the same: encode at the right layer, for the right context, every time.

XSS remains one of the most common web application vulnerabilities. Unlike SQL injection, which targets the database, XSS targets your users — their sessions, their cookies, their trust in your site. The fix is conceptually simple: encode user-supplied data before inserting it into HTML. But developers make this mistake constantly because encoding rules differ depending on where the data appears.

How XSS Works: The Core Problem

When the browser receives HTML, it parses it as a structure. Tags become elements. Attributes become properties. Scripts become executable code. XSS exploits this by tricking the browser into parsing attacker-controlled input as executable HTML.

Consider a search page that echoes the user's query back in the response:

# DANGEROUS - User input placed directly in HTML
@app.route('/search')
def search():
    q = request.args.get('q', '')
    return f'<p>Results for: {q}</p>'

If the user visits /search?q=<script>alert('XSS')</script>, the browser receives:

<p>Results for: <script>alert('XSS')</script></p>

The browser sees a <script> tag and executes it. The attacker's JavaScript runs in the victim's browser, with full access to their cookies, session tokens, and the page context.

The Three Types of XSS

XSS attacks fall into three categories based on how the malicious payload reaches the browser:

Type How Payload Reaches Browser Reaches Server? Persistence Severity
Reflected Reflected from URL, form, or header in server response Yes One request HIGH
Stored Saved in database, displayed to other users later Yes Persistent CRITICAL
DOM-based Processed entirely by client-side JavaScript No One page load HIGH

Reflected XSS

Reflected XSS occurs when user input is included directly in the server's response without encoding. The most common vector is a URL parameter or form field that gets echoed back.

Common Reflected XSS Patterns

Input Source Vulnerable Output Safe Output
URL parameter: ?q=... f"<p>{q}</p>" f"<p>{html.escape(q)}</p>"
Form field: username f"<h3>Welcome {name}</h3>" f"<h3>Welcome {html.escape(name)}</h3>"
Header: User-Agent f"<p>Browser: {ua}</p>" f"<p>Browser: {html.escape(ua)}</p>"

Demo result: When we tested the same malicious input through both paths:

Input: <script>alert("XSS")</script>

VULNERABLE response HTML:
  <p>Results for: <script>alert("XSS")</script></p>
  -> Browser EXECUTES the script tag!

SAFE response HTML:
  <p>Results for: &lt;script&gt;alert(&quot;XSS&quot;)&lt;/script&gt;</p>
  -> Browser displays the text harmlessly.

Stored XSS

Stored XSS is the most dangerous variant because the malicious payload is saved persistently. Every user who views the affected page receives the injected script. Common targets include comment sections, forum posts, user profiles, and any content that stores and later displays user input.

Comment System Example

# DANGEROUS - Rendering stored comments without encoding
def render_comment(comment):
    return f"<div class='comment'>\
      <strong>{comment['user']}</strong>: {comment['text']}\
    </div>"
# SAFE - Encode stored content before rendering
def render_comment(comment):
    safe_user = html.escape(comment['user'])
    safe_text = html.escape(comment['text'])
    return f"<div class='comment'>\
      <strong>{safe_user}</strong>: {safe_text}\
    </div>"

Demo result: Three comments with one malicious entry:

VULNERABLE (raw output):
  <strong>alice</strong>: Great article!
  <strong>attacker</strong>: <img src=x onerror="alert(document.cookie)">
  <strong>charlie</strong>: Thanks for sharing!
  -> The second comment executes JavaScript for ALL viewers!

SAFE (encoded output):
  <strong>alice</strong>: Great article!
  <strong>attacker</strong>: &lt;img src=x onerror=&quot;alert(document.cookie)&quot;&gt;
  <strong>charlie</strong>: Thanks for sharing!
  -> All comments displayed as plain text. No code execution.

DOM-Based XSS

DOM-based XSS is unique because the entire attack happens in the browser. The server never sees the payload. Client-side JavaScript reads data from a source like the URL fragment and writes it to the DOM using an unsafe method.

The Dangerous Pattern

// VULNERABLE - Reads URL fragment, writes to innerHTML
document.getElementById("output").innerHTML =
    decodeURIComponent(location.hash.substring(1));

// URL: /page#<img src=x onerror=alert(1)>
// Result: img tag is injected and executes

The Safe Pattern

// SAFE - textContent never parses HTML
document.getElementById("output").textContent =
    decodeURIComponent(location.hash.substring(1));
// Result: displays as plain text, no execution

// SAFE - If you NEED to insert HTML, sanitize first
document.getElementById("output").innerHTML =
    DOMPurify.sanitize(
        decodeURIComponent(location.hash.substring(1))
    );

The key distinction: innerHTML tells the browser to parse the string as HTML and create DOM elements. textContent sets the text content of the element without any HTML parsing.

Output Encoding: The Primary Defense

Output encoding is converting special characters into their HTML entity equivalents before inserting them into HTML. This prevents the browser from interpreting user data as code.

Character HTML Entity Why It Matters
< &lt; Prevents new HTML tags
> &gt; Prevents closing tags
& &amp; Prevents entity injection
" &quot; Prevents attribute breakout
' &#x27; Prevents single-quoted attr breakout

Context-Specific Encoding

Different HTML contexts require different encoding. Using the wrong encoder for the context is a common vulnerability:

Context Example Correct Encoding
HTML body <p>{data}</p> HTML entity encoding
HTML attribute <input value="{data}"> HTML entity + quote encoding
JavaScript string var x = "{data}"; JS escape (\, ", ', \n, \u)
URL parameter <a href="/?q={data}"> Percent encoding (%XX)
CSS value style="color: {data}" Restrict to [a-zA-Z0-9]

Content Security Policy (CSP)

CSP is a browser-enforced HTTP header that restricts which scripts, styles, and other resources a page can load. It acts as a second line of defense: even if an attacker injects a script tag, CSP can prevent it from executing.

# Strict CSP - blocks all external scripts and inline scripts
Content-Security-Policy:
  default-src 'self';
  script-src 'self';
  style-src 'self';
  img-src 'self';

# With nonce for allowed inline scripts
Content-Security-Policy:
  script-src 'self' 'nonce-abc123';

CSP does not replace output encoding. It is a defense-in-depth layer that limits the damage when encoding is missed.

Framework Auto-Escaping

Modern frontend and backend frameworks escape HTML by default. This protects developers who use the framework correctly:

Framework Auto-Escapes By Default? Dangerous Escape Hatch
React / JSX Yes dangerouslySetInnerHTML
Vue Yes v-html directive
Angular Yes [innerHTML] binding
Django Yes |safe filter, mark_safe()
Jinja2 Yes |safe filter, Markup()
Rails ERB Yes raw helper

The danger is in the escape hatches. When you use dangerouslySetInnerHTML or |safe, you take responsibility for encoding. Use a sanitizer like DOMPurify before inserting untrusted HTML.

XSS Defense Checklist

[  ] All user data encoded with HTML entities before display
[  ] Use textContent instead of innerHTML for untrusted data
[  ] Content Security Policy header configured
[  ] DOMPurify used for rich HTML from untrusted sources
[  ] Framework auto-escaping not disabled
[  ] HttpOnly flag set on session cookies
[  ] SameSite cookie attribute configured
[  ] Context-appropriate encoding (HTML, JS, URL, CSS)
[  ] Input validation applied (type, length, format)
[  ] Security headers configured (X-XSS-Protection, etc)
[  ] No eval(), no new Function(), no setTimeout(string)
[  ] Regular XSS testing with tools like OWASP ZAP

Try It Yourself

Experiment with web security concepts using BestWordz tools:

Related BestWordz Articles

Further Reading

Conclusion

Cross-Site Scripting is not a single vulnerability — it is a class of vulnerabilities that share one root cause: user-controlled data being interpreted as code by the browser. The three variants differ in delivery mechanism, but the defense is the same principle applied at different layers.

Encode user data with HTML entities before inserting it into any HTML context. Use textContent instead of innerHTML in JavaScript. Configure CSP as a browser-side safety net. And remember that encoding rules differ depending on the context — HTML encoding does not protect JavaScript strings, and URL encoding does not protect HTML attributes.

The frameworks you use already provide auto-escaping. The most dangerous thing you can do is disable it.

BOTTOM LINE
XSS exists because the browser cannot distinguish your content from attacker-injected code. Output encoding solves this by making all user data visually harmless to the parser. Encode at output, for the right context, every time.

💬 Discuss on BestWordz Community

Join the conversation about JavaScript, RAG, AI Agents on the BestWordz Community forum.

Visit Forum →