HTML Security Best Practices
Protect your websites from common security vulnerabilities. Learn about XSS, CSRF, clickjacking, Content Security Policy, and security headers for safer web applications.
Why Web Security Matters
Web security vulnerabilities can lead to data breaches, stolen user information, defaced websites, and loss of trust. Understanding HTML security is the first line of defense.
43%
of cyber attacks target small businesses
$4.35M
average cost of a data breach (2022)
68%
of breaches take months to discover
Cross-Site Scripting (XSS)
XSS attacks inject malicious scripts into web pages viewed by other users.
Types of XSS:
1. Reflected XSS
Malicious script comes from current HTTP request
<!-- Vulnerable code -->
<p>Search results for: <?php echo $_GET['q']; ?></p>
<!-- Attacker sends: -->
example.com/search?q=<script>alert('XSS')</script>2. Stored XSS
Malicious script stored in database
<!-- Vulnerable: User comment stored without sanitization -->
<div class="comment">
<script>/* Malicious code */</script>
</div>3. DOM-based XSS
Vulnerability in client-side JavaScript
// Vulnerable JavaScript
const name = new URLSearchParams(window.location.search).get('name');
document.getElementById('welcome').innerHTML = `Hello ${name}`;
// Attacker uses:
// example.com?name=<img src=x onerror=alert('XSS')>Prevention: Escape User Input
// NEVER do this with user input!
const userInput = getUserInput();
element.innerHTML = userInput; // XSS vulnerability!
document.write(userInput); // Also vulnerable!// Use textContent to escape HTML
const userInput = getUserInput();
element.textContent = userInput; // Safe! Auto-escapes HTML
// Or create text nodes
const textNode = document.createTextNode(userInput);
element.appendChild(textNode);Sanitize HTML Input
// Use DOMPurify library for HTML sanitization
import DOMPurify from 'dompurify';
const userHTML = getUserInput();
const cleanHTML = DOMPurify.sanitize(userHTML);
element.innerHTML = cleanHTML; // Now safe
// Or use template literals with escaping
function escapeHTML(str) {
const div = document.createElement('div');
div.textContent = str;
return div.innerHTML;
}
const safe = escapeHTML(userInput);Content Security Policy (CSP)
CSP helps prevent XSS by restricting what resources can be loaded:
<meta http-equiv="Content-Security-Policy"
content="default-src 'self';
script-src 'self' https://trusted-cdn.com;
style-src 'self' 'unsafe-inline';
img-src 'self' data: https:;
font-src 'self' https://fonts.gstatic.com;
connect-src 'self' https://api.example.com;
frame-ancestors 'none';">CSP Directives:
| Directive | Purpose | Example |
|---|---|---|
default-src | Fallback for other directives | 'self' |
script-src | Valid sources for JavaScript | 'self' cdn.com |
style-src | Valid sources for stylesheets | 'self' 'unsafe-inline' |
img-src | Valid sources for images | 'self' https: |
frame-ancestors | Who can embed this page | 'none' |
CSP with Nonces
<!-- Server generates unique nonce per request -->
<meta http-equiv="Content-Security-Policy"
content="script-src 'nonce-random123abc'">
<!-- Only scripts with matching nonce execute -->
<script nonce="random123abc">
// This script will execute
console.log('Allowed!');
</script>
<script>
// This script will be blocked (no nonce)
console.log('Blocked!');
</script>Clickjacking Protection
Prevent your site from being embedded in malicious iframes:
1. X-Frame-Options Header
<!-- Via meta tag (limited support) -->
<meta http-equiv="X-Frame-Options" content="DENY">
<!-- Options: -->
<!-- DENY - Cannot be framed at all -->
<!-- SAMEORIGIN - Can only be framed by same origin -->
<!-- ALLOW-FROM https://example.com - Specific origin (deprecated) -->2. CSP frame-ancestors
<!-- Prevent all framing -->
<meta http-equiv="Content-Security-Policy"
content="frame-ancestors 'none'">
<!-- Allow same origin -->
<meta http-equiv="Content-Security-Policy"
content="frame-ancestors 'self'">
<!-- Allow specific origins -->
<meta http-equiv="Content-Security-Policy"
content="frame-ancestors 'self' https://trusted-site.com">3. JavaScript Frame-Busting (Fallback)
<script>
// Prevent page from being framed
if (window.top !== window.self) {
window.top.location = window.self.location;
}
</script>Cross-Site Request Forgery (CSRF)
CSRF tricks users into performing unwanted actions while authenticated:
Attack Example:
<!-- Malicious site creates hidden form -->
<form action="https://bank.com/transfer" method="POST">
<input type="hidden" name="amount" value="1000">
<input type="hidden" name="to" value="attacker-account">
</form>
<script>
// Auto-submit when victim visits page
document.forms[0].submit();
</script>Prevention: CSRF Tokens
<!-- Server generates unique token per session -->
<form action="/transfer" method="POST">
<input type="hidden" name="csrf_token" value="unique-token-123">
<input type="text" name="amount">
<button type="submit">Transfer</button>
</form>
<!-- Server validates token matches session before processing -->SameSite Cookies
<!-- Set cookies with SameSite attribute -->
Set-Cookie: sessionid=abc123; SameSite=Strict; Secure; HttpOnly
<!-- SameSite options: -->
<!-- Strict - Cookie not sent on any cross-site request -->
<!-- Lax - Cookie sent on top-level navigation (default) -->
<!-- None - Cookie sent on all requests (requires Secure) -->Secure Forms
1. Use HTTPS
<!-- Always use HTTPS for forms with sensitive data -->
<form action="https://example.com/login" method="POST">
<input type="email" name="email" required>
<input type="password" name="password" required autocomplete="current-password">
<button type="submit">Login</button>
</form>2. Autocomplete Attributes
<!-- Control autocomplete for sensitive fields -->
<form>
<!-- Enable autocomplete for username -->
<input type="text" name="username" autocomplete="username">
<!-- Enable for current password -->
<input type="password" name="password" autocomplete="current-password">
<!-- For new password -->
<input type="password" name="new-password" autocomplete="new-password">
<!-- Disable for sensitive fields -->
<input type="text" name="credit-card" autocomplete="off">
</form>3. Input Validation
<!-- Client-side validation (user experience) -->
<form>
<input type="email"
name="email"
required
pattern="[a-z0-9._%+-]+@[a-z0-9.-]+\.[a-z]{2,}$">
<input type="password"
name="password"
required
minlength="8"
pattern="(?=.*\d)(?=.*[a-z])(?=.*[A-Z]).{8,}">
<button type="submit">Submit</button>
</form>
<!-- ALWAYS validate on server too! Client-side can be bypassed -->Secure Links & Downloads
1. External Links
<!-- Always use rel="noopener noreferrer" with target="_blank" -->
<a href="https://external-site.com"
target="_blank"
rel="noopener noreferrer">
External Link
</a>
<!-- Why? Prevents window.opener exploitation -->2. User-Generated Links
<!-- Prevent javascript: URLs -->
<a href="javascript:alert('XSS')">Click</a> <!-- Dangerous! -->
<!-- Validate and sanitize URLs -->
<script>
function isSafeURL(url) {
try {
const parsed = new URL(url);
// Only allow http and https
return ['http:', 'https:'].includes(parsed.protocol);
} catch {
return false;
}
}
// Use it
const userURL = getUserInput();
if (isSafeURL(userURL)) {
link.href = userURL;
} else {
console.error('Invalid URL');
}
</script>3. File Downloads
<!-- Specify download attribute to prevent execution -->
<a href="/files/document.pdf" download="report.pdf">Download Report</a>
<!-- Set correct Content-Type headers on server -->
<!-- Set Content-Disposition: attachment -->
<!-- Scan uploaded files for malware -->Security Headers
HTTP headers that improve security (set on server):
1. Strict-Transport-Security (HSTS)
<!-- Force HTTPS for specified time -->
Strict-Transport-Security: max-age=31536000; includeSubDomains; preload2. X-Content-Type-Options
<!-- Prevent MIME-sniffing -->
X-Content-Type-Options: nosniff3. X-XSS-Protection
<!-- Enable browser's XSS filter (legacy browsers) -->
X-XSS-Protection: 1; mode=block4. Referrer-Policy
<!-- Control referrer information -->
Referrer-Policy: strict-origin-when-cross-origin
<meta name="referrer" content="strict-origin-when-cross-origin">5. Permissions-Policy
<!-- Control browser features -->
Permissions-Policy: geolocation=(), microphone=(), camera=()
<meta http-equiv="Permissions-Policy"
content="geolocation=(), microphone=(), camera=()">Subresource Integrity (SRI)
Verify that files from CDNs haven't been tampered with:
<!-- SRI for external scripts -->
<script src="https://cdn.example.com/library.js"
integrity="sha384-hash-of-file-content"
crossorigin="anonymous"></script>
<!-- SRI for external stylesheets -->
<link rel="stylesheet"
href="https://cdn.example.com/style.css"
integrity="sha384-hash-of-file-content"
crossorigin="anonymous">
<!-- Generate SRI hash: https://www.srihash.org/ -->Why Use SRI:
- Protects against compromised CDNs
- Prevents malicious code injection
- Ensures file integrity
- Required for security-sensitive applications
Sensitive Data Handling
1. Never Store Sensitive Data Client-Side
// NEVER store these in localStorage, sessionStorage, or cookies!
localStorage.setItem('password', userPassword); // NO!
localStorage.setItem('creditCard', cardNumber); // NO!
localStorage.setItem('ssn', socialSecurity); // NO!
localStorage.setItem('apiKey', secretKey); // NO!// Store sensitive data server-side with encryption
// Use secure session tokens
const sessionToken = generateSecureToken();
localStorage.setItem('sessionToken', sessionToken); // OK (token only)
// Use HttpOnly cookies for auth (can't be accessed by JavaScript)
Set-Cookie: auth=token; HttpOnly; Secure; SameSite=Strict2. Clear Sensitive Data
// Clear sensitive form data after use
function clearSensitiveData() {
document.getElementById('password').value = '';
document.getElementById('creditCard').value = '';
}
// Clear on logout
function logout() {
localStorage.clear();
sessionStorage.clear();
clearSensitiveData();
window.location.href = '/login';
}Security Checklist
✅ XSS Prevention:
- ☑ Use
textContentinstead ofinnerHTML - ☑ Sanitize user input with DOMPurify
- ☑ Implement Content Security Policy
- ☑ Validate and escape all user input
- ☑ Use template engines with auto-escaping
✅ CSRF Prevention:
- ☑ Implement CSRF tokens
- ☑ Use SameSite cookies
- ☑ Verify Origin/Referer headers
- ☑ Require re-authentication for sensitive actions
✅ General Security:
- ☑ Use HTTPS everywhere
- ☑ Implement security headers
- ☑ Use
rel="noopener noreferrer"on external links - ☑ Validate all input (client AND server)
- ☑ Use Subresource Integrity for CDN resources
- ☑ Never store sensitive data client-side
- ☑ Keep dependencies updated
- ☑ Regular security audits
Security Testing Tools
- Mozilla Observatory: Test security headers
- Security Headers: Check header configuration
- OWASP ZAP: Automated vulnerability scanner
- Burp Suite: Web security testing platform
- npm audit: Check for vulnerable dependencies
- Lighthouse: Security audit in Chrome DevTools
Best Practices Summary
✅ Do
- Always use HTTPS
- Escape and sanitize user input
- Implement CSP
- Use security headers
- Validate on client AND server
- Use
rel="noopener"on external links - Keep dependencies updated
- Regular security audits
⌠Don't
- Trust user input
- Use
innerHTMLwith user data - Store sensitive data client-side
- Ignore security warnings
- Use
eval()or similar functions - Disable security features
- Use deprecated security methods
- Forget to update dependencies