XSS Filter and Defense: Practical Configuration Guide
Practical XSS defense covering output encoding, CSP, input filtering, ModSecurity WAF rules, and HttpOnly cookies. Includes deployable config.
Scope
XSS (Cross-Site Scripting) is among the most common web vulnerabilities. It applies to every public-facing web site. This tutorial is for ops, security engineers, and developers implementing XSS defenses on self-hosted or maintained apps. The config and encoding rules apply to PHP, Python, Java, and Node.js.
Prerequisites
- Permission to edit web server (Nginx/Apache) config
- Ability to edit application source (PHP, Python, Java)
- SSH access to install security modules
- Basic HTTP request/response knowledge
How it works
XSS injects malicious script into a page. Other users execute it in their browsers. Three types: reflected XSS (script arrives in a URL param and is echoed immediately), stored XSS (script is persisted in the database then rendered), DOM XSS (client-side DOM mutation; fully client-side).
Core rule: never trust user input. Any user-controlled data (URL params, form posts, HTTP headers, cookies, uploaded filenames) must be filtered, escaped, or encoded before it is written to HTML.
Steps
Step 1: Output encoding (baseline defense)
Encode user data on output. Match encoding to context:
`// PHP: htmlspecialchars function
echo htmlspecialchars($user_input, ENT_QUOTES | ENT_HTML5, 'UTF-8');
// Python Flask: Jinja2 template auto-escaping
{{ user_input }} <!-- Jinja2 enables HTML escaping by default -->
// Java: use OWASP Java Encoder
import org.owasp.encoder.Encode;
out.println(Encode.forHtml(user_input));
// Node.js: use the escape-html package
const escape = require('escape-html');
res.send(escape(user_input));`
Step 2: Set Content Security Policy (CSP)
CSP is a browser-enforced control. HTTP response headers restrict resource origins. Nginx config:
`# Nginx config: CSP header
add_header Content-Security-Policy "default-src 'self'; script-src 'self' 'nonce-{random-value}' https://trusted-cdn.com; style-src 'self' 'unsafe-inline'; img-src 'self' data:; object-src 'none'; frame-ancestors 'self'; base-uri 'self';" always;
# Apache config
Header set Content-Security-Policy "default-src 'self'; script-src 'self'; object-src 'none';"`
Key CSP directives: script-src 'self' allows same-origin scripts only; object-src 'none' blocks plugins such as Flash; frame-ancestors 'self' blocks clickjacking.
Step 3: Configure X-XSS-Protection header
`# Nginx
add_header X-XSS-Protection "1; mode=block" always;
# Apache
Header set X-XSS-Protection "1; mode=block"`
Step 4: Input filtering (server-side allowlist)
Validate user fields strictly. Prefer allowlists over denylists:
`// PHP: allow only letters, digits, and specific characters
$clean_input = preg_replace('/[^a-zA-Z0-9_@.\\-]/', '', $user_input);
// Python: use the bleach library to sanitize HTML
import bleach
allowed_tags = ['b', 'i', 'u', 'a', 'p', 'br']
clean_html = bleach.clean(user_html, tags=allowed_tags, strip=True)
// Java: use OWASP Java HTML Sanitizer
import org.owasp.html.Sanitizers;
String clean = Sanitizers.FORMATTING.sanitize(user_html);`
Step 5: ModSecurity WAF rules
ModSecurity is an open-source WAF. It intercepts XSS in front of Nginx/Apache:
`# OWASP CRS Core Rule Set: XSS detection
SecRule ARGS|ARGS_NAMES|REQUEST_HEADERS|REQUEST_HEADERS_NAMES|REQUEST_BODY|REQUEST_BODY_NAMES "@detectXSS" \
"id:941100,phase:2,deny,status:403,msg:'XSS Attack Detected'"
# Custom rule: block common XSS payloads
SecRule ARGS "(?i:<script.*?>|javascript:|onerror=|onload=|onclick=)" \
"id:950001,phase:2,deny,status:403,msg:'Common XSS Payload Blocked'"
# Rule: block scripts in data: URIs
SecRule ARGS "(?i)data:text/html" \
"id:950002,phase:2,deny,status:403,msg:'Data URI XSS Blocked'"`
Step 6: Set HttpOnly and Secure cookies
Stop JavaScript from stealing cookies:
`// PHP: set Session Cookie
ini_set('session.cookie_httponly', 1);
ini_set('session.cookie_secure', 1);
ini_set('session.cookie_samesite', 'Strict');
// Nginx: configure application-layer Cookie header
proxy_cookie_path / "/; HTTPOnly; Secure; SameSite=Strict";`
Verify
After deploy, confirm XSS defenses work:
`# Test 1: reflected XSS test
curl 'https://your-site.com/search?q=<script>alert(1)</script>'
# Expected result: script tags are HTML-escaped and displayed as text; alert does not pop up
# Test 2: check response headers
curl -I https://your-site.com/ | grep -E 'X-XSS-Protection|Content-Security-Policy'
# Expected:
# X-XSS-Protection: 1; mode=block
# Content-Security-Policy: default-src 'self'; ...
# Test 3: CSP violation test (script should be blocked)
curl -I https://your-site.com/`
FAQ
Q1: Site features break after CSP?
Over-strict CSP can block third-party scripts (analytics, ads, chat). Start with report-only (Content-Security-Policy-Report-Only). Collect violations, tighten the allowlist, then enforce. Use nonce or hash to allow specific inline scripts.
Q2: How to prevent XSS with rich-text editors (TinyMCE/Quill)?
Rich-text editors emit HTML. htmlspecialchars would destroy formatting. Sanitize with OWASP HTML Sanitizer or bleach. Keep safe tags only (b, i, u, a, p, br, ul, ol, li). Strip event-handler attributes (onerror, onclick) and javascript: URLs.
Takeaway
XSS defense is layered. Output encoding is the floor. CSP is browser-level. Input filtering shrinks the attack surface. WAF blocks known payloads. HttpOnly cookies limit impact. Single controls get bypassed. Combine them. Add XSS checks to CI/CD. Scan regularly with tools such as OWASP ZAP and XSStrike.