- Zero-Day Wire
- Posts
- Defend the Web (Week 5): XSS Defense
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:
Encode all user data before displaying it
Use Content Security Policy (CSP) to block unauthorized scripts
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 <script>alert('XSS')</script>.
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()
Java: OWASP Java Encoder
.NET: HttpUtility.HtmlEncode
Ruby: Sanitize gem
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 domainscript-src 'self' https://trusted-cdn.comβ Only run scripts from your domain or trusted CDNobject-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.
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=StrictResources:
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 MattersDid You Enjoy This Weekβs Defensive Tutorial? |
P.S. Got questions about implementing these defenses? Reply to this email. I read everything.
Reply