Security Hardening
Security hardening is the process of configuring systems and applications to reduce the attack surface. It involves implementing security headers, input validation, rate limiting, dependency auditing, and following OWASP guidelines.
Security is not a one-time task — it is an ongoing practice of monitoring, patching, and improving. This guide covers the most impactful security measures you can implement for a web application.
The OWASP Top 10 represents the most critical security risks to web applications. Understanding these is the foundation of application security.
| Rank | Risk | Description |
|---|---|---|
| A01 | Broken Access Control | Users can access resources beyond their permissions |
| A02 | Cryptographic Failures | Weak encryption, exposed sensitive data |
| A03 | Injection | SQL, NoSQL, OS command injection attacks |
| A04 | Insecure Design | Architecture-level security gaps |
| A05 | Security Misconfiguration | Default credentials, unpatched systems |
| A06 | Vulnerable & Outdated Components | Known CVEs in dependencies |
| A07 | Identification & Auth Failures | Weak passwords, session management flaws |
| A08 | Software & Data Integrity Failures | Supply chain attacks, unsigned updates |
| A09 | Security Logging & Monitoring | Insufficient detection of breaches |
| A10 | Server-Side Request Forgery (SSRF) | Server makes requests to internal resources |
best practice
CSP is a browser security mechanism that mitigates XSS (cross-site scripting) and data injection attacks by specifying which sources of content are allowed to load.
| 1 | // CSP Headers — Next.js middleware or next.config.js |
| 2 | // next.config.js |
| 3 | const cspHeader = ` |
| 4 | default-src 'self'; |
| 5 | script-src 'self' 'unsafe-eval' 'unsafe-inline' https://www.googletagmanager.com; |
| 6 | style-src 'self' 'unsafe-inline' https://fonts.googleapis.com; |
| 7 | img-src 'self' blob: data: https://*.example.com https://www.google-analytics.com; |
| 8 | font-src 'self' https://fonts.gstatic.com; |
| 9 | connect-src 'self' https://api.example.com https://*.sentry.io wss://*.pusher.com; |
| 10 | frame-src 'self' https://www.youtube.com; |
| 11 | object-src 'none'; |
| 12 | base-uri 'self'; |
| 13 | form-action 'self'; |
| 14 | frame-ancestors 'none'; |
| 15 | block-all-mixed-content; |
| 16 | upgrade-insecure-requests; |
| 17 | `; |
| 18 | |
| 19 | module.exports = { |
| 20 | async headers() { |
| 21 | return [ |
| 22 | { |
| 23 | source: '/(.*)', |
| 24 | headers: [ |
| 25 | { |
| 26 | key: 'Content-Security-Policy', |
| 27 | value: cspHeader.replace(/\s{2,}/g, ' ').trim(), |
| 28 | }, |
| 29 | ], |
| 30 | }, |
| 31 | ]; |
| 32 | }, |
| 33 | }; |
| 34 | |
| 35 | // Strict CSP for static pages |
| 36 | const strictCsp = { |
| 37 | 'default-src': "'self'", |
| 38 | 'script-src': "'self'", |
| 39 | 'style-src': "'self' 'unsafe-inline'", |
| 40 | 'img-src': "'self' data:", |
| 41 | 'font-src': "'self'", |
| 42 | 'connect-src': "'self'", |
| 43 | 'frame-ancestors': "'none'", |
| 44 | 'base-uri': "'self'", |
| 45 | 'form-action': "'self'", |
| 46 | }; |
| 47 | |
| 48 | // Reporting-only mode (monitor before enforcing) |
| 49 | // Content-Security-Policy-Report-Only: |
| 50 | // This logs violations without blocking them |
warning
Rate limiting prevents brute-force attacks, credential stuffing, and API abuse by restricting the number of requests a client can make in a given time window.
| 1 | // Next.js API route rate limiting with Upstash |
| 2 | import { Ratelimit } from '@upstash/ratelimit'; |
| 3 | import { Redis } from '@upstash/redis'; |
| 4 | |
| 5 | const ratelimit = new Ratelimit({ |
| 6 | redis: Redis.fromEnv(), |
| 7 | limiter: Ratelimit.slidingWindow(10, '10 s'), // 10 requests per 10 seconds |
| 8 | analytics: true, |
| 9 | }); |
| 10 | |
| 11 | export default async function handler(req, res) { |
| 12 | const ip = req.headers['x-forwarded-for'] || req.socket.remoteAddress; |
| 13 | const { success, limit, remaining, reset } = await ratelimit.limit(ip); |
| 14 | |
| 15 | // Set rate limit headers |
| 16 | res.setHeader('X-RateLimit-Limit', limit); |
| 17 | res.setHeader('X-RateLimit-Remaining', remaining); |
| 18 | res.setHeader('X-RateLimit-Reset', new Date(reset).toISOString()); |
| 19 | |
| 20 | if (!success) { |
| 21 | return res.status(429).json({ |
| 22 | error: 'Too many requests', |
| 23 | retryAfter: Math.ceil((reset - Date.now()) / 1000), |
| 24 | }); |
| 25 | } |
| 26 | |
| 27 | // Process the request |
| 28 | res.status(200).json({ message: 'Success' }); |
| 29 | } |
| 30 | |
| 31 | // Middleware-based rate limiting (for all routes) |
| 32 | import { Ratelimit } from '@upstash/ratelimit'; |
| 33 | import { Redis } from '@upstash/redis'; |
| 34 | import type { NextRequest } from 'next/server'; |
| 35 | |
| 36 | const ratelimit = new Ratelimit({ |
| 37 | redis: Redis.fromEnv(), |
| 38 | limiter: Ratelimit.slidingWindow(100, '60 s'), // 100 requests per minute |
| 39 | }); |
| 40 | |
| 41 | export async function middleware(request: NextRequest) { |
| 42 | const ip = request.headers.get('x-forwarded-for') || 'unknown'; |
| 43 | const { success } = await ratelimit.limit(ip); |
| 44 | |
| 45 | if (!success) { |
| 46 | return new Response('Too Many Requests', { status: 429 }); |
| 47 | } |
| 48 | |
| 49 | return NextResponse.next(); |
| 50 | } |
| 51 | |
| 52 | // Rate limit tiers: |
| 53 | // API: 10 req/10s (authenticated), 5 req/10s (unauthenticated) |
| 54 | // Auth endpoints: 5 req/min (login attempts) |
| 55 | // Public pages: 100 req/min per IP |
info
Vulnerable dependencies are a leading cause of security breaches. Regular auditing with npm audit, Snyk, or GitHub Dependabot helps identify and fix known vulnerabilities.
| 1 | # npm audit — Check for vulnerabilities |
| 2 | npm audit |
| 3 | |
| 4 | # Detailed audit with JSON output |
| 5 | npm audit --json |
| 6 | |
| 7 | # Fix vulnerabilities automatically (when possible) |
| 8 | npm audit fix |
| 9 | |
| 10 | # Install only with force (bypass audit — AVOID) |
| 11 | # npm install --audit=false |
| 12 | |
| 13 | # GitHub Dependabot configuration (.github/dependabot.yml) |
| 14 | version: 2 |
| 15 | updates: |
| 16 | - package-ecosystem: "npm" |
| 17 | directory: "/" |
| 18 | schedule: |
| 19 | interval: "weekly" |
| 20 | day: "monday" |
| 21 | time: "09:00" |
| 22 | timezone: "America/New_York" |
| 23 | open-pull-requests-limit: 10 |
| 24 | labels: |
| 25 | - "dependencies" |
| 26 | - "automerge" |
| 27 | reviewers: |
| 28 | - "team-lead" |
| 29 | ignore: |
| 30 | - dependency-name: "eslint" |
| 31 | versions: ["9.x"] |
| 32 | |
| 33 | - package-ecosystem: "docker" |
| 34 | directory: "/" |
| 35 | schedule: |
| 36 | interval: "weekly" |
| 37 | |
| 38 | - package-ecosystem: "github-actions" |
| 39 | directory: "/" |
| 40 | schedule: |
| 41 | interval: "weekly" |
| 42 | |
| 43 | # Snyk CLI — More comprehensive than npm audit |
| 44 | npm install -g snyk |
| 45 | snyk auth |
| 46 | snyk test |
| 47 | snyk monitor # Continuous monitoring |
| 48 | snyk code test # SAST (Static Application Security Testing) |
| 49 | |
| 50 | # Trivy — Container image scanning |
| 51 | trivy image my-app:latest |
danger
Helmet.js is a collection of Express middleware that sets security-related HTTP headers. CORS (Cross-Origin Resource Sharing) controls which domains can access your API.
| 1 | // Express/Node.js — Helmet.js security headers |
| 2 | import helmet from 'helmet'; |
| 3 | import cors from 'cors'; |
| 4 | import express from 'express'; |
| 5 | |
| 6 | const app = express(); |
| 7 | |
| 8 | // Apply all Helmet middleware defaults |
| 9 | app.use(helmet()); |
| 10 | |
| 11 | // Or customize individual directives |
| 12 | app.use( |
| 13 | helmet({ |
| 14 | contentSecurityPolicy: false, // Set CSP separately (see previous section) |
| 15 | crossOriginEmbedderPolicy: false, |
| 16 | crossOriginResourcePolicy: { policy: 'cross-origin' }, |
| 17 | hsts: { |
| 18 | maxAge: 31536000, |
| 19 | includeSubDomains: true, |
| 20 | preload: true, |
| 21 | }, |
| 22 | referrerPolicy: { policy: 'strict-origin-when-cross-origin' }, |
| 23 | frameguard: { action: 'deny' }, |
| 24 | }) |
| 25 | ); |
| 26 | |
| 27 | // CORS configuration |
| 28 | const corsOptions = { |
| 29 | origin: process.env.NODE_ENV === 'production' |
| 30 | ? ['https://example.com', 'https://www.example.com'] |
| 31 | : '*', |
| 32 | methods: ['GET', 'POST', 'PUT', 'PATCH', 'DELETE'], |
| 33 | allowedHeaders: ['Content-Type', 'Authorization', 'X-CSRF-Token'], |
| 34 | exposedHeaders: ['X-RateLimit-Remaining'], |
| 35 | credentials: true, |
| 36 | maxAge: 86400, // Preflight cache 24h |
| 37 | }; |
| 38 | |
| 39 | app.use(cors(corsOptions)); |
| 40 | |
| 41 | // In Next.js API routes, set CORS headers manually: |
| 42 | export default function handler(req, res) { |
| 43 | res.setHeader('Access-Control-Allow-Origin', 'https://example.com'); |
| 44 | res.setHeader('Access-Control-Allow-Methods', 'GET, POST, PUT'); |
| 45 | res.setHeader('Access-Control-Allow-Headers', 'Content-Type, Authorization'); |
| 46 | |
| 47 | if (req.method === 'OPTIONS') { |
| 48 | res.status(200).end(); // Handle preflight |
| 49 | return; |
| 50 | } |
| 51 | |
| 52 | // ... handle the request |
| 53 | } |
| 54 | |
| 55 | // Security headers set by Helmet (default): |
| 56 | // Content-Security-Policy (can be customized) |
| 57 | // Cross-Origin-Embedder-Policy |
| 58 | // Cross-Origin-Opener-Policy |
| 59 | // Cross-Origin-Resource-Policy |
| 60 | // Expect-CT |
| 61 | // Origin-Agent-Cluster |
| 62 | // Referrer-Policy |
| 63 | // Strict-Transport-Security (HSTS) |
| 64 | // X-Content-Type-Options |
| 65 | // X-DNS-Prefetch-Control |
| 66 | // X-Download-Options |
| 67 | // X-Frame-Options (anti-clickjacking) |
| 68 | // X-Permitted-Cross-Domain-Policies |
| 69 | // X-Powered-By (removed) |
| 70 | // X-XSS-Protection |
Never trust user input. All input must be validated (type, format, length) and sanitized (remove malicious content) before processing or storage.
| 1 | // Input validation with Zod |
| 2 | import { z } from 'zod'; |
| 3 | |
| 4 | // API request validation schema |
| 5 | const CreateUserSchema = z.object({ |
| 6 | email: z.string().email('Invalid email format'), |
| 7 | name: z.string().min(2, 'Name too short').max(100, 'Name too long'), |
| 8 | age: z.number().int().positive().optional(), |
| 9 | role: z.enum(['user', 'admin', 'moderator']).default('user'), |
| 10 | }); |
| 11 | |
| 12 | // Sanitize HTML content |
| 13 | import DOMPurify from 'isomorphic-dompurify'; |
| 14 | |
| 15 | function sanitizeHtml(input: string): string { |
| 16 | return DOMPurify.sanitize(input, { |
| 17 | ALLOWED_TAGS: ['b', 'i', 'em', 'strong', 'a', 'code', 'pre'], |
| 18 | ALLOWED_ATTR: ['href', 'target', 'rel'], |
| 19 | }); |
| 20 | } |
| 21 | |
| 22 | // SQL injection prevention — Always use parameterized queries |
| 23 | // BAD (vulnerable): |
| 24 | const query = `SELECT * FROM users WHERE id = '${req.query.id}'`; |
| 25 | |
| 26 | // GOOD (safe): |
| 27 | const result = await db.query('SELECT * FROM users WHERE id = $1', [req.query.id]); |
| 28 | |
| 29 | // NoSQL injection prevention (MongoDB) |
| 30 | // BAD: |
| 31 | const user = await User.find({ username: req.body.username, password: req.body.password }); |
| 32 | |
| 33 | // GOOD (sanitize and validate first, then query with typed schema): |
| 34 | const credentials = LoginSchema.parse(req.body); |
| 35 | const user = await User.findOne({ |
| 36 | username: credentials.username, |
| 37 | password: hash(credentials.password), |
| 38 | }); |
| 39 | |
| 40 | // XSS prevention — Always encode output |
| 41 | // React does this automatically with JSX |
| 42 | // dangerouslySetInnerHTML should be an absolute last resort |
best practice
HTTPS encrypts all data between the browser and the server. HSTS (HTTP Strict Transport Security) tells browsers to always use HTTPS, preventing downgrade attacks.
| 1 | // Next.js — Force HTTPS (in next.config.js) |
| 2 | module.exports = { |
| 3 | async headers() { |
| 4 | return [ |
| 5 | { |
| 6 | source: '/(.*)', |
| 7 | headers: [ |
| 8 | { |
| 9 | key: 'Strict-Transport-Security', |
| 10 | value: 'max-age=63072000; includeSubDomains; preload', |
| 11 | }, |
| 12 | ], |
| 13 | }, |
| 14 | ]; |
| 15 | }, |
| 16 | }; |
| 17 | |
| 18 | // Nginx — Redirect HTTP to HTTPS |
| 19 | server { |
| 20 | listen 80; |
| 21 | server_name example.com www.example.com; |
| 22 | return 301 https://$server_name$request_uri; |
| 23 | } |
| 24 | |
| 25 | // Add your site to HSTS preload list: |
| 26 | // https://hstspreload.org/ |
| 27 | // Requirements: |
| 28 | // - Valid HTTPS certificate |
| 29 | // - Redirect all HTTP to HTTPS |
| 30 | // - HSTS header with max-age >= 31536000 and includeSubDomains |
| 31 | // - HSTS header on the main domain |
| 32 | |
| 33 | // Check HTTPS configuration: |
| 34 | // https://www.ssllabs.com/ssltest/ |
| 35 | // https://securityheaders.com/ |