Security Best Practices
1. Content Security Policy Implementation
| CSP Directive | Syntax | Description | Example Value |
|---|---|---|---|
| default-src | default-src 'self' |
Fallback for all fetch directives; restricts all resources to same origin | 'self' https://cdn.example.com |
| script-src | script-src 'self' 'nonce-xyz' |
Controls valid sources for JavaScript execution | 'self' 'unsafe-inline' https://apis.google.com |
| style-src | style-src 'self' 'sha256-hash' |
Defines valid sources for stylesheets | 'self' 'unsafe-inline' https://fonts.googleapis.com |
| img-src | img-src 'self' data: https: |
Restricts sources for images; supports data URIs and wildcards | 'self' data: https://*.cdn.com |
| connect-src | connect-src 'self' wss: |
Controls fetch, XMLHttpRequest, WebSocket connections | 'self' https://api.example.com wss://socket.io |
| font-src | font-src 'self' data: |
Specifies valid sources for fonts | 'self' https://fonts.gstatic.com data: |
| frame-src | frame-src 'none' |
Restricts iframe embedding sources | 'self' https://www.youtube.com |
| base-uri | base-uri 'self' |
Limits URLs that can be used in <base> element | 'self' (prevents base tag injection) |
| form-action | form-action 'self' |
Restricts form submission targets | 'self' https://secure-payment.com |
| upgrade-insecure-requests | upgrade-insecure-requests |
Automatically upgrade HTTP requests to HTTPS | No value needed (directive only) |
Example: CSP implementation in HTML meta tag
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<!-- Content Security Policy -->
<meta http-equiv="Content-Security-Policy"
content="default-src 'self';
script-src 'self' 'nonce-2726c7f26c' https://cdn.example.com;
style-src 'self' 'unsafe-inline' https://fonts.googleapis.com;
img-src 'self' data: https:;
font-src 'self' https://fonts.gstatic.com;
connect-src 'self' https://api.example.com;
frame-src 'none';
base-uri 'self';
form-action 'self';
upgrade-insecure-requests;">
<!-- Script with matching nonce -->
<script nonce="2726c7f26c">
console.log('This script is allowed by CSP');
</script>
<link rel="stylesheet" href="https://fonts.googleapis.com/css2?family=Roboto">
</head>
<body>
<img src="image.jpg" alt="Allowed">
<img src="https://external.com/image.jpg" alt="Also allowed">
</body>
</html>
CSP Keywords:
'self'- Same origin only'none'- Block all sources'unsafe-inline'- Allow inline scripts/styles (avoid)'unsafe-eval'- Allow eval() (avoid)'nonce-{random}'- Cryptographic nonce for specific resources'sha256-{hash}'- Whitelist by content hash
Security Warning: Avoid
'unsafe-inline' and 'unsafe-eval' in
production. Use nonces or hashes for inline scripts. Implement CSP in report-only mode first to detect
violations before enforcing.
2. XSS Prevention and Input Sanitization
| Technique | Implementation | Description | Protection Level |
|---|---|---|---|
| HTML Entity Encoding | Escape < > & " ' | Convert special characters to HTML entities before output | Essential - Prevents most XSS |
| Attribute Encoding | Encode attribute values | Escape quotes and special chars in HTML attributes | Required for attribute injection |
| JavaScript Encoding | Escape for JS context | Properly encode data inserted into JavaScript | Critical for dynamic JS |
| URL Encoding | encodeURIComponent() |
Encode user input before adding to URLs | Prevents URL injection |
| Content-Type Header | text/html; charset=UTF-8 |
Explicitly set content type to prevent MIME sniffing | Defense in depth |
| X-Content-Type-Options | nosniff |
Prevent browser from MIME-sniffing responses | Prevents XSS via uploads |
| DOMPurify Library | Sanitize untrusted HTML | Remove dangerous elements/attributes from user HTML | For rich text content |
| Trusted Types API NEW | Enforce type checking for DOM sinks | Browser-enforced XSS protection for innerHTML, eval, etc. | Chrome 83+, Edge 83+ |
Example: XSS prevention techniques
<!-- VULNERABLE: Direct user input insertion -->
<div id="output"></div>
<script>
// NEVER do this:
const userInput = '<img src=x onerror=alert("XSS")>';
document.getElementById('output').innerHTML = userInput; // XSS!
</script>
<!-- SAFE: Use textContent for plain text -->
<div id="safe-output"></div>
<script>
const userInput = '<img src=x onerror=alert("XSS")>';
document.getElementById('safe-output').textContent = userInput; // Safe!
// Output: <img src=x onerror=alert("XSS")>
</script>
<!-- SAFE: HTML entity encoding (server-side) -->
<div>
<!-- User input: <script>alert('XSS')</script> -->
<!-- Rendered: <script>alert('XSS')</script> -->
<script>alert('XSS')</script>
</div>
<!-- SAFE: DOMPurify for rich content -->
<script src="https://cdn.jsdelivr.net/npm/dompurify/dist/purify.min.js"></script>
<script>
const dirtyHTML = '<p>Safe content</p><script>alert("XSS")</script>';
const clean = DOMPurify.sanitize(dirtyHTML);
document.body.innerHTML = clean; // Only <p> tag remains
</script>
<!-- SAFE: Attribute encoding -->
<input type="text" value="user-input-here" data-user="escaped-value">
<!-- SAFE: URL encoding -->
<script>
const searchTerm = 'user&input';
const url = '/search?q=' + encodeURIComponent(searchTerm);
// Result: /search?q=user%26input
</script>
Common XSS Attack Vectors:
innerHTML,outerHTML,document.write()with unsanitized input- Event handlers in attributes:
<img onerror="...">,<body onload="..."> - JavaScript URLs:
<a href="javascript:alert(1)"> - Data URIs:
<img src="data:text/html,<script>..."> - SVG script injection:
<svg><script>alert(1)</script></svg> - CSS expression injection (IE legacy):
style="expression(alert(1))"
3. CSRF Protection in Forms
| Protection Method | Implementation | Description | Effectiveness |
|---|---|---|---|
| CSRF Token | <input type="hidden" name="csrf_token" value="random"> |
Server-generated unique token per session/request | Most effective |
| SameSite Cookie Attribute | Set-Cookie: session=abc; SameSite=Strict |
Prevent cookie from being sent in cross-site requests | Strong defense |
| Double Submit Cookie | Send token in both cookie and request body | Server validates both match; no server-side state needed | Stateless alternative |
| Custom Request Header | X-Requested-With: XMLHttpRequest |
AJAX requests include custom header; can't be set cross-origin | For API endpoints |
| Origin/Referer Validation | Check Origin/Referer headers match | Verify request comes from same origin | Supplementary check |
| Re-authentication | Require password for sensitive actions | User must confirm identity for critical operations | High-value transactions |
Example: CSRF protection implementation
<!-- Method 1: Synchronizer Token Pattern -->
<form action="/transfer" method="POST">
<!-- Server generates unique token per session -->
<input type="hidden" name="csrf_token" value="8f7d6c5b4a3e2d1c">
<label>Amount: <input type="number" name="amount" required></label>
<label>To: <input type="text" name="recipient" required></label>
<button type="submit">Transfer</button>
</form>
<!-- Method 2: Double Submit Cookie Pattern -->
<!-- Server sets cookie: csrf_token=abc123 -->
<form action="/delete-account" method="POST">
<!-- Same value as cookie -->
<input type="hidden" name="csrf_token" value="abc123">
<button type="submit">Delete Account</button>
</form>
<!-- Method 3: SameSite Cookie (server response header) -->
<!--
Set-Cookie: sessionId=xyz789; SameSite=Strict; Secure; HttpOnly
SameSite values:
- Strict: Never sent in cross-site requests
- Lax: Sent on top-level navigation (GET only)
- None: Always sent (requires Secure flag)
-->
<!-- Method 4: Custom header for AJAX -->
<script>
fetch('/api/delete-user', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-CSRF-Token': document.querySelector('meta[name="csrf-token"]').content,
'X-Requested-With': 'XMLHttpRequest'
},
body: JSON.stringify({ userId: 123 })
});
</script>
<!-- Store CSRF token in meta tag for JavaScript access -->
<meta name="csrf-token" content="8f7d6c5b4a3e2d1c">
Best Practices: Use multiple layers: SameSite cookies (Lax or Strict) + CSRF tokens for
state-changing operations. For sensitive actions (money transfer, password change), add re-authentication.
Always use POST/PUT/DELETE for state changes, never GET.
4. Secure Embedding and Sandboxing
| Attribute/Directive | Syntax | Description | Security Impact |
|---|---|---|---|
| sandbox (iframe) | <iframe sandbox> |
Enable all restrictions (most secure default) | Blocks scripts, forms, popups, same-origin access |
| sandbox="allow-scripts" | <iframe sandbox="allow-scripts"> |
Allow JavaScript execution only | Scripts can run but can't submit forms or open popups |
| sandbox="allow-same-origin" | <iframe sandbox="allow-same-origin"> |
Allow same-origin access | Can access cookies/storage of parent if same origin |
| sandbox="allow-forms" | <iframe sandbox="allow-forms"> |
Allow form submission | Forms can be submitted |
| sandbox="allow-popups" | <iframe sandbox="allow-popups"> |
Allow popup windows | Can open new windows/tabs |
| sandbox="allow-top-navigation" | <iframe sandbox="allow-top-navigation"> |
Allow navigating top-level window | Can change parent page URL (risky) |
| X-Frame-Options | X-Frame-Options: DENY |
Prevent your page from being embedded | Protects against clickjacking attacks |
| frame-ancestors (CSP) | frame-ancestors 'none' |
Modern alternative to X-Frame-Options | More flexible; supports multiple origins |
Example: Secure iframe embedding
<!-- Most restrictive: Completely sandboxed (no scripts, forms, or navigation) -->
<iframe src="untrusted.html" sandbox></iframe>
<!-- Allow scripts but nothing else -->
<iframe src="third-party-widget.html"
sandbox="allow-scripts">
</iframe>
<!-- Allow scripts and forms (common for embedded content) -->
<iframe src="https://external.com/embed"
sandbox="allow-scripts allow-forms allow-popups"
loading="lazy">
</iframe>
<!-- DANGEROUS: allow-same-origin + allow-scripts = no sandbox protection! -->
<!-- Iframe can remove its own sandbox attribute via JavaScript -->
<iframe src="malicious.html"
sandbox="allow-same-origin allow-scripts"> <!-- AVOID THIS -->
</iframe>
<!-- Prevent your page from being embedded (clickjacking protection) -->
<meta http-equiv="X-Frame-Options" content="DENY">
<!-- Or allow only same origin: -->
<meta http-equiv="X-Frame-Options" content="SAMEORIGIN">
<!-- Modern CSP alternative (preferred) -->
<meta http-equiv="Content-Security-Policy"
content="frame-ancestors 'none';">
<!-- Or whitelist specific origins: -->
<meta http-equiv="Content-Security-Policy"
content="frame-ancestors 'self' https://trusted.com;">
<!-- Secure YouTube embed example -->
<iframe src="https://www.youtube-nocookie.com/embed/VIDEO_ID"
sandbox="allow-scripts allow-same-origin allow-presentation"
allow="accelerometer; encrypted-media; gyroscope; picture-in-picture"
loading="lazy"
width="560"
height="315">
</iframe>
Critical Security Rule: NEVER combine
allow-same-origin and
allow-scripts when embedding untrusted content. This combination allows the iframe to remove its
own sandbox restrictions, completely bypassing security.
5. HTTP Security Headers
| Header | Value/Directive | Description | Implementation |
|---|---|---|---|
| Strict-Transport-Security (HSTS) | max-age=31536000; includeSubDomains; preload |
Force HTTPS for all future requests | Essential for HTTPS sites |
| X-Content-Type-Options | nosniff |
Prevent MIME-type sniffing | Always enable |
| X-Frame-Options | DENY | SAMEORIGIN |
Clickjacking protection (legacy, use CSP) | Use with CSP frame-ancestors |
| X-XSS-Protection | 0 |
Disable legacy XSS filter (can cause vulnerabilities) | Set to 0 |
| Referrer-Policy | strict-origin-when-cross-origin |
Control Referer header information leakage | Recommended |
| Permissions-Policy | geolocation=(), camera=(), microphone=() |
Control browser features and APIs access | Feature restrictions |
| Cross-Origin-Embedder-Policy (COEP) | require-corp |
Require explicit opt-in for cross-origin resources | Enables SharedArrayBuffer |
| Cross-Origin-Opener-Policy (COOP) | same-origin |
Isolate browsing context from cross-origin windows | Spectre mitigation |
| Cross-Origin-Resource-Policy (CORP) | same-site | same-origin | cross-origin |
Control which sites can embed resource | Resource isolation |
Example: Security headers in HTML meta tags
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<!-- Content Security Policy -->
<meta http-equiv="Content-Security-Policy"
content="default-src 'self'; script-src 'self' 'nonce-xyz'; style-src 'self' 'unsafe-inline'; img-src 'self' data: https:; frame-ancestors 'none';">
<!-- Prevent MIME sniffing -->
<meta http-equiv="X-Content-Type-Options" content="nosniff">
<!-- Clickjacking protection -->
<meta http-equiv="X-Frame-Options" content="DENY">
<!-- Disable legacy XSS filter (prevents vulnerabilities) -->
<meta http-equiv="X-XSS-Protection" content="0">
<!-- Referrer policy -->
<meta name="referrer" content="strict-origin-when-cross-origin">
<!-- Permissions policy (control browser features) -->
<meta http-equiv="Permissions-Policy"
content="geolocation=(), camera=(), microphone=(), payment=()">
</head>
<body>
<!-- Content -->
</body>
</html>
<!-- Note: While meta tags work for CSP and some headers,
HTTP response headers are preferred and more reliable.
Configure these in server settings:
Strict-Transport-Security: max-age=31536000; includeSubDomains; preload
X-Content-Type-Options: nosniff
X-Frame-Options: DENY
Content-Security-Policy: default-src 'self'
Referrer-Policy: strict-origin-when-cross-origin
Permissions-Policy: geolocation=(), camera=()
Cross-Origin-Opener-Policy: same-origin
Cross-Origin-Embedder-Policy: require-corp
-->
| Referrer-Policy Value | Behavior | Use Case |
|---|---|---|
| no-referrer | Never send Referer header | Maximum privacy, may break analytics |
| no-referrer-when-downgrade | Send full URL except HTTPS→HTTP | Default browser behavior |
| origin | Send only origin (no path) | Balance privacy and functionality |
| strict-origin-when-cross-origin | Full URL for same-origin; origin only cross-origin | Recommended default |
| same-origin | Send Referer only to same origin | High privacy, may affect external integrations |
6. Form Security and Data Protection
| Security Measure | Implementation | Description | Protection Against |
|---|---|---|---|
| HTTPS Only | <form action="https://..." method="POST"> |
Always submit forms over encrypted connection | Man-in-the-middle, eavesdropping |
| autocomplete="off" | <input autocomplete="off"> |
Disable browser autofill for sensitive fields | Credential theft from shared computers |
| autocomplete="new-password" | <input type="password" autocomplete="new-password"> |
Prevent autofill of existing passwords | Password manager confusion |
| maxlength Attribute | <input maxlength="50"> |
Limit input length (client-side) | Buffer overflow, DoS (supplement server validation) |
| input type Validation | <input type="email|url|tel"> |
Browser validates input format | Malformed data (basic validation only) |
| pattern Attribute | <input pattern="[A-Za-z]{3,}"> |
Regex validation on client side | Invalid formats (must validate server-side too) |
| novalidate Attribute | <form novalidate> |
Disable HTML5 validation (use custom JS) | N/A - allows custom validation logic |
| rel="noopener noreferrer" | <form target="_blank" rel="noopener"> |
Prevent window.opener access | Reverse tabnabbing attacks |
| Rate Limiting | Server-side throttling | Limit submissions per IP/user | Brute force, spam, DoS attacks |
| CAPTCHA | reCAPTCHA, hCaptcha integration | Distinguish humans from bots | Automated spam, credential stuffing |
Example: Secure form implementation
<!-- Secure login form -->
<form action="https://example.com/login"
method="POST"
autocomplete="off">
<!-- CSRF token -->
<input type="hidden" name="csrf_token" value="secure-random-token">
<!-- Username/email field -->
<label for="email">Email:</label>
<input type="email"
id="email"
name="email"
required
autocomplete="username"
maxlength="100"
pattern="[a-z0-9._%+-]+@[a-z0-9.-]+\.[a-z]{2,}$">
<!-- Password field -->
<label for="password">Password:</label>
<input type="password"
id="password"
name="password"
required
autocomplete="current-password"
minlength="8"
maxlength="128">
<button type="submit">Login</button>
</form>
<!-- Secure registration form -->
<form action="https://example.com/register"
method="POST"
autocomplete="off">
<input type="hidden" name="csrf_token" value="secure-random-token">
<label for="new-email">Email:</label>
<input type="email"
id="new-email"
name="email"
required
autocomplete="email"
maxlength="100">
<label for="new-password">Password:</label>
<input type="password"
id="new-password"
name="password"
required
autocomplete="new-password"
minlength="8"
aria-describedby="password-requirements">
<div id="password-requirements" class="note">
Must be at least 8 characters with uppercase, lowercase, number, and special character
</div>
<!-- CAPTCHA -->
<div class="g-recaptcha" data-sitekey="your-site-key"></div>
<button type="submit">Register</button>
</form>
<!-- Payment form (extra security) -->
<form action="https://secure-payment.com/process"
method="POST"
autocomplete="off"
target="_blank"
rel="noopener noreferrer">
<input type="hidden" name="csrf_token" value="secure-token">
<label for="card">Card Number:</label>
<input type="text"
id="card"
name="card_number"
required
autocomplete="cc-number"
pattern="[0-9]{13,19}"
maxlength="19"
inputmode="numeric">
<label for="cvv">CVV:</label>
<input type="password"
id="cvv"
name="cvv"
required
autocomplete="cc-csc"
pattern="[0-9]{3,4}"
maxlength="4"
inputmode="numeric">
<button type="submit">Pay Securely</button>
</form>
Autocomplete Values for Security:
username- Email/username fieldcurrent-password- Login passwordnew-password- Registration/change passwordone-time-code- 2FA/OTP codescc-number- Credit card numbercc-csc- Card security codeoff- Disable completely (sensitive data)
Never Trust Client-Side Validation:
- Always validate and sanitize on server
- HTML5 validation can be bypassed
- Use client validation for UX only
- Implement server-side rate limiting
- Log and monitor suspicious activity
- Never store sensitive data in localStorage
Critical Reminders: Client-side security is defense in depth only - never trust the client.
Always validate, sanitize, and authorize on the server. Security is a process, not a feature - regular audits
and updates are essential.