Skip to main content

Command Palette

Search for a command to run...

Cross-Site Scripting (XSS)

Types, Detection, and Practical Defenses (with Safe Code Examples)

Published
6 min read
Cross-Site Scripting (XSS)
T

Cybersecurity student with a strong interest in Offensive Security. Passionate about ethical hacking, hands-on learning, and turning curiosity into practical skills that build stronger defenses.

A defence-first guide to Cross-Site Scripting: learn types of XSS, how attackers think conceptually, safe vulnerable-pattern examples and secure fixes, plus practical Content Security Policy guidance and detection strategies.

Introduction

Cross-Site Scripting (XSS) is one of the oldest — and still most common — web vulnerabilities. At its core, XSS arises when an application includes untrusted data in a page without proper handling, allowing an attacker’s data to be interpreted as executable script by other users’ browsers. This article explains XSS types, gives safe example patterns to help you identify risky code, and shows secure alternatives and Content-Security-Policy (CSP) recommendations.

Reminder: I will not provide exploit payloads or step-by-step attack instructions. For hands-on practice, use legal, isolated labs (OWASP Juice Shop, WebGoat, PortSwigger Academy, DVWA).

XSS — The Types (Short & Clear)

  1. Reflected XSS (non-persistent)

    • What: The server takes input (query parameter, form value), immediately reflects it into an HTTP response without safe encoding.

    • Impact: Usually targets users who click crafted links.

    • Where to look: Search pages, error messages, search results, and any immediate echo of request data.

  2. Stored XSS (persistent)

    • What: Unsanitized input is stored on the server (database, message board, comment), and later served to other users.

    • Impact: High—affects every viewer of the stored content.

    • Where to look: Comments, profiles, message boards, product reviews.

  3. DOM-based XSS (client-side)

    • What: The vulnerability exists in client-side JavaScript that reads from the DOM/location (hash, search, localStorage) and writes directly to the page without sanitization. The server may never see malicious input.

    • Impact: Depends on client logic; often tricky to detect via server scans.

    • Where to look: Client code using innerHTML, document.write, eval, insertAdjacentHTML, or building script/HTML from untrusted sources.

Attacker Mindset (Conceptual — Non-Actionable)

Attackers look for places where untrusted data is treated as code instead of data. Their goals are generally to execute script in victims’ browsers to: steal session/authorization tokens, perform actions as the victim (CSRF-style), display phishing UI, or move laterally within a web app. Knowing attacker goals helps prioritize protections (protect session tokens, sensitive endpoints, and user input surfaces).

Recognize Unsafe Coding Patterns (VULNERABLE PATTERNS — safe examples)

Below are vulnerable patterns shown for recognition only. I have not included exploit strings or instructions.

1) Unsafe server-side rendering (string concatenation into HTML)

// Vulnerable pattern (server-side template / manual concatenation)
app.get('/greet', (req, res) => {
  const name = req.query.name || 'Guest';
  // Danger: raw value injected into HTML without encoding
  res.send(`<h1>Welcome, ${name}</h1>`);
});

Why risky: If name can include characters that the browser interprets as HTML/JS, it changes page behavior.

Secure fix — properly encode output (escape special characters)

// Secure pattern (example using a simple escape)
function escapeHtml(str) {
  return str
    .replace(/&/g, '&amp;')
    .replace(/</g, '&lt;')
    .replace(/>/g, '&gt;')
    .replace(/"/g, '&quot;')
    .replace(/'/g, '&#39;');
}

app.get('/greet', (req, res) => {
  const name = escapeHtml(req.query.name || 'Guest');
  res.send(`<h1>Welcome, ${name}</h1>`);
});

Better: Use a mature templating engine that escapes by default (e.g., Handlebars, Pug) or frameworks that auto-escape.

2) Unsafe client-side DOM insertion (innerHTML)

<!-- Vulnerable pattern (client-side) -->
<div id="profile"></div>
<script>
  // userBio comes from server or location and is inserted as HTML
  const userBio = getUserBio(); // untrusted
  document.getElementById('profile').innerHTML = userBio;
</script>

Why risky: innerHTML parses and executes HTML/JS in the string.

Secure fixes:

  • Use textContent or innerText when you want to render plain text:
document.getElementById('profile').textContent = userBio;
  • If you must render safe HTML (e.g., limited markup), sanitize with a vetted library before insertion:
// Use a trusted sanitizer like DOMPurify (example)
const safeHtml = DOMPurify.sanitize(userBio, {ALLOWED_TAGS: ['b','i','a']});
document.getElementById('profile').innerHTML = safeHtml;

Always prefer sanitizers that are actively maintained and configured for your context.

3) Unsafe handling of attributes / URL building

