{"schemaVersion":"1.0","type":"Article","types":["Article"],"slug":"node-js-api-security-hardening-the-checklist-that-closes-the-gaps-tutorials-leave-open-clulv","url":"https://zyvop.com/node-js-api-security-hardening-the-checklist-that-closes-the-gaps-tutorials-leave-open-clulv","title":"Node.js API Security Hardening: The Checklist That Closes the Gaps Tutorials Leave Open","subtitle":"Helmet, CORS lockdown, parameter pollution, timing-safe comparisons, secrets that never touch your codebase, and a 30-point audit checklist","tldr":"A practical Node.js performance debugging guide covering CPU vs I/O bottlenecks, event loop lag analysis, flame graphs with Clinic.js and 0x, async wait tracing with Bubbleprof, production monitoring using perf_hooks, and fixes for common real-world regressions.","keywords":["SQL injection prevention","Node.js security checklist 2026","Express security headers Helmet","dependency vulnerabilities Node.js","timing attack Node.js","secrets management Node.js","CORS configuration Express","Node.js API security"],"entities":["ZyVOP","SQL injection prevention","Node.js security checklist 2026","Express security headers Helmet","dependency vulnerabilities Node.js","timing attack Node.js","secrets management Node.js","CORS configuration Express","Node.js API security"],"keyTakeaways":["Most Node.js security tutorials cover the obvious things — use HTTPS, hash passwords, validate input.","The gaps they leave are the ones attackers exploit.","This guide covers the security measures that go beyond the basics: HTTP security headers, dependency vulnerability management, secrets that never touch your codebase, the specific Express configurations that prevent common attacks, and the audit that tells you where your current API is exposed."],"headings":["1 — HTTP Security Headers with Helmet","2 — CORS Configuration","3 — Request Size Limits","4 — Parameter Pollution Prevention","5 — Secrets Management","6 — SQL Injection: Never Use String Interpolation","7 — Dependency Vulnerability Management","8 — Timing Attack Prevention","9 — Express Configuration Hardening","10 — The Security Audit Checklist","Running a Quick Security Scan"],"outboundLinks":[],"contentText":"Most Node.js security tutorials cover the obvious things — use HTTPS, hash passwords, validate input. The gaps they leave are the ones attackers exploit. This guide covers the security measures that go beyond the basics: HTTP security headers, dependency vulnerability management, secrets that never touch your codebase, the specific Express configurations that prevent common attacks, and the audit that tells you where your current API is exposed. 1 — HTTP Security Headers with Helmet One middleware, eleven attack vectors closed. Helmet sets HTTP response headers that tell browsers how to handle your content. npm install helmetimport helmet from 'helmet'; // Default helmet configuration is a strong starting point app.use(helmet()); // Or configure explicitly for your specific needs app.use(helmet({ contentSecurityPolicy: { directives: { defaultSrc: [\"'self'\"], scriptSrc: [\"'self'\"], // No inline scripts styleSrc: [\"'self'\", \"'unsafe-inline'\"], imgSrc: [\"'self'\", 'data:', 'https:'], connectSrc: [\"'self'\"], fontSrc: [\"'self'\"], objectSrc: [\"'none'\"], frameSrc: [\"'none'\"], upgradeInsecureRequests: [], }, }, crossOriginEmbedderPolicy: false, // Disable if embedding third-party content hsts: { maxAge: 31536000, // 1 year includeSubDomains: true, preload: true, }, referrerPolicy: { policy: 'strict-origin-when-cross-origin' }, frameguard: { action: 'deny' }, // No iframes — prevents clickjacking noSniff: true, // X-Content-Type-Options: nosniff xssFilter: true, // Legacy XSS protection header }));What each one prevents: contentSecurityPolicy — XSS by restricting where scripts can load from hsts — SSL stripping attacks frameguard — clickjacking noSniff — MIME-type confusion attacks referrerPolicy — referrer information leaking to external sites 2 — CORS Configuration Wildcard CORS (*) is almost never the right answer for an API with authenticated routes. Lock it down: import cors from 'cors'; const ALLOWED_ORIGINS = process.env.ALLOWED_ORIGINS?.split(',') || [ 'https://yourapp.com', 'https://www.yourapp.com', ]; // Add localhost only in development if (process.env.NODE_ENV === 'development') { ALLOWED_ORIGINS.push('http://localhost:3000', 'http://localhost:5173'); } app.use(cors({ origin: (origin, callback) =&gt; { // Allow requests with no origin (mobile apps, Postman, server-to-server) if (!origin) return callback(null, true); if (ALLOWED_ORIGINS.includes(origin)) { callback(null, true); } else { callback(new Error(`Origin ${origin} not allowed by CORS`)); } }, credentials: true, // Required for cookies (refresh tokens) methods: ['GET', 'POST', 'PUT', 'PATCH', 'DELETE', 'OPTIONS'], allowedHeaders: [ 'Content-Type', 'Authorization', 'X-Request-ID', ], maxAge: 86400, // Cache preflight for 24 hours }));3 — Request Size Limits Without size limits, a single request with a 1GB body can exhaust your server's memory. // Limit JSON body size app.use(express.json({ limit: '100kb' })); app.use(express.urlencoded({ extended: true, limit: '100kb' })); // Different limit for file upload endpoints app.use('/api/upload', express.raw({ limit: '10mb' })); // Limit URL length (default is unlimited in Express) app.use((req, res, next) =&gt; { if (req.url.length &gt; 2048) { return res.status(414).json({ error: 'URL too long' }); } next(); });4 — Parameter Pollution Prevention HTTP allows the same query parameter multiple times: ?sort=asc&amp;sort=desc. If your code does req.query.sort expecting a string and gets an array, it can cause unexpected behavior or crashes. npm install hppimport hpp from 'hpp'; // Prevents HTTP Parameter Pollution // Takes the last occurrence of duplicate params by default app.use(hpp({ whitelist: ['tags', 'categories'], // Allow arrays for these specific params }));5 — Secrets Management Every secret that appears in your codebase or .env committed to git is a potential breach. The right pattern depends on your environment. Never do: // Hard-coded — rotatable only by code change const API_KEY = 'sk_live_abc123'; // In git history even if deleted later // .env files committed to the repoFor VPS deployments: # On the server — set via SSH, not through CI echo \"JWT_PRIVATE_KEY=$(cat private.pem)\" &gt;&gt; /opt/app/.env.production chmod 600 /opt/app/.env.production chown deploy:deploy /opt/app/.env.productionFor Docker — use secrets, not environment variables: # docker-compose.yml secrets: jwt_private_key: file: ./secrets/jwt_private_key.pem services: app: secrets: - jwt_private_key environment: # Reference the file path, not the secret value JWT_PRIVATE_KEY_FILE: /run/secrets/jwt_private_key// Read from file if available, fall back to env function getSecret(name: string): string { const fileVar = process.env[`${name}_FILE`]; if (fileVar) { return fs.readFileSync(fileVar, 'utf-8').trim(); } const value = process.env[name]; if (!value) throw new Error(`Secret ${name} not configured`); return value; } const JWT_PRIVATE_KEY = getSecret('JWT_PRIVATE_KEY');Scan for accidental secret commits: # Add to your CI pipeline — runs before every merge npx secretlint \"**/*\" # Or use git-secrets pre-commit hook git secrets --install git secrets --register-aws6 — SQL Injection: Never Use String Interpolation Every SQL query with user input must use parameterized queries. No exceptions. // VULNERABLE — string interpolation in SQL const result = await db.query( `SELECT * FROM users WHERE email = '${email}'` ); // Input \"' OR '1'='1\" returns all users // SAFE — parameterized query const result = await db.query( 'SELECT * FROM users WHERE email = $1', [email] );With an ORM like Drizzle, parameterization is handled automatically. The risk is in raw SQL strings — audit every db.query() call that includes a variable. 7 — Dependency Vulnerability Management Outdated dependencies are one of the most common real-world attack vectors. Automate the scanning: # Run locally npm audit # Fix automatically where possible npm audit fix # Check for outdated packages npm outdatedAdd to CI — fail the build on high-severity vulnerabilities: # .github/workflows/ci.yml - name: Security audit run: npm audit --audit-level=highEnable Dependabot for automated PRs: # .github/dependabot.yml version: 2 updates: - package-ecosystem: npm directory: / schedule: interval: weekly open-pull-requests-limit: 10 ignore: - dependency-name: \"*\" update-types: [\"version-update:semver-major\"] # Major versions need manual review8 — Timing Attack Prevention String comparison using === is not constant-time. An attacker can measure response times to determine how many characters of a token or HMAC they got right. import { timingSafeEqual, createHmac } from 'crypto'; // BAD — vulnerable to timing attack function verifyToken(provided: string, expected: string): boolean { return provided === expected; } // GOOD — constant-time comparison function verifyToken(provided: string, expected: string): boolean { // Buffers must be same length for timingSafeEqual try { return timingSafeEqual( Buffer.from(provided), Buffer.from(expected) ); } catch { return false; // Different lengths — definitely not equal } }This matters for: webhook signature verification, API key comparison, HMAC verification, and password reset token comparison. 9 — Express Configuration Hardening Several Express defaults expose information attackers can use: // Remove X-Powered-By header — tells attackers you're using Express app.disable('x-powered-by'); // (Helmet does this too, but belt-and-suspenders) // Trust proxy — required if behind Nginx/load balancer // Without this, req.ip returns the proxy's IP, not the client's // Rate limiting by IP breaks without this app.set('trust proxy', 1); // Trust one proxy level // Disable etag for API responses — prevents cache-related issues app.set('etag', false); // Set secure cookie defaults app.use((req, res, next) =&gt; { res.cookie = new Proxy(res.cookie, { apply(target, thisArg, args) { const [name, value, options = {}] = args; if (process.env.NODE_ENV === 'production') { options.secure = options.secure ?? true; options.httpOnly = options.httpOnly ?? true; options.sameSite = options.sameSite ?? 'strict'; } return target.call(thisArg, name, value, options); } }); next(); });10 — The Security Audit Checklist Run through this before every major release: Authentication &amp; Authorization ✅ Passwords hashed with bcrypt (cost factor &gt;= 12) ✅ JWT tokens use RS256, not HS256 ✅ Access tokens expire in &lt;= 15 minutes ✅ Refresh tokens stored as hashes in DB ✅ Every authenticated route actually calls authenticate() ✅ Every tenant-scoped query includes tenant_id Input &amp; Output ✅ All user input validated with Zod before use ✅ All SQL queries use parameterized syntax — no string interpolation ✅ File uploads validate MIME type server-side ✅ Response never includes password_hash or internal fields Network &amp; Headers ✅ Helmet middleware installed and configured ✅ CORS locked to specific origins ✅ Request body size limits set ✅ HPP middleware installed ✅ HTTPS enforced (HSTS header set) Secrets &amp; Config ✅ No secrets in codebase or git history ✅ No secrets in environment variables in CI logs ✅ .env not committed to git ✅ npm audit passes at high severity level Dependencies ✅ Dependabot enabled ✅ No packages with known critical CVEs ✅ Node.js version is current LTS Infrastructure ✅ Database not exposed to internet (internal network only) ✅ Redis not exposed to internet ✅ SSH password authentication disabled ✅ UFW firewall configuredRunning a Quick Security Scan # Dependency vulnerabilities npm audit --audit-level=moderate # Check for exposed secrets in git history npx git-secrets --scan-history # Static analysis for common security issues npx eslint --plugin security src/ # Check HTTP headers in production curl -I https://yourapi.com/api/health | grep -E \"X-Frame|Content-Security|Strict-Transport|X-Content\"Security is not a feature you add at the end. Every item on this checklist is a decision made (or deferred) during implementation. The deferred ones accumulate silently until an incident makes them visible.","contentHash":"sha256:26cfa4554d2d2ef601d64e61cec9c85057ba8d7f4041d8726ad9c8bdfe076d61","authorName":"ZyVOP","authorUrl":"https://zyvop.com/author/zyvop","authorSameAs":["https://zyvop.com","https://github.com/zyvop","https://x.com/zyvop1"],"category":null,"tags":["SQL injection prevention","Node.js security checklist 2026","Express security headers Helmet","dependency vulnerabilities Node.js","timing attack Node.js","secrets management Node.js","CORS configuration Express","Node.js API security"],"audience":"Software engineers and developers building applications with SQL injection prevention","tone":"Practical and evidence-based engineering guidance","readingTimeMinutes":6,"wordCount":1304,"faqs":null,"primaryTopic":"SQL injection prevention","publishedAt":"2026-05-27T05:59:26.485Z","updatedAt":"2026-08-18T02:10:00.203Z","canonicalUrl":"https://zyvop.com/node-js-api-security-hardening-the-checklist-that-closes-the-gaps-tutorials-leave-open-clulv"}