HTTPS and Web Security
Web Security Best Practices: A Defense-in-Depth Approach
Read aloud is unavailable in this browser.
Security as a Mindset, Not a Checklist
HTTPS, certificate validation, and HSTS form the secure transport foundation—but web security extends far beyond encrypted connections. Modern web applications face a complex threat landscape: cross-site scripting (XSS), injection attacks, authentication flaws, session hijacking, and constantly evolving attack vectors.
Security is not a product you install; it's a process you adopt. The most secure applications are built by teams who think about security at every layer—from architecture decisions through code review to operational monitoring.
This comprehensive page synthesizes web security best practices into an actionable guide. We'll cover the OWASP Top 10 vulnerabilities, essential security headers, authentication and session management, and the defense-in-depth philosophy that protects applications even when individual controls fail.
By the end of this page, you will understand: (1) The OWASP Top 10 web application vulnerabilities, (2) Essential security headers and their configuration, (3) Secure authentication and session management, (4) Input validation and output encoding, (5) Content Security Policy design, and (6) Security as a layered, defense-in-depth approach.
The OWASP Top 10: Understanding Common Vulnerabilities
The Open Web Application Security Project (OWASP) maintains the "Top 10"—a regularly updated list of the most critical web application security risks. Understanding these vulnerabilities is foundational to building secure applications.
OWASP Top 10 (2021):
| Rank | Vulnerability | Description |
|---|---|---|
| A01 | Broken Access Control | Users acting outside their intended permissions |
| A02 | Cryptographic Failures | Inadequate cryptography exposing sensitive data |
| A03 | Injection | SQL, NoSQL, OS, LDAP injection attacks |
| A04 | Insecure Design | Missing or ineffective security controls by design |
| A05 | Security Misconfiguration | Insecure default configs, missing patches |
| A06 | Vulnerable Components | Using components with known vulnerabilities |
| A07 | Auth & Session Failures | Authentication and session management flaws |
| A08 | Software & Data Integrity | Unsigned code, insecure CI/CD pipelines |
| A09 | Logging & Monitoring Failures | Insufficient logging for attack detection |
| A10 | Server-Side Request Forgery | SSRF: Server fetches attacker-controlled URLs |
Key Vulnerability Deep Dives:
A01: Broken Access Control
The #1 vulnerability—users accessing resources they shouldn't.
// VULNERABLE: No authorization check
app.get('/api/users/:id', (req, res) => {
const user = db.getUser(req.params.id); // Any user can access any profile
res.json(user);
});
// SECURE: Verify ownership
app.get('/api/users/:id', requireAuth, (req, res) => {
if (req.params.id !== req.user.id && !req.user.isAdmin) {
return res.status(403).json({ error: 'Forbidden' });
}
const user = db.getUser(req.params.id);
res.json(user);
});
Attack patterns:
- Modifying URLs:
/admin/users→ accessible by non-admins - Changing IDs:
/api/order/123→ changing to/api/order/456(other user's order) - Privilege escalation: Regular user accessing admin functions
A03: Injection
Attacker-controlled data is interpreted as code.
-- VULNERABLE: String concatenation
SELECT * FROM users WHERE username = '" + username + "' AND password = '" + password + "'
-- Attack input: username = "admin'--"
-- Result: SELECT * FROM users WHERE username = 'admin'--' AND password = ''
-- The '--' comments out the password check
-- SECURE: Parameterized queries
SELECT * FROM users WHERE username = ? AND password = ?
-- Parameters are escaped, cannot break out of string context
A07: Authentication Failures
Common authentication flaws:
- Permitting weak passwords
- Storing passwords in plaintext or weak hashes (MD5, SHA1)
- Missing brute-force protection
- Exposing session tokens in URLs
- Not invalidating sessions on logout
Defense: Multi-factor authentication, strong password policies, secure session management, rate limiting.
These vulnerabilities aren't just "security team problems." Developers introduce them in day-to-day coding. Every engineer should understand these risks and code defensively. Security reviews should be part of every code review.
Essential Security Headers
HTTP security headers instruct browsers to enforce security controls. They're a low-effort, high-impact defense layer.
| Header | Purpose | Recommended Value |
|---|---|---|
| Strict-Transport-Security | Force HTTPS | max-age=31536000; includeSubDomains |
| Content-Security-Policy | Control resource loading | (see dedicated section) |
| X-Frame-Options | Prevent clickjacking | DENY or SAMEORIGIN |
| X-Content-Type-Options | Prevent MIME sniffing | nosniff |
| Referrer-Policy | Control referrer leakage | strict-origin-when-cross-origin |
| Permissions-Policy | Disable browser features | geolocation=(), camera=() |
| X-XSS-Protection | XSS filter (legacy) | 0 (disable, CSP is better) |
X-Frame-Options: Preventing Clickjacking
Clickjacking: Attacker overlays your site in an invisible iframe, tricking users into clicking hidden buttons.
# Prevent any framing
X-Frame-Options: DENY
# Allow framing only by same origin
X-Frame-Options: SAMEORIGIN
# CSP equivalent (modern approach)
Content-Security-Policy: frame-ancestors 'none';
Content-Security-Policy: frame-ancestors 'self';
X-Content-Type-Options: Preventing MIME Sniffing
Browsers may "sniff" response content to determine type, potentially executing files as scripts:
# Prevent MIME sniffing
X-Content-Type-Options: nosniff
With nosniff, browsers strictly follow the Content-Type header.
Referrer-Policy: Controlling Information Leakage
The Referer header can leak sensitive URLs:
# Send full referrer to same-origin, origin only to cross-origin
Referrer-Policy: strict-origin-when-cross-origin
# Other options:
no-referrer # Never send referrer
no-referrer-when-downgrade # No referrer on HTTPS→HTTP
origin # Send origin only (no path)
strict-origin # Origin only, no downgrade
same-origin # Referrer only for same-origin
Permissions-Policy: Disabling Browser Features
Limit what browser features your site can use (reduces attack surface):
# Disable various features
Permissions-Policy: geolocation=(), camera=(), microphone=(), payment=()
# Allow specific features for certain origins
Permissions-Policy: fullscreen=(self "https://youtube.com")
# Common policy for most sites
Permissions-Policy: accelerometer=(), ambient-light-sensor=(), autoplay=(),
battery=(), camera=(), cross-origin-isolated=(), display-capture=(),
document-domain=(), encrypted-media=(), execution-while-not-rendered=(),
execution-while-out-of-viewport=(), fullscreen=(), geolocation=(),
gyroscope=(), keyboard-map=(), magnetometer=(), microphone=(), midi=(),
navigation-override=(), payment=(), picture-in-picture=(),
publickey-credentials-get=(), screen-wake-lock=(), sync-xhr=(), usb=(),
web-share=(), xr-spatial-tracking=()
Complete Headers Configuration (Nginx):
add_header Strict-Transport-Security "max-age=31536000; includeSubDomains; preload" always;
add_header Content-Security-Policy "default-src 'self'; script-src 'self';" always;
add_header X-Frame-Options "DENY" always;
add_header X-Content-Type-Options "nosniff" always;
add_header Referrer-Policy "strict-origin-when-cross-origin" always;
add_header Permissions-Policy "geolocation=(), camera=(), microphone=()" always;
Content Security Policy: The Swiss Army Knife of Web Security
Content Security Policy (CSP) is the most powerful security header, providing defense against XSS, injection, and data exfiltration attacks.
CSP Fundamentals:
CSP defines an allowlist of content sources. Resources not matching the policy are blocked.
# Basic CSP
Content-Security-Policy: default-src 'self'
# Meaning: Only load resources from the same origin
# Result: Third-party scripts, inline scripts, and eval() are blocked
Key CSP Directives:
| Directive | Controls | Example |
|---|---|---|
default-src | Fallback for all resource types | default-src 'self' |
script-src | JavaScript sources | script-src 'self' https://cdn.example.com |
style-src | CSS sources | style-src 'self' 'unsafe-inline' |
img-src | Image sources | img-src 'self' data: https: |
connect-src | Fetch, XHR, WebSocket | connect-src 'self' https://api.example.com |
font-src | Font sources | font-src 'self' https://fonts.gstatic.com |
frame-src | Frame/iframe sources | frame-src https://www.youtube.com |
object-src | Plugins (Flash, etc.) | object-src 'none' |
base-uri | Base URL restriction | base-uri 'self' |
form-action | Form submission targets | form-action 'self' |
frame-ancestors | Who can frame this page | frame-ancestors 'none' |
CSP Source Values:
'self' # Same origin
'none' # Block all
'unsafe-inline' # Allow inline scripts/styles (AVOID)
'unsafe-eval' # Allow eval() (AVOID)
'strict-dynamic' # Trust scripts loaded by trusted scripts
https: # Any HTTPS URL
data: # Data URIs
*.example.com # Wildcard subdomain
'nonce-abc123' # Script with matching nonce attribute
'sha256-...' # Script with matching hash
Strict CSP (Modern Best Practice):
# Nonce-based strict CSP
Content-Security-Policy:
default-src 'none';
script-src 'nonce-{random}' 'strict-dynamic';
style-src 'self';
img-src 'self' data:;
font-src 'self';
connect-src 'self';
base-uri 'none';
form-action 'self';
frame-ancestors 'none';
upgrade-insecure-requests;
Nonce Implementation:
<!-- Server generates random nonce per request -->
<script nonce="abc123">
// This script runs because nonce matches
</script>
<script>
// This script is BLOCKED - no matching nonce
</script>
<script src="https://evil.com/inject.js">
// BLOCKED - can't guess the nonce
</script>
Deploy CSP in report-only mode first to discover violations without breaking your site:
Content-Security-Policy-Report-Only: default-src 'self'; report-uri /csp-reports
Monitor reports, fix violations, then switch to enforcing mode.
CSP Violation Reporting:
Content-Security-Policy: default-src 'self'; report-to csp-endpoint;
Report-To: {
"group": "csp-endpoint",
"max_age": 10886400,
"endpoints": [
{ "url": "https://example.com/csp-reports" }
]
}
CSP Report Format:
{
"csp-report": {
"document-uri": "https://example.com/page",
"violated-directive": "script-src 'self'",
"blocked-uri": "inline",
"source-file": "https://example.com/page",
"line-number": 123,
"column-number": 456
}
}
Cross-Site Scripting (XSS) Prevention
Cross-Site Scripting (XSS) remains one of the most common web vulnerabilities. Attackers inject malicious scripts that execute in victims' browsers.
XSS Types:
1. Reflected XSS: Malicious script in URL is reflected back to user
Attack URL: https://example.com/search?q=<script>steal(document.cookie)</script>
Vulnerable response:
<p>Results for: <script>steal(document.cookie)</script></p>
2. Stored XSS: Malicious script stored in database, served to all users
Comment form → Database → Displayed to all users
Payload: <script>stealCredentials()</script>
All visitors execute attacker's script
3. DOM-based XSS: Vulnerability in client-side JavaScript
// Vulnerable: directly inserting URL parameter into DOM
const name = new URLSearchParams(location.search).get('name');
document.getElementById('greeting').innerHTML = 'Hello, ' + name;
// Attack: ?name=<img src=x onerror=evil()>
- •Output Encoding — Encode all user-controlled data before inserting into HTML:
- HTML context:
<>&'" - JavaScript context: JSON.stringify() + additional escaping
- URL context: encodeURIComponent()
- CSS context: Strict validation or avoid entirely
- HTML context:
- •Content Security Policy — Block inline scripts and restrict external sources. This is defense-in-depth—even if encoding fails, CSP can prevent execution.
- •DOM APIs — Use
textContentinstead ofinnerHTMLfor text. UsesetAttribute()for attributes. Modern frameworks auto-escape by default. - •HttpOnly Cookies — Prevent JavaScript access to sensitive cookies. Even if XSS occurs, session tokens can't be stolen.
- •Sanitization Libraries — For rich content (HTML editors), use battle-tested libraries like DOMPurify.
Framework Protections:
React:
// Safe: JSX auto-escapes by default
return <div>{userInput}</div>;
// DANGEROUS: dangerouslySetInnerHTML bypasses protection
return <div dangerouslySetInnerHTML={{__html: userInput}} />; // AUDIT CAREFULLY
Angular:
<!-- Safe: interpolation is auto-escaped -->
<div>{{userInput}}</div>
<!-- DANGEROUS: bypassSecurityTrust methods -->
<div [innerHTML]="trustedHtml"></div> <!-- Requires DomSanitizer -->
Vue:
<!-- Safe: mustache syntax auto-escapes -->
<div>{{ userInput }}</div>
<!-- DANGEROUS: v-html bypasses protection -->
<div v-html="userInput"></div> <!-- AUDIT CAREFULLY -->
Modern frameworks provide auto-escaping, but they also provide escape hatches (dangerouslySetInnerHTML, v-html, etc.). Each use of these must be audited. User input should NEVER be passed directly to these functions without sanitization.
Secure Authentication and Session Management
Authentication is the front door to your application. Weaknesses here have catastrophic consequences.
Password Security:
✓ DO:
- Use bcrypt, scrypt, or Argon2 for hashing (NOT MD5, SHA1, SHA256)
- Salt is built into these algorithms
- Use cost factor that takes ~100-500ms on your hardware
- Enforce reasonable password length (8-64+ chars)
- Check passwords against breach databases (HaveIBeenPwned API)
✗ DON'T:
- Store plaintext passwords (ever)
- Use fast hashes (MD5, SHA256—too fast to brute-force)
- Implement password rules that frustrate users (special chars required)
- Reveal whether username or password was wrong (info disclosure)
Bcrypt Example:
const bcrypt = require('bcrypt');
// Hashing (registration)
const saltRounds = 12; // Cost factor
const hashedPassword = await bcrypt.hash(plainPassword, saltRounds);
// Store hashedPassword in database
// Verification (login)
const match = await bcrypt.compare(inputPassword, storedHash);
if (match) {
// Authenticated
}
Session Management:
Secure Session Token Properties:
- Random: Cryptographically random, at least 128 bits of entropy
- Unique: One token per session
- Invalidated on logout: Server-side invalidation required
- Rotated on privilege change: New token after login
Secure Cookie Configuration:
Set-Cookie: session=abc123;
HttpOnly; // Not accessible via JavaScript
Secure; // Only sent over HTTPS
SameSite=Lax; // CSRF protection
Path=/; // Scope to entire site
Max-Age=3600; // 1 hour expiration
// Express.js session configuration
app.use(session({
name: 'session',
secret: process.env.SESSION_SECRET, // Strong random secret
resave: false,
saveUninitialized: false,
cookie: {
httpOnly: true,
secure: true, // In production
sameSite: 'lax',
maxAge: 3600000, // 1 hour
},
}));
- •TOTP (Time-based One-Time Password) — Apps like Google Authenticator, Authy. Standard, widely supported.
- •WebAuthn/FIDO2 — Hardware security keys (YubiKey). Phishing-resistant, strongest option.
- •SMS Codes — Better than nothing, but vulnerable to SIM swapping. Use as fallback only.
- •Push Notifications — Approve login via trusted device. Good UX but requires app installation.
- •Backup Codes — One-time use codes for recovery. Store securely, inform users.
Don't implement authentication from scratch. Use established libraries: Passport.js (Node), Django Auth (Python), Devise (Ruby), Spring Security (Java). For multi-provider auth, use Auth0, Firebase Auth, or Clerk.
Cross-Site Request Forgery (CSRF) Protection
CSRF attacks trick authenticated users into performing unintended actions.
The Attack:
<!-- Attacker's site: evil.com -->
<img src="https://bank.com/transfer?to=attacker&amount=10000" />
<!-- Or via form submission -->
<form action="https://bank.com/transfer" method="POST">
<input name="to" value="attacker" />
<input name="amount" value="10000" />
</form>
<script>document.forms[0].submit();</script>
If the user is logged into bank.com, their session cookie is automatically sent, and the transfer executes.
Defense 1: SameSite Cookies
Modern browsers support SameSite cookie attribute:
Set-Cookie: session=abc123; SameSite=Lax; Secure; HttpOnly
| SameSite Value | Behavior |
|---|---|
Strict | Cookie only sent for same-site requests (breaks external links) |
Lax | Cookie sent for navigations, not for cross-site POST/iframes |
None | Cookie sent always (requires Secure; old behavior) |
SameSite=Lax is the default in modern browsers and provides good CSRF protection.
Defense 2: CSRF Tokens
Include a secret, unpredictable token in forms that attackers can't guess:
<form action="/transfer" method="POST">
<input type="hidden" name="_csrf" value="abc123-random-token">
<input name="amount" value="100">
<button>Transfer</button>
</form>
// Server validation
if (req.body._csrf !== req.session.csrfToken) {
return res.status(403).send('Invalid CSRF token');
}
Defense 3: Origin/Referer Header Checking
Verify that requests come from your origin:
function validateOrigin(req) {
const origin = req.headers.origin || req.headers.referer;
const allowed = ['https://example.com', 'https://www.example.com'];
if (!origin) {
return false; // Be cautious with missing origin
}
return allowed.some(a => origin.startsWith(a));
}
Defense 4: Custom Headers for API Requests
Cross-origin requests can't set custom headers without CORS approval:
// Client: include custom header
fetch('/api/transfer', {
method: 'POST',
headers: { 'X-Requested-With': 'XMLHttpRequest' },
credentials: 'include',
});
// Server: require custom header
if (!req.headers['x-requested-with']) {
return res.status(403).send('Invalid request');
}
Use multiple CSRF defenses together: SameSite cookies (default protection), CSRF tokens (traditional forms), and custom headers (API requests). Each layer catches attacks the others might miss.
Input Validation and Secure Data Handling
Never trust user input—the fundamental security principle. All input is potentially malicious until validated.
Validation Principles:
Input Validation Strategy:
1. VALIDATE: Check input format, type, length, range
2. SANITIZE: Remove or escape dangerous characters
3. PARAMETERIZE: Use placeholders for database queries
4. ENCODE: Escape output for the target context (HTML, URL, JS)
Allowlisting vs Denylisting:
// WRONG: Trying to block bad input (denylisting)
if (input.includes('<script>')) {
reject();
}
// Attacker uses: <ScRiPt>, <script/>, <script\x00>, etc.
// RIGHT: Only allow known-good input (allowlisting)
const validName = /^[a-zA-Z ]{1,50}$/;
if (!validName.test(input)) {
reject();
}
SQL Injection Prevention:
// VULNERABLE: String concatenation
const query = `SELECT * FROM users WHERE id = ${userId}`;
// Attack: userId = "1; DROP TABLE users;--"
// SECURE: Parameterized queries
const query = 'SELECT * FROM users WHERE id = ?';
db.query(query, [userId]); // userId is treated as data, not code
// SECURE: ORM (Prisma example)
const user = await prisma.user.findUnique({
where: { id: userId },
});
Command Injection Prevention:
// VULNERABLE: Embedding user input in shell command
const cmd = `ping ${hostname}`;
exec(cmd); // Attack: hostname = "example.com; rm -rf /"
// SECURE: Use argument arrays
const { spawn } = require('child_process');
spawn('ping', ['-c', '1', hostname]); // hostname is one argument
// BETTER: Validate input strictly
const validHostname = /^[a-z0-9.-]+$/i;
if (!validHostname.test(hostname)) {
throw new Error('Invalid hostname');
}
- •Email — Validate format, consider verification email
- •URLs — Validate protocol (https only?), domain allowlist for redirects
- •File Uploads — Validate MIME type, extension, scan for malware, randomize filenames, store outside webroot
- •Numbers — Parse to number type, check range, handle non-numeric input
- •Dates — Parse to Date object, validate range and format
- •JSON — Parse safely (JSON.parse), validate schema (Zod, Joi)
- •HTML Content — Use DOMPurify or similar sanitization library
Client-side validation improves UX but provides zero security. Attackers bypass the browser entirely. All security validation MUST be done server-side, even if duplicated on the client.
KEEP LEARNING
There’s more to understand.
You’re reading the introduction. Create a free account to read the complete lesson, save notes and keep track of your progress.