// Vulnerable: building an onclick or href with untrusted input
const link = document.createElement('a');
link.href = '/profile?user=' + userInput; // ok for href if encoded
link.setAttribute('data-bio', userInput); // danger if later read into HTML

Secure approach: Always encode/validate values used in HTML attributes or when constructing URLs; prefer using DOM APIs and encoding helpers.

Content Security Policy (CSP) — Practical Guidance & Example

CSP reduces the impact of XSS by restricting allowed sources of scripts/styles/images and preventing inline script execution unless explicitly allowed.

Why use CSP: Even if XSS exists, a restrictive CSP can stop inline script execution or block scripts from untrusted origins.

Example of a reasonably strict CSP header:

Content-Security-Policy:
  default-src 'self';
  script-src 'self' https://apis.example.com;
  object-src 'none';
  base-uri 'self';
  frame-ancestors 'none';
  upgrade-insecure-requests;
  report-uri /csp-report-endpoint;

Notes:

  • Avoid unsafe-inline for script-src and style-src. Prefer nonces or hashes when inline styles/scripts are unavoidable.

  • Use report-uri or report-to to collect CSP violation reports and tune policy.

  • Start with a report-only policy (Content-Security-Policy-Report-Only) to detect breakages before enforcement.

  • CSP is defence in depth — it does not substitute secure coding.

Secure Development Practices (Actionable, Defensive)

  1. Output Encoding — encode untrusted data for the specific output context (HTML body, attribute, URL, CSS, JavaScript). Context matters.

  2. Use Frameworks that Auto-Escape — trust battle-tested templating features rather than manual concatenation.

  3. Sanitize Allowed HTML — if users can supply limited markup, sanitize with a maintained library (DOMPurify, bleach for Python).

  4. Avoid Dangerous APIs — reduce use of innerHTML, document.write, eval, setTimeout(string), or new Function(...).

  5. Use HTTPOnly & Secure Cookies — prevent client-side scripts from reading session tokens.

  6. Apply CSP — restrict script origins and disallow inline scripts or use nonces/hashes where needed.

  7. Principle of Least Privilege for Data — avoid rendering or exposing more data than needed.

  8. Escape on Output, Validate on Input — input validation helps but never replaces output encoding.

Detection, Monitoring & Logging

  • Automated Scanners: include SAST/DAST tools (in CI and in pre-release) to find obvious sinks and sources.

  • Runtime Protection: WAFs and RASP can provide additional detection; they are complementary to secure coding.

  • CSP Reports: collect and review CSP violation reports to discover attempted script executions.

  • Logging: log unusual input patterns, repeated failed sanitization attempts, and DOM errors that may indicate exploitation attempts.

  • User Reports & Telemetry: allow users to report suspicious content and monitor client-side exceptions that may indicate DOM injections.

Safe Testing & Responsible Learning

  • Practice only in authorized labs — OWASP Juice Shop, WebGoat, PortSwigger Academy.

  • Use unit tests to assert that outputs are encoded correctly.

  • Use integration tests that render pages and assert that untrusted content is escaped/sanitized.

  • For red team assessments, follow written authorization and scope limits.

Quick Checklist — XSS Hardening

  • No raw user input inserted into innerHTML or templates without escaping/sanitization.

  • Use textContent for plain text.

  • Sanitize allowed HTML with a trusted library when rendering rich text.

  • Session cookies set with HttpOnly, Secure, and proper SameSite.

  • CSP implemented and monitored (start with report-only mode).

  • Dangerous JS APIs are minimized and reviewed.

  • Automated scanning and CSP reporting enabled.

  • Testing (unit/integration) covers output encoding.

Make XSS Hard to Exploit, Easy to Detect

XSS is fundamentally a data-vs-code boundary problem. Design your app so user input is always treated as data — encoded or sanitized for the intended output context — and add layers like CSP and HTTPOnly cookies to reduce impact if something slips through. Combine secure coding, runtime monitoring, and safe, authorized testing to keep your users protected.

Breaking the Web: Understanding & Preventing Vulnerabilities

Part 4 of 4

This series explores web vulnerabilities from authentication flaws to advanced exploits. Each post offers clear, practical insights to help you understand, detect, and defend against real-world attacks—empowering beginners and experts alike.

Start from the beginning

Authentication Vulnerabilities

What They Are, Why They Matter, and How to Protect Systems