127: Node/Express API Security — Validation, CORS, Cookies, Sessions, Headers, Rate Limits, and Abuse Resistance
Learning objectives
You will learn to:
- treat every network input as untrusted;
- validate and normalize inputs;
- understand CORS correctly;
- configure security headers;
- understand cookies and session security;
- distinguish authentication from authorization;
- understand rate limiting and abuse controls;
- defend against body-size, regex, query, and resource exhaustion;
- understand proxy/IP trust;
- prevent common injection and SSRF classes;
- manage secrets safely;
- create a practical Node API security checklist.
Security is layered
A secure API needs multiple controls:
network/TLS ↓ reverse proxy ↓ request limits ↓ authentication ↓ authorization ↓ validation ↓ business invariants ↓ database safe queries ↓ safe response ↓ logging/audit
No single middleware “secures Express.”
Input sources
Untrusted:
req.params req.query req.body req.headers cookies uploaded files JWT claims unless cryptographically verified webhook payloads upstream API responses database data originally supplied by users
Validate at trust boundaries.
Schema validation
Example with a schema library:
const CreateTaskSchema = z.object({
title: z
.string()
.trim()
.min(3)
.max(80),
priority: z
.enum(['low', 'normal', 'high'])
.default('normal'),
}).strict();
Strict/reject unknown fields where appropriate.
Then:
const result = CreateTaskSchema.safeParse(req.body);
if (!result.success) {
throw new ValidationError(...);
}
const input = result.data;
Do not pass raw body deeper after validation.
Use parsed value.
Type coercion
Query values arrive as strings/structures.
Be explicit:
"false" ≠ false "0" ≠ 0
Schema coercion can help, but test empty string, repeated params, arrays, and malicious objects.
Prototype pollution
Some object-merging/parsing packages historically suffered prototype-pollution issues.
Keep dependencies patched.
Do not deep-merge arbitrary request objects into configuration/domain objects.
Prefer explicit field selection.
NoSQL/operator injection preview
Dangerous pattern later with Mongo:
collection.find(req.query);
Attacker may supply operator-shaped query values.
Never use request object directly as database filter.
Build filter from validated allowed primitives.
SQL injection principle
Even if this module later uses Mongo, know the general rule:
Use parameterized queries/ORM builders.
Never concatenate untrusted strings into SQL.
Command injection
Danger:
exec(`convert ${req.body.filename} output.png`);
Shell metacharacters can execute commands.
Prefer execFile/spawn with argument arrays:
spawn('convert', [
validatedInputPath,
outputPath,
]);
Still validate paths/options.
Avoid invoking shell unless required.
Path traversal
Covered in filesystem lesson.
Never map request path directly to filesystem.
SSRF
Covered in HTTP lesson.
User-controlled outbound URL can access internal services/cloud metadata.
Use destination policy + egress controls.
CORS
CORS controls whether browser JavaScript can read/call cross-origin resources under browser policy.
It does not authenticate requests.
A curl script/server attacker ignores browser CORS.
Example:
import cors from 'cors';
app.use(
cors({
origin(origin, callback) {
if (!origin) {
return callback(null, true);
}
if (allowedOrigins.has(origin)) {
return callback(null, true);
}
callback(new Error('Origin not allowed'));
},
credentials: true,
}),
);
Do not reflect arbitrary Origin when credentials are enabled.
Preflight
Browser may send:
OPTIONS Access-Control-Request-Method Access-Control-Request-Headers
Framework CORS middleware can handle.
Do not confuse preflight success with authorization.
Security headers
Helmet is a common Express package:
npm install helmet
import helmet from 'helmet';
app.use(helmet());
It helps set security-related HTTP headers.
Still review policies:
- CSP;
- HSTS;
- frame protections;
- referrer policy;
- MIME sniffing.
Do not disable protections globally to fix one frontend issue.
CSP
Content Security Policy is especially relevant if Node serves browser pages.
JSON-only APIs still benefit from other headers but CSP primarily controls browser content execution.
If React is separately hosted, configure CSP at frontend delivery layer.
Cookies
Session cookie:
HttpOnly Secure SameSite Path Domain carefully Max-Age/Expires
HttpOnly reduces JavaScript access.
Secure requires HTTPS.
SameSite reduces some cross-site request risks.
Cookie name/value alone is not security.
Session IDs
A session cookie should contain an unpredictable session identifier, not raw sensitive session data unless using a carefully designed signed/encrypted format.
Server-side session store maps:
sessionId → user/session data
Session fixation prevention and rotation matter at login/privilege changes.
CSRF
Cookie-based authentication can be sent automatically by browser on requests.
Cross-Site Request Forgery can trick browser into submitting authenticated action.
Mitigations depend on architecture:
- SameSite cookies;
- CSRF tokens;
- Origin/Referer validation;
- custom headers + CORS;
- framework strategy.
Do not assume JSON automatically eliminates CSRF.
Bearer tokens
Authorization header:
Authorization: Bearer <token>
Bearer means whoever possesses token can use it.
Protect:
- transmission via TLS;
- storage;
- logs;
- rotation/revocation strategy.
Do not put access tokens in URLs where logs/history may leak them.
JWT misconception
JWT is a token format, not an authentication system by itself.
You still need:
- issuer;
- audience;
- signature verification;
- algorithm policy;
- expiration;
- key rotation;
- revocation/session policy;
- authorization checks.
Authentication lesson goes deeper.
Passwords
Never store plaintext passwords.
Use modern password-hashing functions designed for passwords, such as Argon2id/bcrypt/scrypt according to current security guidance and ecosystem support.
Node's crypto supports scrypt.
Third-party password packages must be maintained.
Use salts as designed by algorithm/library.
Do not invent hashing scheme:
SHA256(password)
alone.
Rate limiting
Rate limits control abuse/resource use.
Example categories:
login attempts password reset expensive search public API upload
A simple IP-only limit can hurt users behind NAT and is bypassable with distributed attackers.
Combine dimensions when appropriate:
- account;
- API key;
- IP;
- tenant;
- endpoint cost.
In multi-instance deployment, in-memory rate limiter must use shared store or edge control for consistent global limits.
429
Return:
429 Too Many Requests
and possibly Retry-After.
Do not reveal excessive security detail.
Request size
Configure:
express.json({
limit: '256kb',
});
Upload endpoints get separate limits.
Also consider reverse proxy limits.
Defense in depth.
Timeout
Slow clients/upstreams can consume resources.
Configure:
- server header/request timeouts;
- proxy timeouts;
- upstream fetch deadlines;
- database query limits.
Do not set arbitrary 2-second timeout for every operation. Understand expected workloads.
Regex DoS
Unsafe regex can have catastrophic backtracking on attacker-controlled input.
Prefer safe patterns, bounded lengths, and modern engines/features.
Do not run complex regex over megabytes of input.
JSON/body CPU
Even before business logic, parsing huge JSON costs memory/CPU.
Body limits are security and performance controls.
Pagination abuse
Reject/clamp:
limit=10000000
Enforce max filters/date ranges.
A query can be valid but operationally expensive.
Database timeouts
Use database driver timeout/maxTimeMS where appropriate.
Do not allow public endpoint to trigger unbounded full collection scan.
Indexes/data modeling are security availability concerns too.
Trust proxy and IP
Express:
app.set('trust proxy', ...)
If misconfigured, attacker can forge forwarded IP and bypass:
- secure cookie detection;
- rate limits;
- IP access rules.
Model actual proxy hops.
Logging security
Never log:
password Authorization bearer token session cookie credit-card full number private key full sensitive form body
Use redaction.
Logs are data stores.
Protect retention/access.
Error security
Unexpected:
{
"error": {
"code": "INTERNAL_ERROR",
"message": "The request could not be completed."
}
}
Log internal details privately.
Do not return Mongo connection strings, stack frames, filesystem paths.
Dependency security
- supported Node LTS;
- supported Express;
- lockfile;
- npm audit/advisories;
- dependency review;
- minimal packages;
- automated updates with tests.
Patch runtime too, not only npm packages.
TLS
Use HTTPS.
If TLS terminates at proxy, secure the proxy-to-app network according to threat model.
Do not accept plaintext secrets over public network.
Authorization is server-side
Client hides Delete button:
{canDelete && <button>Delete</button>}
good UX.
Server must still check:
authenticated user tenant role/permissions resource ownership resource status
for every operation.
Multi-tenant query
Never trust body:
{ "tenantId": "victim" }
to scope data.
Derive tenant from authenticated server context.
Query:
repository.findOne({
tenantId: auth.tenantId,
id: taskId,
});
Prevent IDOR/BOLA.
API security checklist
Before release:
[ ] supported Node/Express [ ] TLS [ ] body/upload limits [ ] schema validation [ ] unknown fields policy [ ] authentication [ ] per-resource authorization [ ] tenant scoping [ ] CORS policy [ ] CSRF policy [ ] secure cookies/token storage [ ] security headers [ ] rate limits [ ] upstream timeouts [ ] database timeouts [ ] query cost limits [ ] path/SSRF/command injection review [ ] secret redaction [ ] safe errors [ ] dependency/runtime patching [ ] audit logs for sensitive actions
Exercises
- Add strict schema validation to Task API.
- Demonstrate CORS does not block curl.
- Configure Helmet and inspect headers.
- Design cookie session attributes.
- Threat-model CSRF for cookie auth.
- Add shared-store rate-limit architecture design.
- Build safe outbound URL allowlist.
- Fix command-injection example with spawn args.
- Design tenant-scoped repository query.
- Write abuse test cases for body/page limits.
Mastery checklist
Explain:
- validation;
- CORS;
- CSP/headers;
- sessions/cookies;
- CSRF;
- bearer/JWT limitations;
- password hashing;
- rate limits;
- body/query limits;
- trust proxy;
- SSRF/path/command injection;
- authorization/tenant scoping;
- safe logging/errors.
