Loading content...
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 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:
/admin/users → accessible by non-admins/api/order/123 → changing to /api/order/456 (other user's order)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:
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.
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 (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) 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()>
< > & ' "textContent instead of innerHTML for text. Use setAttribute() for attributes. Modern frameworks auto-escape by default.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.
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:
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
},
}));
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.
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.
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');
}
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.
Defense in depth is the principle that security shouldn't rely on any single control. Multiple layers of protection ensure that when one fails, others contain the damage.
The Castle Analogy:
┌─────────────────────────────────────────────┐
│ Network Perimeter │ ← Firewall, WAF, DDoS protection
│ ┌───────────────────────────────────────┐ │
│ │ Load Balancer/Proxy │ │ ← Rate limiting, bot protection
│ │ ┌─────────────────────────────────┐ │ │
│ │ │ Application Layer │ │ │ ← Input validation, auth, CSP
│ │ │ ┌───────────────────────────┐ │ │ │
│ │ │ │ Business Logic │ │ │ │ ← Authorization, access control
│ │ │ │ ┌─────────────────────┐ │ │ │ │
│ │ │ │ │ Data Layer │ │ │ │ │ ← Encryption, parameterization
│ │ │ │ │ ┏━━━━━━━━━━━━━━━┓ │ │ │ │ │
│ │ │ │ │ ┃ Sensitive ┃ │ │ │ │ │ ← The crown jewels
│ │ │ │ │ ┃ Data ┃ │ │ │ │ │
│ │ │ │ │ ┗━━━━━━━━━━━━━━━┛ │ │ │ │ │
Layer 1: Network/Infrastructure
Layer 2: Transport
Layer 3: Application
Layer 4: Data
Prevention is essential, but detection and response are equally important. You can't prevent every attack—you need to know when they happen and respond effectively.
What to Log (Security Events):
✓ Authentication Events:
- Successful logins (with IP, user agent)
- Failed logins (with IP, attempted username)
- Password changes
- MFA enrollments and challenges
- Session creation and destruction
✓ Authorization Events:
- Access denied events
- Privilege escalation (user becoming admin)
- Sensitive resource access (download reports, view PII)
✓ Input Validation Failures:
- Rejected requests (invalid format, suspicious patterns)
- CSP violations
- Rate limit triggers
✓ Application Errors:
- Exceptions and stack traces (don't expose to users!)
- Database connection failures
- External service errors
Secure Logging Practices:
// WRONG: Logging sensitive data
logger.info(`User login: ${email} with password ${password}`);
// RIGHT: Log only necessary information
logger.info('User login', {
email,
success: true,
ip: req.ip,
userAgent: req.headers['user-agent'],
timestamp: new Date().toISOString()
});
// WRONG: Exposing stack traces to users
res.status(500).json({ error: error.stack });
// RIGHT: Generic user message, detailed internal log
logger.error('Payment processing failed', { error, orderId });
res.status(500).json({ error: 'Payment failed. Please try again.' });
Alerting Thresholds (Examples):
| Event | Threshold | Alert |
|---|---|---|
| Failed logins for account | >5 in 5 min | Account lockout + email user |
| Failed logins from IP | >20 in 1 min | Block IP temporarily |
| CSP violations | Any from new source | Review for XSS |
| 5xx errors | >1% of requests | Page oncall |
| Auth service latency | >1s | Warning, investigate |
| Unusual data access | Admin exports all users | Immediate alert |
For larger organizations, aggregate logs into a SIEM (Splunk, Elastic Security, Microsoft Sentinel). This enables correlation across systems, automated threat detection, and incident investigation. Even for smaller teams, centralized logging (CloudWatch, Datadog) provides essential visibility.
Incident Response Basics:
1. DETECT & IDENTIFY
- How was the incident discovered?
- What is the scope? Which systems/data affected?
- Is the attack ongoing?
2. CONTAIN
- Isolate affected systems
- Revoke compromised credentials
- Block attack source if identified
- Preserve evidence (logs, memory dumps)
3. ERADICATE
- Remove attacker access
- Patch exploited vulnerability
- Change all potentially compromised secrets
4. RECOVER
- Restore from clean backups if needed
- Gradually restore services
- Monitor closely for re-compromise
5. LESSONS LEARNED
- Timeline analysis: what happened when?
- Root cause: how did attacker get in?
- Improvements: what will we fix?
- Document and share knowledge
We've comprehensively explored web security beyond HTTPS—the practices and principles that protect modern web applications. Let's consolidate the essential concepts:
The Security Mindset:
Security is not a feature you add at the end—it's a quality you build throughout development:
The organizations with the best security aren't necessarily those with the largest budgets—they're those where every engineer thinks about security every day.
Congratulations! You've completed the HTTPS and Web Security module. You now understand: TLS integration, certificate validation, mixed content, HSTS, and comprehensive web security best practices. Apply these principles to build applications that protect users and data in an adversarial world.