Defend the Web (Week 5): XSS Defense

How to Encode Output, Implement CSP & Stop Cross-Site Scripting Attacks

πŸ›‘οΈ Week 5: XSS Defense

Defend the Web: Part 5 of 8

Welcome back to Defensive Wednesday.

Hey πŸ‘‹

Yesterday you weaponized browsers. You injected JavaScript into search boxes, hijacked sessions with stored payloads, and hooked victims with BeEF. You turned innocent web pages into attack platforms. If you missed it, catch up here πŸ‘‰ Week 5 Offensive

Today? We lock it down.

You're the developer now. Your job is to make sure every pixel on your page is safe β€” even when attackers throw <script>alert('XSS')</script> at every input field.

Here's the truth most devs don't get β€” XSS happens because you're displaying data as code. That's it. User input goes in. Your page renders it. Boom. Their JavaScript runs in your users' browsers.

The fix? Simple in theory. Hard in practice.

Encode everything. Trust nothing. Separate content from code.

Let's build pages so clean that even BeEF bounces off them. 🧱

⚠️ Think your Internet Service Provider can't see what you're doing online?

Wrong.

Every site. Every search. Every download. Logged. Sold. Handed over when asked.

Your browsing history is worth $0.50 to advertisers. Your ISP made the sale before you finished reading this.

NordVPN makes you invisible. Military-grade encryption. Zero logs. Your ISP sees nothing.

74% off + 3 months free (limited time) β†’ Claim your privacy

🎯 The Core Problem

Every XSS attack exploits one fatal mistake: rendering user input without encoding it.

When you write code that directly displays user input, you're handing attackers the keys to your users' browsers.

I'll send your users a malicious link. Your page displays my payload. Their browser executes it. I steal their cookies.

Three golden rules:

  1. Encode all user data before displaying it

  2. Use Content Security Policy (CSP) to block unauthorized scripts

  3. Validate and sanitize inputs (but never rely on this alone)

That's it. Follow these, and XSS becomes nearly impossible.

Let's break down exactly how.

πŸ”’ Defense #1: Output Encoding (Your Primary Shield)

This is your nuclear option. Not optional. Not negotiable.

Output encoding converts dangerous characters into safe ones before displaying them on the page.

When I type <script>alert('XSS')</script>, your app should display it as &lt;script&gt;alert('XSS')&lt;/script&gt;.

The browser renders it as text, not code. My attack becomes visible gibberish.

Context-Specific Encoding (This is Critical)

Here's where most devs screw up β€” different contexts need different encoding.

HTML context (displaying in page content):

  • Encode: < > " ' &

  • Use: HTML entity encoding

JavaScript context (inside <script> tags):

  • Encode: ' " \ /

  • Use: JavaScript escaping

URL context (inside href or src):

  • Encode: everything

  • Use: URL encoding

CSS context (inside style attributes):

  • Encode: special CSS chars

  • Use: CSS escaping

Pro tip: Don't try to build your own encoder. Use battle-tested libraries for your language.

Resources:

OWASP XSS Prevention Cheat Sheet β€” Complete encoding guide

PortSwigger Context-Specific Encoding β€” Detailed examples

DOMPurify GitHub β€” JavaScript HTML sanitizer library

OWASP ESAPI Libraries β€” Encoding libraries for multiple languages

Language-Specific Encoding Libraries:

Python: MarkupSafe (Jinja2), html.escape()

JavaScript: DOMPurify, he (HTML entities)

Why this matters:

Even if I bypass your input filters, my payload gets neutered when you encode the output. I can type whatever I want β€” it'll display as harmless text.

πŸ”’ Defense #2: Content Security Policy (CSP)

CSP is your second line of defense. It tells browsers: "Only execute scripts from these sources. Block everything else."

What CSP does:

βœ… Blocks inline JavaScript (like <script>alert(1)</script>)

βœ… Blocks eval() and other dangerous functions

βœ… Whitelists safe script sources

βœ… Blocks unauthorized external scripts

βœ… Prevents data exfiltration to unknown domains

How to implement it:

Add this HTTP header to every response:

Content-Security-Policy: default-src 'self'; script-src 'self' https://trusted-cdn.com; object-src 'none';

What this does:

  • default-src 'self' β€” Only load resources from your domain

  • script-src 'self' https://trusted-cdn.com β€” Only run scripts from your domain or trusted CDN

  • object-src 'none' β€” Block plugins like Flash

Start strict, then relax as needed.

CSP Levels:

Level 1 (Basic):

  • Whitelist script sources

  • Block inline scripts

Level 2 (Better):

  • Use nonces (random tokens) for inline scripts

  • Use hashes for specific inline scripts

Level 3 (Best):

  • Use strict-dynamic

  • Eliminate unsafe-inline completely

Example strict CSP:

Content-Security-Policy: script-src 'nonce-{random}' 'strict-dynamic'; object-src 'none'; base-uri 'none';

Resources:

MDN CSP Guide β€” Complete reference

CSP Evaluator β€” Test your policy

Google Strict CSP Guide β€” Best practices

CSP Level 3 Spec β€” Technical details

Pro tip: Use CSP in report-only mode first. Monitor what breaks. Then enforce.

Add this header to test without blocking:

Content-Security-Policy-Report-Only: your-policy-here; report-uri /csp-report

Why this matters:

