Cross-Site Scripting (XSS)
Types, Detection, and Practical Defenses (with Safe Code Examples)

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)
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.
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.
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, '&')
.replace(/</g, '<')
.replace(/>/g, '>')
.replace(/"/g, '"')
.replace(/'/g, ''');
}
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
textContentorinnerTextwhen 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-inlineforscript-srcandstyle-src. Prefer nonces or hashes when inline styles/scripts are unavoidable.Use
report-uriorreport-toto 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)
Output Encoding — encode untrusted data for the specific output context (HTML body, attribute, URL, CSS, JavaScript). Context matters.
Use Frameworks that Auto-Escape — trust battle-tested templating features rather than manual concatenation.
Sanitize Allowed HTML — if users can supply limited markup, sanitize with a maintained library (DOMPurify, bleach for Python).
Avoid Dangerous APIs — reduce use of
innerHTML,document.write,eval,setTimeout(string), ornew Function(...).Use HTTPOnly & Secure Cookies — prevent client-side scripts from reading session tokens.
Apply CSP — restrict script origins and disallow inline scripts or use nonces/hashes where needed.
Principle of Least Privilege for Data — avoid rendering or exposing more data than needed.
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
innerHTMLor templates without escaping/sanitization.Use
textContentfor plain text.Sanitize allowed HTML with a trusted library when rendering rich text.
Session cookies set with
HttpOnly,Secure, and properSameSite.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.


