Security — Overview & Defense in Depth
Web application security is the practice of protecting websites and APIs from attacks that could compromise data integrity, availability, or confidentiality. No single mechanism makes an application secure — security is achieved through layers of overlapping defenses.
This guide covers the foundational principles every developer needs: defense in depth, threat modeling, the CIA triad, attack surface reduction, and how to build security into your development lifecycle from day one.
Confidentiality, Integrity, and Availability form the foundation of information security. Every security decision maps back to protecting one or more of these properties.
| Property | Definition | Threats | Controls |
|---|---|---|---|
| Confidentiality | Data is accessible only to authorized users | XSS, data leakage, insecure storage | Encryption, access control, least privilege |
| Integrity | Data is accurate and unaltered | SQL injection, CSRF, tampering | Hashing, signatures, validation, CSRF tokens |
| Availability | Systems are accessible when needed | DDoS, resource exhaustion, deadlocks | Rate limiting, redundancy, autoscaling |
info
Defense in depth means layering multiple independent security controls so that if one fails, another catches the threat. No single layer is sufficient on its own — each adds a barrier that an attacker must overcome.
| 1 | Layer 1: Network |
| 2 | ├── Firewall rules (block unnecessary ports) |
| 3 | ├── DDoS protection (Cloudflare, AWS Shield) |
| 4 | ├── WAF (Web Application Firewall) |
| 5 | └── VPN / private networking for internal services |
| 6 | |
| 7 | Layer 2: Transport |
| 8 | ├── TLS 1.2+ everywhere (HSTS, certificate pinning) |
| 9 | ├── Mutual TLS for service-to-service |
| 10 | └── Certificate transparency monitoring |
| 11 | |
| 12 | Layer 3: Application |
| 13 | ├── Input validation (allowlists, Zod schemas) |
| 14 | ├── Output encoding (prevent XSS) |
| 15 | ├── Authentication & session management |
| 16 | ├── Authorization (RBAC / ABAC) |
| 17 | ├── CSRF protection (tokens, SameSite cookies) |
| 18 | └── Security headers (CSP, X-Frame-Options, etc.) |
| 19 | |
| 20 | Layer 4: Data |
| 21 | ├── Encryption at rest (AES-256-GCM) |
| 22 | ├── Encryption in transit (TLS) |
| 23 | ├── Database access controls (least privilege) |
| 24 | ├── Backup encryption |
| 25 | └── Key management (Vault, AWS KMS) |
| 26 | |
| 27 | Layer 5: Monitoring & Response |
| 28 | ├── Security event logging (SIEM) |
| 29 | ├── Intrusion detection (IDS) |
| 30 | ├── Alerting on anomalies |
| 31 | ├── Incident response plan |
| 32 | └── Forensics & audit trails |
best practice
Every user, process, and service should have only the minimum permissions necessary to perform its function. This limits the blast radius of any compromise.
| 1 | // Database — least privilege in practice |
| 2 | // BAD: application uses root/admin database user |
| 3 | const db = createConnection({ |
| 4 | host: 'db.example.com', |
| 5 | user: 'root', // Can do anything |
| 6 | password: DB_PASS, |
| 7 | }); |
| 8 | |
| 9 | // GOOD: application uses a scoped user |
| 10 | // GRANT SELECT, INSERT, UPDATE ON app_db.* TO 'app_user'@'%'; |
| 11 | // REVOKE DELETE, DROP, ALTER, CREATE ON app_db.* FROM 'app_user'@'%'; |
| 12 | const db = createConnection({ |
| 13 | host: 'db.example.com', |
| 14 | user: 'app_user', // Only necessary operations |
| 15 | password: DB_PASS, |
| 16 | database: 'app_db', |
| 17 | }); |
| 18 | |
| 19 | // Service accounts — each service gets its own credentials |
| 20 | const paymentService = createConnection({ |
| 21 | user: 'payment_svc', // Only payment tables |
| 22 | // Cannot access user data, logs, or admin tables |
| 23 | }); |
| 24 | |
| 25 | const analyticsService = createConnection({ |
| 26 | user: 'analytics_svc', // Read-only access to events |
| 27 | // Cannot write or modify any data |
| 28 | }); |
| 29 | |
| 30 | // IAM policy — AWS example (deny by default) |
| 31 | const policy = { |
| 32 | Version: '2012-10-17', |
| 33 | Statement: [ |
| 34 | { |
| 35 | Effect: 'Allow', |
| 36 | Action: ['s3:GetObject'], |
| 37 | Resource: 'arn:aws:s3:::my-bucket/public/*', |
| 38 | }, |
| 39 | // Everything else is implicitly denied |
| 40 | ], |
| 41 | }; |
Systems should be secure out of the box. Every default configuration should be the safest option — users and developers should have to explicitly opt into less secure behavior, not opt out of it.
| Component | Insecure Default | Secure Default |
|---|---|---|
| CORS | Allow all origins (*) | Deny all, explicit allowlist |
| Sessions | No SameSite, no Secure flag | SameSite=Lax, Secure, HttpOnly |
| Error handling | Full stack traces to client | Generic error message, log server-side |
| Admin panel | Accessible in production | Disabled or IP-restricted |
| Debug endpoints | Exposed in all environments | Gated behind NODE_ENV=development |
Threat modeling is the process of identifying what can go wrong before it does. STRIDE is a mnemonic that categorizes threats into six types, each with a corresponding security property.
| 1 | STRIDE Threat Model for a Web Application |
| 2 | ─────────────────────────────────────────── |
| 3 | |
| 4 | S — Spoofing (Authentication) |
| 5 | Threat: Attacker pretends to be another user |
| 6 | Example: Stolen session cookie, phishing login page |
| 7 | Control: Strong auth, MFA, session management, CSRF tokens |
| 8 | |
| 9 | T — Tampering (Integrity) |
| 10 | Threat: Data is modified in transit or at rest |
| 11 | Example: SQL injection, modifying form data in transit |
| 12 | Control: Input validation, HMAC signatures, parameterized queries |
| 13 | |
| 14 | R — Repudiation (Non-repudiation) |
| 15 | Threat: User denies performing an action |
| 16 | Example: "I never transferred that money" |
| 17 | Control: Audit logging, digital signatures, immutable logs |
| 18 | |
| 19 | I — Information Disclosure (Confidentiality) |
| 20 | Threat: Data exposed to unauthorized parties |
| 21 | Example: XSS stealing tokens, verbose error messages |
| 22 | Control: Encryption, CSP, error sanitization, least privilege |
| 23 | |
| 24 | D — Denial of Service (Availability) |
| 25 | Threat: System made unavailable to legitimate users |
| 26 | Example: DDoS, resource exhaustion, regex DoS (ReDoS) |
| 27 | Control: Rate limiting, WAF, autoscaling, input length limits |
| 28 | |
| 29 | E — Elevation of Privilege (Authorization) |
| 30 | Threat: User gains higher access than intended |
| 31 | Example: IDOR, admin functions accessible without auth check |
| 32 | Control: RBAC, authorization middleware, server-side enforcement |
info
Every feature, endpoint, and integration is a potential entry point. Reducing the attack surface means removing unnecessary components, closing unused ports, and minimizing the amount of code and configuration that could be exploited.
| 1 | // Attack surface reduction checklist |
| 2 | |
| 3 | // 1. Remove unused routes and endpoints |
| 4 | // BAD: debug endpoint left in production |
| 5 | app.get('/debug', (req, res) => { |
| 6 | res.json({ env: process.env, db: db.config }); // Exposed secrets! |
| 7 | }); |
| 8 | |
| 9 | // GOOD: gate behind environment check, or remove entirely |
| 10 | if (process.env.NODE_ENV === 'development') { |
| 11 | app.get('/debug', (req, res) => { |
| 12 | res.json({ status: 'ok' }); |
| 13 | }); |
| 14 | } |
| 15 | |
| 16 | // 2. Disable unnecessary HTTP methods |
| 17 | app.use((req, res, next) => { |
| 18 | const allowed = ['GET', 'POST', 'OPTIONS']; |
| 19 | if (!allowed.includes(req.method)) { |
| 20 | return res.status(405).json({ error: 'Method not allowed' }); |
| 21 | } |
| 22 | next(); |
| 23 | }); |
| 24 | |
| 25 | // 3. Remove server identification headers |
| 26 | app.disable('x-powered-by'); |
| 27 | app.disable('x-aspnet-version'); |
| 28 | app.disable('x-runtime'); |
| 29 | |
| 30 | // 4. Limit request body size |
| 31 | app.use(express.json({ limit: '100kb' })); |
| 32 | app.use(express.urlencoded({ limit: '100kb', extended: false })); |
| 33 | |
| 34 | // 5. Set timeouts on incoming requests |
| 35 | app.use((req, res, next) => { |
| 36 | req.setTimeout(30000); // 30 second timeout |
| 37 | res.setTimeout(30000); |
| 38 | next(); |
| 39 | }); |
| 40 | |
| 41 | // 6. Remove directory listing and file serving defaults |
| 42 | // Do not serve static files from the application root |
Security should be integrated into every phase of development — not bolted on at the end. A Secure SDLC (S-SDLC) embeds security activities into requirements, design, implementation, testing, and deployment.
| 1 | Secure Development Lifecycle Phases |
| 2 | ──────────────────────────────────── |
| 3 | |
| 4 | 1. Requirements |
| 5 | ├── Define security requirements (CIA for each feature) |
| 6 | ├── Identify compliance needs (GDPR, HIPAA, PCI-DSS) |
| 7 | └── Establish security acceptance criteria |
| 8 | |
| 9 | 2. Design |
| 10 | ├── Threat modeling (STRIDE on critical paths) |
| 11 | ├── Architecture security review |
| 12 | ├── Define trust boundaries and data flows |
| 13 | └── Select secure patterns and libraries |
| 14 | |
| 15 | 3. Implementation |
| 16 | ├── Secure coding guidelines (OWASP Cheat Sheets) |
| 17 | ├── Peer code review with security focus |
| 18 | ├── Static Application Security Testing (SAST) |
| 19 | └── Dependency vulnerability scanning |
| 20 | |
| 21 | 4. Testing |
| 22 | ├── Dynamic Application Security Testing (DAST) |
| 23 | ├── Penetration testing (manual + automated) |
| 24 | ├── Fuzz testing for input handling |
| 25 | └── Security regression tests |
| 26 | |
| 27 | 5. Deployment |
| 28 | ├── Infrastructure hardening (CIS benchmarks) |
| 29 | ├── Container image scanning |
| 30 | ├── Secrets management (no hardcoded credentials) |
| 31 | └── Security headers and CSP configuration |
| 32 | |
| 33 | 6. Operations |
| 34 | ├── Security event monitoring (SIEM) |
| 35 | ├── Vulnerability management and patching |
| 36 | ├── Incident response procedures |
| 37 | └── Regular security audits and re-assessment |
HTTP response headers are your first line of defense at the transport layer. They tell the browser how to behave — what to load, what to block, and what policies to enforce.
| 1 | // Helmet.js — sets 15+ security headers in one line |
| 2 | import helmet from 'helmet'; |
| 3 | |
| 4 | app.use(helmet()); |
| 5 | |
| 6 | // What Helmet sets (and why each matters): |
| 7 | |
| 8 | // Content-Security-Policy (CSP) |
| 9 | // Prevents XSS by whitelisting allowed sources for scripts, styles, etc. |
| 10 | // Default: script-src 'self' |
| 11 | |
| 12 | // Strict-Transport-Security (HSTS) |
| 13 | // Forces HTTPS for all future requests (including subdomains) |
| 14 | Strict-Transport-Security: max-age=63072000; includeSubDomains; preload |
| 15 | |
| 16 | // X-Content-Type-Options |
| 17 | // Prevents MIME-type sniffing (loading CSS as HTML, etc.) |
| 18 | X-Content-Type-Options: nosniff |
| 19 | |
| 20 | // X-Frame-Options |
| 21 | // Prevents clickjacking by blocking iframe embedding |
| 22 | X-Frame-Options: DENY |
| 23 | |
| 24 | // X-XSS-Protection |
| 25 | // Legacy XSS filter (modern browsers rely on CSP instead) |
| 26 | X-XSS-Protection: 0 |
| 27 | |
| 28 | // Referrer-Policy |
| 29 | // Controls how much referrer information is sent |
| 30 | Referrer-Policy: strict-origin-when-cross-origin |
| 31 | |
| 32 | // Permissions-Policy |
| 33 | // Controls browser features (camera, microphone, geolocation) |
| 34 | Permissions-Policy: camera=(), microphone=(), geolocation=() |
| 35 | |
| 36 | // Custom configuration with Helmet |
| 37 | app.use(helmet({ |
| 38 | contentSecurityPolicy: { |
| 39 | directives: { |
| 40 | defaultSrc: ["'self'"], |
| 41 | scriptSrc: ["'self'", "'strict-dynamic'"], |
| 42 | styleSrc: ["'self'", "'unsafe-inline'"], |
| 43 | imgSrc: ["'self'", 'data:', 'https:'], |
| 44 | connectSrc: ["'self'", 'https://api.example.com'], |
| 45 | frameSrc: ["'none'"], |
| 46 | objectSrc: ["'none'"], |
| 47 | upgradeInsecureRequests: [], |
| 48 | }, |
| 49 | }, |
| 50 | hsts: { |
| 51 | maxAge: 63072000, |
| 52 | includeSubDomains: true, |
| 53 | preload: true, |
| 54 | }, |
| 55 | referrerPolicy: { policy: 'strict-origin-when-cross-origin' }, |
| 56 | })); |
warning
The two most fundamental security practices: validate all input on the server (never trust the client), and encode all output before rendering (never embed raw data in HTML, JS, or SQL).
| 1 | // Validate input with Zod (server-side only) |
| 2 | import { z } from 'zod'; |
| 3 | |
| 4 | const CreateUserSchema = z.object({ |
| 5 | name: z.string().min(1).max(100).regex(/^[a-zA-Z\s'-]+$/), |
| 6 | email: z.string().email().max(255), |
| 7 | age: z.number().int().min(13).max(150), |
| 8 | role: z.enum(['user', 'editor', 'admin']).default('user'), |
| 9 | }); |
| 10 | |
| 11 | // Always parse on the server — never trust client validation alone |
| 12 | app.post('/api/users', (req, res) => { |
| 13 | const result = CreateUserSchema.safeParse(req.body); |
| 14 | if (!result.success) { |
| 15 | return res.status(400).json({ |
| 16 | error: 'Validation failed', |
| 17 | details: result.error.issues, |
| 18 | }); |
| 19 | } |
| 20 | // result.data is fully validated and typed |
| 21 | createUser(result.data); |
| 22 | }); |
| 23 | |
| 24 | // Output encoding — context-dependent |
| 25 | // HTML context: escape <, >, &, ", ' |
| 26 | import escapeHtml from 'escape-html'; |
| 27 | const safe = escapeHtml(userInput); // <script> → <script> |
| 28 | |
| 29 | // JavaScript context: JSON.stringify or template literal escaping |
| 30 | const safe = JSON.stringify(userInput); // Safe for embedding in JS |
| 31 | |
| 32 | // URL context: encode special characters |
| 33 | const safe = encodeURIComponent(userInput); |
| 34 | |
| 35 | // SQL context: ALWAYS use parameterized queries |
| 36 | await db.query('SELECT * FROM users WHERE name = $1', [userInput]); |
Error messages can leak sensitive information — stack traces reveal internal paths, database errors reveal schema, and version numbers reveal known vulnerabilities. Always sanitize errors before sending them to clients.
| 1 | // Secure error handling middleware (Express) |
| 2 | class AppError extends Error { |
| 3 | constructor(public statusCode: number, message: string) { |
| 4 | super(message); |
| 5 | } |
| 6 | } |
| 7 | |
| 8 | // Global error handler — last middleware in the chain |
| 9 | app.use((err: Error, req, res, next) => { |
| 10 | // Log full error server-side (for debugging and SIEM) |
| 11 | console.error('[ERROR]', { |
| 12 | timestamp: new Date().toISOString(), |
| 13 | method: req.method, |
| 14 | path: req.path, |
| 15 | error: err.message, |
| 16 | stack: err.stack, |
| 17 | userId: req.session?.userId, |
| 18 | ip: req.ip, |
| 19 | }); |
| 20 | |
| 21 | // Determine if this is a known/expected error |
| 22 | if (err instanceof AppError) { |
| 23 | return res.status(err.statusCode).json({ |
| 24 | error: err.message, |
| 25 | }); |
| 26 | } |
| 27 | |
| 28 | // Unknown errors — generic message, no details |
| 29 | res.status(500).json({ |
| 30 | error: 'Internal server error', |
| 31 | // Never include: stack, database errors, file paths, version numbers |
| 32 | }); |
| 33 | }); |
| 34 | |
| 35 | // Database errors — sanitize before leaking |
| 36 | app.post('/api/users', async (req, res) => { |
| 37 | try { |
| 38 | await createUser(req.body); |
| 39 | res.json({ success: true }); |
| 40 | } catch (err) { |
| 41 | // Database unique constraint violation |
| 42 | if (err.code === '23505') { |
| 43 | return res.status(409).json({ error: 'Email already exists' }); |
| 44 | } |
| 45 | // All other DB errors — generic message |
| 46 | console.error('[DB]', err); |
| 47 | res.status(500).json({ error: 'Internal server error' }); |
| 48 | } |
| 49 | }); |
| 50 | |
| 51 | // Custom error pages |
| 52 | app.get('/404', (req, res) => { |
| 53 | res.status(404).json({ error: 'Not found' }); |
| 54 | }); |
danger
| Category | Item | Priority |
|---|---|---|
| Transport | HTTPS everywhere, HSTS with preload | Critical |
| Auth | Rate limiting on login, MFA for admins | Critical |
| Input | Server-side validation on all endpoints | Critical |
| Sessions | HttpOnly, Secure, SameSite cookies | Critical |
| Headers | CSP, X-Frame-Options, X-Content-Type-Options | High |
| CSRF | CSRF tokens on all state-changing requests | High |
| Dependencies | npm audit in CI, Dependabot enabled | High |
| Secrets | No hardcoded secrets, use env vars / vault | High |
| Errors | Generic error messages to clients | High |
| Logging | Security events logged, no secrets in logs | Medium |
| Monitoring | Alerting on 5xx spikes and auth failures | Medium |
- Never trust the client — validate and sanitize all input on the server.
- Use deny-by-default for CORS, access control, and feature flags.
- Encrypt sensitive data in transit (TLS) and at rest (AES-GCM).
- Apply least privilege at every level: database users, API keys, IAM roles.
- Automate security scanning in CI/CD: SAST, DAST, dependency audit, secret scanning.
- Log security-relevant events and centralize logs for investigation.
- Keep dependencies updated and audit for known CVEs regularly.
- Use managed services for auth, secrets, and encryption when possible — don't roll your own.
- Conduct threat modeling before implementation, not after a breach.
- Train every developer on OWASP Top 10 and secure coding at least annually.
best practice
Community
Get help on Slack, Discord or VIP
Stuck on a guide? Join the community and ask.