Even if XSS gets through, CSP blocks the malicious script from executing. I can inject <script> tags all day β€” your CSP won't let them run.

πŸ”’ Defense #3: Input Validation & Sanitization (But Never Trust This Alone)

Encoding handles output. CSP blocks execution. Input validation is your early warning system.

Validation strategies:

βœ… Whitelist, don't blacklist β€” Define what's allowed, not what's forbidden

βœ… Type checking β€” Expecting an email? Reject anything that doesn't match email format

βœ… Length limits β€” Comment should be 500 chars? Enforce it

βœ… Character restrictions β€” Username should be alphanumeric? Block everything else

Example rules:

  • Name field: Only letters, spaces, hyphens

  • Email field: Must match email regex pattern

  • Age field: Must be integer between 1-120

  • Comment field: Strip HTML tags (or use markdown)

HTML Sanitization:

If users need to submit rich content (like forum posts), use a sanitizer that strips dangerous tags while keeping safe ones.

Allowed: <b>, <i>, <u>, <p>, <br>

Blocked: <script>, <iframe>, <object>, <embed>, event handlers

Resources:

OWASP Input Validation Cheat Sheet β€” Complete guide

DOMPurify β€” HTML sanitizer (works in browser and Node.js)

Bleach β€” Python HTML sanitizer

HTML Purifier β€” PHP sanitizer

OWASP Java HTML Sanitizer β€” Java sanitizer

Validator.js β€” JavaScript validation library

Warning: Validation is your second layer, not your first. Attackers bypass client-side validation easily. Always validate server-side. Always encode output.

Never use blacklists. I'll find the bypass you didn't think of.

πŸ”’ Defense #4: Use Safe APIs & Avoid Dangerous Functions

Some JavaScript functions are XSS magnets. Avoid them completely.

Dangerous Functions (Never Use with User Input):

❌ innerHTML β€” Interprets HTML, executes scripts

❌ document.write() β€” Writes directly to page

❌ eval() β€” Executes arbitrary JavaScript

❌ setTimeout() with strings β€” Executes code

❌ setInterval() with strings β€” Executes code

❌ Function() constructor β€” Creates executable code

❌ element.outerHTML β€” Same as innerHTML

Safe Alternatives:

βœ… Use textContent instead of innerHTML β€” Treats everything as text

βœ… Use setAttribute() instead of direct property assignment

βœ… Use createElement() and appendChild() instead of innerHTML

βœ… Use DOM APIs instead of string manipulation

Example:

Bad:

element.innerHTML = userInput;

Good:

element.textContent = userInput;

Resources:

MDN textContent vs innerHTML β€” Safe text insertion

DOM Manipulation Security β€” OWASP guide

JavaScript Security Best Practices β€” PortSwigger guide

Why this matters:

The wrong API turns every user input into a potential attack vector. The right API makes XSS impossible by design.

πŸ”’ Defense #5: HTTPOnly & Secure Cookies

Cookies are the primary target of XSS attacks. Lock them down.

HTTPOnly flag:

  • Prevents JavaScript from accessing cookies

  • Even if XSS succeeds, attacker can't steal session cookies

  • Set on all authentication cookies

Secure flag:

  • Cookie only sent over HTTPS

  • Prevents interception over insecure connections

SameSite flag:

  • Prevents cookies from being sent in cross-site requests

  • Mitigates CSRF and some XSS scenarios

  • Options: Strict, Lax, None

How to set them:

Set-Cookie: sessionid=abc123; HttpOnly; Secure; SameSite=Strict

Resources:

MDN Set-Cookie β€” Complete cookie reference

OWASP Session Management Cheat Sheet β€” Cookie security

SameSite Cookie Explained β€” Google guide

Why this matters:

Even if I find XSS, I can't steal the session cookie. Your HTTPOnly flag blocks me. Game over.

πŸ”’ Defense #6: Framework-Level Protections

Modern frameworks come with XSS protections built-in. Use them correctly.

React:

βœ… JSX escapes output by default

βœ… Never use dangerouslySetInnerHTML with user input

βœ… Use sanitizers like DOMPurify if you must render HTML

Vue.js:

βœ… Template syntax escapes output automatically

βœ… Avoid v-html directive with user input

βœ… Use text interpolation {{ }} for safe rendering

Angular:

βœ… Template binding escapes output by default

βœ… Built-in sanitizer for HTML, styles, URLs

βœ… Never bypass sanitization unless absolutely necessary

Resources:

React Security β€” Official security docs

Vue.js Security β€” Security best practices

Angular Security Guide β€” Built-in protections

Pro tip: Don't fight your framework's security features. They're there for a reason. If you're bypassing them, you're probably doing something wrong.

Why this matters:

Frameworks do most of the heavy lifting. But only if you use them correctly. One dangerouslySetInnerHTML can undo everything.

That's Week 5 defense done. πŸ›‘οΈ

Next Tuesday: File Uploads & Remote Code Execution β€” we're uploading shells and taking over servers.

Next Wednesday: File Upload Defense β€” how to safely handle user files without getting pwned.

See you then.

β€” Zwire ✌️

Your Feedback Matters

Did You Enjoy This Week’s Defensive Tutorial?

Login or Subscribe to participate in polls.

P.S. Got questions about implementing these defenses? Reply to this email. I read everything.

Reply

or to participate.