
Typescript Security
- 4 installs
- 21 repo stars
- Updated July 31, 2026
- jim60105/copilot-prompt
Design, review, and harden TypeScript/JavaScript apps (Node, Deno, Bun, browser) against the OWASP Top 10, including prototype pollution and XSS.
About
A structured guide for secure TypeScript and JavaScript development across threat modeling, secure coding, and verification against the OWASP Top 10 on both server and client. A developer uses it to audit or harden TS/JS code and set up security tooling.
- Covers server (Node/Deno/Bun) and browser trust boundaries
- Prototype pollution, XSS, SSRF prevention with ESLint-security/Semgrep/Snyk
Typescript Security by the numbers
- 4 all-time installs (skills.sh)
- Ranked #1,741 of 2,203 Security skills by installs in the Skillselion catalog
- Data as of Aug 2, 2026 (Skillselion catalog sync)
npx skills add https://github.com/jim60105/copilot-prompt --skill typescript-securityAdd your badge
Show developers this skill is listed on Skillselion. Paste this into your README.
| Installs | 4 |
|---|---|
| repo stars | ★ 21 |
| Last updated | July 31, 2026 |
| Repository | jim60105/copilot-prompt ↗ |
What it does
Design, review, and harden TypeScript/JavaScript apps (Node, Deno, Bun, browser) against the OWASP Top 10, including prototype pollution and XSS.
Files
TypeScript / JavaScript Security Development Guide
Provide a structured approach to building secure TypeScript and JavaScript applications, covering the OWASP Top 10, secure coding patterns, and verification checklists. Apply these guidelines throughout the secure development lifecycle — from threat modeling through deployment. This guide covers both server-side (Node.js, Deno, Bun) and client-side (browser) contexts.
Secure Development Lifecycle
Phase 1: Threat Modeling and Secure Design
Before writing code, identify and mitigate threats at the design level:
- Identify trust boundaries — Map where untrusted data enters the system (HTTP requests, WebSocket messages, file uploads, database reads, environment variables, third-party APIs,
postMessage, URL parameters, localStorage) - Map data flows — Trace sensitive data (credentials, PII, tokens) through the system and verify protection at each stage
- Enumerate entry points — List all routes, endpoints, CLI arguments, message queue consumers, WebSocket handlers, and scheduled tasks
- Map attack surfaces to OWASP Top 10 — Cross-reference each entry point against the OWASP categories in the quick reference table below
Design with security controls built-in:
- Centralized authentication and authorization middleware — never scatter auth checks across handlers
- Input validation at every trust boundary — validate early, reject invalid data before processing
- Least-privilege database access — use read-only connections where writes are not needed
- Defense in depth — layer multiple controls (input validation + parameterized queries + WAF)
- Fail securely — deny by default, require explicit grants
- Server-side enforcement — never rely solely on client-side validation or access controls
Phase 2: Secure Implementation
Critical Prohibitions
Never use these patterns. Violations are high-severity findings in any review.
| Never | Instead |
|---|---|
eval() / Function() constructor with untrusted input | JSON.parse() or a dedicated parser |
child_process.exec() with user input | child_process.execFile() or spawn() with array args |
| String concatenation / template literals in SQL | Parameterized queries (db.query(sql, params)) |
innerHTML / outerHTML / document.write() with untrusted data | textContent, framework templating, or DOMPurify |
dangerouslySetInnerHTML with unsanitized data | DOMPurify + explicit sanitization |
Math.random() for security purposes | crypto.randomUUID() / crypto.getRandomValues() |
| MD5 / SHA1 for password hashing | bcrypt, argon2, or scrypt via crypto.scrypt() |
== for security comparisons | === strict equality |
Object.assign() / spread with untrusted input on prototypes | Validated schema (Zod, class-validator) + Object.create(null) |
require() / import() with user-controlled paths | Static imports with allowlisted modules |
| Hardcoded secrets in source code | Environment variables or secret manager (Vault, AWS SM) |
NODE_ENV !== 'production' left in production | Environment-specific configuration |
JSON.parse() without schema validation on untrusted data | Zod, io-ts, or class-validator after parsing |
new RegExp(userInput) | Escape user input or use a safe regex library |
vm.runInNewContext() / vm.runInThisContext() with untrusted code | Isolated worker threads or dedicated sandbox |
Disabling TLS verification (rejectUnauthorized: false) | Proper certificate management |
Secure Implementation References
- For OWASP Top 10 details with vulnerable → secure code examples: See references/owasp-top-10.md
- For secure coding patterns organized by domain (input validation, auth, crypto, DOM security, subprocess, file I/O, web frameworks): See references/secure-coding.md
Phase 3: Security Verification
Apply a layered verification approach:
1. Static Analysis — Detect common vulnerability patterns automatically
eslint-plugin-security— Node.js security linter ruleseslint-plugin-no-unsanitized— Detect unsafe DOM manipulationsemgrep— Pattern-based analysis with OWASP and TypeScript/JavaScript rulesetstypescript-eslint— Type-aware linting for TypeScript
2. Dependency Audit — Identify known vulnerabilities in third-party packages
npm audit/yarn audit/pnpm audit— Built-in package manager auditingsnyk— Comprehensive vulnerability database and remediation advicesocket.dev— Supply chain attack detection (typosquatting, install scripts)
3. Secrets Detection — Find leaked credentials and API keys
detect-secrets— Baseline-aware secrets scannergitleaks— Git-aware secrets scanning
4. Code Review — Apply the security review workflow and checklists 5. Security Testing — Write negative tests that verify rejection of malicious inputs; fuzz-test parsers and validators
Quick tool commands:
# ESLint security plugins
npm install --save-dev eslint-plugin-security eslint-plugin-no-unsanitized
npx eslint --ext .ts,.js,.tsx,.jsx src/
# npm audit — dependency vulnerabilities
npm audit
npm audit --audit-level=high
# Snyk — comprehensive dependency and code scanning
npx snyk test
npx snyk code test
# detect-secrets — secrets scanning
detect-secrets scan > .secrets.baseline
# Semgrep — advanced pattern matching
semgrep --config=p/javascript --config=p/typescript --config=p/owasp-top-ten src/
# Socket.dev — supply chain security
npx socket npm info <package-name>For complete verification checklists (code review, architecture review, dependency audit, deployment, testing, incident response): See references/security-checklist.md
Phase 4: Dependency and Deployment Security
Dependency Management
- Use lockfiles (
package-lock.json,yarn.lock,pnpm-lock.yaml) and commit them - Run
npm audit/snyk testin CI/CD pipeline on every build - Enable
--ignore-scriptsfor packages where postinstall scripts are not needed - Monitor for typosquatting — verify package names carefully before installing
- Review new dependencies before adding — check maintainership, download counts, known issues
- Use
socket.devor similar tools to detect supply chain attacks (install scripts, obfuscated code) - Prefer packages with provenance attestations (
npm provenance)
Deployment Hardening
- Container security — Scan images with
trivy; use minimal base images (distroless, alpine); run as non-root user - HTTPS/TLS — Enforce TLS 1.2+ for all connections; redirect HTTP to HTTPS; set
Strict-Transport-Securityheader - Security headers — Configure
Content-Security-Policy,X-Content-Type-Options: nosniff,X-Frame-Options: DENY,Permissions-Policy - Secrets at runtime — Inject secrets via environment variables or mounted volumes; never bake into images or bundles
- Least privilege — Run processes as non-root; use read-only filesystems where possible; limit network access
- Source maps — Never deploy source maps to production in public-facing applications
- Client-side — Enable Subresource Integrity (SRI) for CDN scripts; configure strict CSP; avoid inline scripts
- Logging — Use structured logging (JSON); never log passwords, tokens, PII, or full stack traces to users; log authentication events and access denials for audit
OWASP Top 10:2025 Quick Reference
Map each OWASP 2025 category to TypeScript/JavaScript-specific risks and primary mitigations:
| # | Category | TypeScript/JavaScript-Specific Risks | Primary Mitigation |
|---|---|---|---|
| A01 | Broken Access Control | Missing auth middleware, IDOR via sequential IDs, path traversal, SSRF via fetch(userUrl), CORS origin: *, client-side-only auth checks | Centralized auth middleware, object-level permissions, path.resolve() + containment check, URL allowlisting, explicit CORS origins |
| A02 | Security Misconfiguration | NODE_ENV=development in prod, Swagger/docs exposed, verbose error stacks, permissive CORS, default express.static() serving .env | Environment-specific config, disable docs in prod, centralized error handler, explicit CORS, .env outside webroot |
| A03 | Software Supply Chain Failures | Unpinned deps, typosquatting on npm, malicious postinstall scripts, no lockfile, unvetted transitive deps, CI/CD secrets exposure | npm audit / snyk in CI, lockfiles committed, --ignore-scripts, socket.dev, npm provenance |
| A04 | Cryptographic Failures | Math.random() for tokens, weak hashing, hardcoded API keys, disabled TLS verification, secrets in client bundles | crypto.randomUUID() / crypto.getRandomValues(), bcrypt/argon2, env vars / secret manager, proper TLS config |
| A05 | Injection | SQL via template literals, XSS via innerHTML/dangerouslySetInnerHTML, child_process.exec(), NoSQL injection ($gt/$ne operators), SSTI, eval() | Parameterized queries, DOM sanitization (DOMPurify), execFile()/spawn() with array args, input validation, textContent |
| A06 | Insecure Design | No rate limiting, missing input validation layer, no abuse case modeling, client-side enforcement of server-side security | Threat modeling, validation at boundaries (Zod/class-validator), rate limiting middleware, server-side enforcement |
| A07 | Authentication Failures | Weak session config, JWT algorithm: "none" or HS256 with public key, no brute-force protection, tokens in localStorage | Secure session settings, explicit algorithms: ["RS256"], account lockout / rate limiting, HttpOnly cookies |
| A08 | Software or Data Integrity Failures | Prototype pollution, node-serialize deserialization, unsigned updates, CDN scripts without SRI, CI/CD pipeline injection | Schema validation (Zod), JSON.parse() + validation, SRI for CDN scripts, pinned CI actions with SHA |
| A09 | Security Logging and Alerting Failures | Logging passwords/tokens, console.log in production, no auth event logging, missing alerting, no structured logging | Structured logging (pino/winston) with field filtering, audit trail, alerting thresholds, honeytokens |
| A10 | Mishandling of Exceptional Conditions | Unhandled promise rejections, empty catch {}, failing open, sensitive info in error responses, uncaught exceptions crashing process | Specific error types, finally blocks, centralized error handler, process.on('unhandledRejection'), fail-closed patterns |
For detailed vulnerable → secure code examples for each category: See references/owasp-top-10.md
Security Review Workflow
Follow this procedure when reviewing TypeScript or JavaScript code for security:
1. Scan for critical prohibitions — Check for any pattern in the "Critical Prohibitions" table above. Each match is an immediate high-severity finding. 2. Check input validation — Verify every entry point (route handler, CLI argument, file parser, WebSocket handler, queue consumer) validates and sanitizes input before processing. 3. Verify authentication and authorization — Confirm every endpoint requires authentication (unless explicitly public) and checks authorization for the specific resource being accessed. 4. Review data handling — Trace how secrets, PII, and sensitive data flow through the system. Verify encryption at rest and in transit, proper key management, and secure deletion. Ensure no secrets are bundled into client-side code. 5. Check error handling — Ensure errors do not leak stack traces, internal paths, database details, or configuration to users. Verify fail-secure behavior. Check for unhandled promise rejections. 6. Audit dependencies — Run npm audit and snyk test. Flag any unpatched dependencies or packages with known CVEs. Check for suspicious postinstall scripts. 7. Verify logging — Confirm no sensitive data (passwords, tokens, PII) appears in logs. Verify authentication events, authorization failures, and security-relevant actions are logged. 8. Run static analysis — Execute ESLint with security plugins and review findings. Run semgrep with JavaScript/TypeScript and OWASP rulesets for deeper analysis. 9. Check DOM security (client-side) — Verify no unsafe DOM manipulation (innerHTML, document.write). Check CSP configuration, SRI on external scripts, and proper sanitization of user content. 10. Report findings — For each finding, document: severity (Critical/High/Medium/Low), location (file:line), vulnerable code snippet, explanation of the risk, and recommended fix with code example.
Security Hardening Quick Commands
# === Static Analysis ===
npm install --save-dev eslint-plugin-security eslint-plugin-no-unsanitized
npx eslint --ext .ts,.js,.tsx,.jsx src/
semgrep --config=p/javascript --config=p/typescript --config=p/owasp-top-ten src/
# === Dependency Audit ===
npm audit --audit-level=high
npx snyk test
# === Secrets Detection ===
detect-secrets scan > .secrets.baseline
gitleaks detect --source .
# === Lock Dependencies ===
npm ci # install from lockfile (CI/CD)
# === Container Scanning ===
# trivy image <image-name>Reference Files
Consult these files for detailed guidance beyond this overview:
- [references/owasp-top-10.md](references/owasp-top-10.md) — Detailed OWASP Top 10 coverage with TypeScript/JavaScript-specific vulnerable → secure code examples for each category, including Express, Fastify, NestJS, Next.js, and React patterns
- [references/secure-coding.md](references/secure-coding.md) — Secure coding patterns organized by domain: input validation, authentication, cryptography, DOM security, subprocess execution, file operations, and web framework configuration (Express, Fastify, NestJS, Next.js, React)
- [references/security-checklist.md](references/security-checklist.md) — Actionable verification checklists for code review, architecture review, dependency audit, deployment hardening, security testing, and incident response
OWASP Top 10:2025 — TypeScript / JavaScript Security Reference
Reference for AI agents performing TypeScript/JavaScript security reviews, threat modeling, and secure code generation. Covers both server-side (Node.js, Deno, Bun) and client-side (browser) contexts.
Table of Contents
- A01: Broken Access Control
- A02: Security Misconfiguration
- A03: Software Supply Chain Failures
- A04: Cryptographic Failures
- A05: Injection
- A06: Insecure Design
- A07: Authentication Failures
- A08: Software or Data Integrity Failures
- A09: Security Logging and Alerting Failures
- A10: Mishandling of Exceptional Conditions
---
A01: Broken Access Control
Failure to enforce that users act only within their intended permissions. Remains the most common web application vulnerability. In 2025, SSRF (previously A10:2021) is consolidated here as a CWE under broken access control.
TypeScript/JavaScript-Specific Risks
- Missing authorization middleware on routes/endpoints
- Insecure Direct Object References (IDOR): accessing objects by user-supplied ID without ownership check
- Path traversal via unsanitized user input in file operations (
path.joinwith../) - Relying solely on client-side or frontend checks (e.g., hiding UI elements instead of enforcing server-side)
- Overly permissive CORS configuration (
origin: '*'withcredentials: true) - Server-Side Request Forgery (SSRF): fetching user-supplied URLs without validation via
fetch()oraxios - JWT manipulation: tampering with tokens, algorithm confusion, missing audience/issuer validation
Vulnerable Code
// IDOR — no ownership verification (Express)
app.get("/api/orders/:orderId", authenticate, async (req, res) => {
const order = await db.query("SELECT * FROM orders WHERE id = $1", [req.params.orderId]);
// Any authenticated user can access any order
res.json(order.rows[0]);
});
// Path traversal
app.get("/files", (req, res) => {
const filename = req.query.name as string;
res.sendFile(path.join("/uploads", filename)); // ../../etc/passwd
});
// SSRF — user controls the URL entirely
app.get("/fetch", async (req, res) => {
const url = req.query.url as string;
const response = await fetch(url); // Can reach http://169.254.169.254/metadata
const data = await response.text();
res.send(data);
});
// Client-side-only access control
// The server has NO auth check — relies on React router guard
app.get("/api/admin/users", async (req, res) => {
const users = await db.query("SELECT * FROM users");
res.json(users.rows);
});Secure Code
// Object-level permission check
app.get("/api/orders/:orderId", authenticate, async (req, res) => {
const order = await db.query(
"SELECT * FROM orders WHERE id = $1 AND user_id = $2",
[req.params.orderId, req.user.id]
);
if (order.rows.length === 0) {
return res.status(404).json({ error: "Order not found" });
}
res.json(order.rows[0]);
});
// Safe file access with path confinement
app.get("/files", (req, res) => {
const filename = req.query.name as string;
const uploadsDir = path.resolve("/uploads");
const safePath = path.resolve(uploadsDir, filename);
if (!safePath.startsWith(uploadsDir + path.sep)) {
return res.status(400).json({ error: "Invalid file path" });
}
res.sendFile(safePath);
});
// SSRF protection — validate URL and resolved IP
import { URL } from "node:url";
import dns from "node:dns/promises";
import net from "node:net";
const ALLOWED_SCHEMES = new Set(["https:"]);
const BLOCKED_CIDRS = [
"10.0.0.0/8", "172.16.0.0/12", "192.168.0.0/16",
"127.0.0.0/8", "169.254.0.0/16", "::1/128",
];
function isPrivateIP(ip: string): boolean {
return net.isIP(ip) !== 0 && BLOCKED_CIDRS.some((cidr) => {
// Use a CIDR matching library like `ip-cidr` or `netmask`
// Simplified check shown here
return ip.startsWith("10.") || ip.startsWith("172.") ||
ip.startsWith("192.168.") || ip.startsWith("127.") ||
ip.startsWith("169.254.") || ip === "::1";
});
}
async function validateUrl(url: string): Promise<boolean> {
let parsed: URL;
try {
parsed = new URL(url);
} catch {
return false;
}
if (!ALLOWED_SCHEMES.has(parsed.protocol)) return false;
if (!parsed.hostname) return false;
try {
const { address } = await dns.lookup(parsed.hostname);
return !isPrivateIP(address);
} catch {
return false;
}
}
app.get("/fetch", async (req, res) => {
const url = req.query.url as string;
if (!(await validateUrl(url))) {
return res.status(400).json({ error: "URL not allowed" });
}
const response = await fetch(url, { redirect: "error" });
const data = await response.text();
res.send(data);
});
// Server-side authorization check
app.get("/api/admin/users", authenticate, authorize("admin"), async (req, res) => {
const users = await db.query("SELECT id, email, role FROM users");
res.json(users.rows);
});Mitigation Strategies
- Deny by default; require explicit authorization for every endpoint
- Enforce object-level permission checks (not just role checks)
- Use
path.resolve()and verify paths start with the expected directory prefix - Return 404 (not 403) for unauthorized resources to prevent enumeration
- Log and alert on access control failures
- SSRF: validate URLs, block private/internal IPs and cloud metadata endpoints
- SSRF: resolve DNS and validate IP before requests; disable redirects or re-validate
- Never rely on client-side-only access controls — always enforce on the server
---
A02: Security Misconfiguration
Insecure default configurations, incomplete setup, open cloud storage, misconfigured HTTP headers, verbose error messages, or XXE vulnerabilities. Moved up from #5 in 2021.
TypeScript/JavaScript-Specific Risks
NODE_ENV !== "production"or left undefined — enables debug features- Swagger/OpenAPI docs, GraphQL Playground exposed in production
- Express default error handler leaking stack traces
express.static()serving.env,package.json, or source maps- Permissive CORS (
origin: "*"withcredentials: true) - Missing security headers (CSP, HSTS, X-Content-Type-Options)
- Default admin credentials in starter templates
- GraphQL introspection enabled in production
Vulnerable Configuration
// Express — INSECURE
const app = express();
// No helmet, no CORS restriction, stack traces exposed
app.use(cors()); // allows all origins
app.use(express.static(".")); // serves everything including .env
app.use((err: Error, req: Request, res: Response, next: NextFunction) => {
res.status(500).json({ error: err.message, stack: err.stack }); // stack trace leak
});
// GraphQL — INSECURE: introspection enabled in production
const server = new ApolloServer({
typeDefs,
resolvers,
introspection: true, // should be false in production
});Secure Configuration
import helmet from "helmet";
import cors from "cors";
const app = express();
// Security headers via helmet
app.use(helmet());
app.use(helmet.contentSecurityPolicy({
directives: {
defaultSrc: ["'self'"],
scriptSrc: ["'self'"],
styleSrc: ["'self'", "'unsafe-inline'"],
imgSrc: ["'self'", "data:"],
connectSrc: ["'self'"],
fontSrc: ["'self'"],
objectSrc: ["'none'"],
frameAncestors: ["'none'"],
},
}));
// Restrictive CORS
app.use(cors({
origin: ["https://example.com"],
credentials: true,
methods: ["GET", "POST", "PUT", "DELETE"],
allowedHeaders: ["Authorization", "Content-Type"],
}));
// Static files from a dedicated public directory only
app.use(express.static("public", { dotfiles: "deny" }));
// Centralized error handler — hide internals in production
app.use((err: Error, req: Request, res: Response, next: NextFunction) => {
console.error(err); // log full error server-side
res.status(500).json({
error: process.env.NODE_ENV === "production"
? "Internal server error"
: err.message,
});
});
// GraphQL — disable introspection in production
const server = new ApolloServer({
typeDefs,
resolvers,
introspection: process.env.NODE_ENV !== "production",
});
// Fastify — secure configuration
import Fastify from "fastify";
import fastifyHelmet from "@fastify/helmet";
import fastifyCors from "@fastify/cors";
const fastify = Fastify({ logger: true });
await fastify.register(fastifyHelmet);
await fastify.register(fastifyCors, {
origin: ["https://example.com"],
credentials: true,
});Mitigation Strategies
- Use
helmet(Express) or@fastify/helmet(Fastify) for security headers - Set
NODE_ENV=productionin production and conditionally disable debug features - Serve static files from a dedicated directory; deny dotfiles
- Configure strict CORS with explicit origins — never use
*with credentials - Disable GraphQL introspection, Swagger UI, and debug endpoints in production
- Implement centralized error handling that hides internals from users
- Deploy source maps only to error tracking services (Sentry), not to public-facing servers
- Run
npm auditand review configuration regularly
---
A03: Software Supply Chain Failures
Covers the entire software supply chain: known vulnerabilities, unpinned dependencies, typosquatting, transitive dependency risks, CI/CD pipeline security, SBOM management, vendor compromise, and malicious packages. The npm ecosystem is particularly vulnerable due to its large size and reliance on postinstall scripts.
TypeScript/JavaScript-Specific Risks
- Outdated packages with known CVEs in
package.json - No lockfile committed or lockfile not used in CI (
npm installinstead ofnpm ci) - Typosquatting attacks on npm (extremely prevalent — e.g.,
lodashvs1odash) - Malicious postinstall scripts executing arbitrary code on
npm install - Transitive dependency vulnerabilities not visible in direct deps
- No Software Bill of Materials (SBOM) for deployed applications
- CI/CD secrets exposed in logs or untrusted workflows
- Self-propagating npm worms (e.g., the 2025 Shai-Hulud attack)
- Using CDN-hosted scripts without Subresource Integrity (SRI)
Vulnerability Scanning
# npm audit — built-in package manager auditing
npm audit
npm audit --audit-level=high
npm audit fix
# Snyk — comprehensive scanning
npx snyk test
npx snyk monitor # continuous monitoring
# Socket.dev — supply chain attack detection
npx socket npm info <package-name>
# Generate CycloneDX SBOM
npx @cyclonedx/cyclonedx-npm --output-file sbom.jsonDependency Pinning
// package.json — pin exact versions
{
"dependencies": {
"express": "4.21.2", // exact version, not "^4.21.2"
"helmet": "8.0.0"
}
}# Always use lockfile in CI/CD
npm ci # installs from lockfile, fails if lockfile is out of sync
# Ignore scripts for untrusted packages
npm install --ignore-scripts
# Verify package provenance
npm audit signaturesSubresource Integrity (SRI) for Client-Side
<!-- Always use integrity attribute for CDN scripts -->
<script
src="https://cdn.example.com/lib.min.js"
integrity="sha384-abc123..."
crossorigin="anonymous"
></script>Mitigation Strategies
- Commit lockfiles (
package-lock.json,yarn.lock,pnpm-lock.yaml) - Use
npm ci(notnpm install) in CI/CD for deterministic builds - Run
npm auditorsnyk testin CI/CD to block vulnerable dependencies - Enable Dependabot, Renovate, or Socket for automated dependency updates and monitoring
- Use
--ignore-scriptswhen installing packages that do not need postinstall hooks - Verify package names carefully — check download stats, maintainer, and repository on npm
- Use SRI for all CDN-hosted scripts and stylesheets
- Generate and maintain SBOM (CycloneDX or SPDX format)
- Pin CI/CD actions by commit SHA, not mutable tags
- Use
npm audit signaturesto verify package provenance
---
A04: Cryptographic Failures
Failure to properly protect data in transit and at rest, including use of weak algorithms or poor key management.
TypeScript/JavaScript-Specific Risks
- Using
Math.random()for security-sensitive values (predictable PRNG) - Weak password hashing with
crypto.createHash("md5")orcrypto.createHash("sha1") - Hardcoded secrets, API keys, or encryption keys in source code or client bundles
- Storing sensitive data in
localStorageorsessionStorage(accessible to XSS) - Disabled TLS verification (
rejectUnauthorized: false) - Client-side encryption with hardcoded keys (visible in source/bundle)
- Using deprecated Node.js crypto APIs (e.g.,
createCipherinstead ofcreateCipheriv)
Vulnerable Code
// Weak random token generation
const token = Math.random().toString(36).substring(2);
// Weak password hashing
import crypto from "node:crypto";
const hash = crypto.createHash("md5").update(password).digest("hex");
// Hardcoded secret
const JWT_SECRET = "super-secret-key-12345";
// Disabled TLS verification
const agent = new https.Agent({ rejectUnauthorized: false });
const response = await fetch(url, { agent });
// Sensitive data in localStorage
localStorage.setItem("authToken", jwt);
localStorage.setItem("creditCard", cardNumber);
// Deprecated crypto API
const cipher = crypto.createCipher("aes-256-cbc", password); // no IV!Secure Code
import crypto from "node:crypto";
// Cryptographically secure random token
const token = crypto.randomUUID();
// or for raw bytes:
const tokenBytes = crypto.randomBytes(32).toString("hex");
// Browser:
const browserToken = globalThis.crypto.randomUUID();
// Strong password hashing with bcrypt
import bcrypt from "bcrypt";
const SALT_ROUNDS = 12;
const hash = await bcrypt.hash(password, SALT_ROUNDS);
const isValid = await bcrypt.compare(candidatePassword, hash);
// Alternative: argon2
import argon2 from "argon2";
const hash2 = await argon2.hash(password);
const isValid2 = await argon2.verify(hash2, candidatePassword);
// Secret from environment
const JWT_SECRET = process.env.JWT_SECRET;
if (!JWT_SECRET) throw new Error("JWT_SECRET environment variable required");
// Proper TLS — do not disable verification
const response = await fetch(url); // default TLS verification
// Sensitive data in HttpOnly cookies (not localStorage)
res.cookie("session", sessionId, {
httpOnly: true,
secure: true,
sameSite: "strict",
maxAge: 3600000,
});
// Modern crypto API with IV
const algorithm = "aes-256-gcm";
const key = crypto.scryptSync(password, salt, 32);
const iv = crypto.randomBytes(16);
const cipher = crypto.createCipheriv(algorithm, key, iv);Mitigation Strategies
- Use
crypto.randomUUID(),crypto.randomBytes(), orcrypto.getRandomValues()— neverMath.random() - Hash passwords with
bcrypt(cost factor ≥ 12) orargon2 - Store secrets in environment variables or a secret manager — never in source code or client bundles
- Use HttpOnly, Secure, SameSite cookies — never store tokens in
localStorage - Never disable TLS verification (
rejectUnauthorized: false) - Use
crypto.createCipheriv()withaes-256-gcm— nevercreateCipher() - Use
crypto.timingSafeEqual()for constant-time comparison of secrets
---
A05: Injection
Injection occurs when untrusted data is sent to an interpreter and executed as commands. In the TypeScript/JavaScript ecosystem, this includes SQL injection, NoSQL injection, XSS, command injection, and server-side template injection.
TypeScript/JavaScript-Specific Risks
- SQL injection via template literals or string concatenation
- NoSQL injection via MongoDB operators (
$gt,$ne,$regex) in query objects - Cross-Site Scripting (XSS) via
innerHTML,outerHTML,document.write(),dangerouslySetInnerHTML - DOM-based XSS via
location.hash,location.search,document.referrer - Command injection via
child_process.exec()with user input - Server-side template injection (SSTI) in EJS, Pug, Handlebars, Nunjucks
eval()/Function()constructor with user-controlled input- ReDoS (Regular Expression Denial of Service) via crafted input
- Header injection via unsanitized user input in HTTP headers
- GraphQL injection via unvalidated query parameters
Vulnerable Code
// SQL injection via template literal
app.get("/users", async (req, res) => {
const name = req.query.name;
const result = await db.query(`SELECT * FROM users WHERE name = '${name}'`);
res.json(result.rows);
});
// NoSQL injection (MongoDB)
app.post("/login", async (req, res) => {
const user = await User.findOne({
username: req.body.username,
password: req.body.password, // attacker sends { "$ne": "" }
});
if (user) res.json({ token: generateToken(user) });
});
// XSS via innerHTML
const userComment = getUserInput();
document.getElementById("comments")!.innerHTML = userComment;
// React — XSS via dangerouslySetInnerHTML
function Comment({ html }: { html: string }) {
return <div dangerouslySetInnerHTML={{ __html: html }} />;
}
// Command injection
app.get("/ping", (req, res) => {
const host = req.query.host;
exec(`ping -c 4 ${host}`, (err, stdout) => { // host="; cat /etc/passwd"
res.send(stdout);
});
});
// eval with user input
app.get("/calc", (req, res) => {
const expr = req.query.expression;
const result = eval(expr); // arbitrary code execution
res.json({ result });
});
// ReDoS
const EMAIL_REGEX = /^([a-zA-Z0-9_\-\.]+)*@([a-zA-Z0-9_\-\.]+)*\.([a-zA-Z]{2,5})$/;
EMAIL_REGEX.test(userInput); // catastrophic backtracking on crafted input
// Server-side template injection (EJS)
app.get("/greet", (req, res) => {
const template = `<h1>Hello ${req.query.name}</h1>`; // SSTI if name contains template syntax
res.render("inline", { body: template });
});Secure Code
// Parameterized SQL query
app.get("/users", async (req, res) => {
const name = req.query.name;
const result = await db.query("SELECT * FROM users WHERE name = $1", [name]);
res.json(result.rows);
});
// ORM with parameterized queries (Prisma)
const users = await prisma.user.findMany({
where: { name: req.query.name as string },
});
// NoSQL injection prevention — validate input types
import { z } from "zod";
const loginSchema = z.object({
username: z.string().min(1).max(64),
password: z.string().min(1).max(128),
});
app.post("/login", async (req, res) => {
const { username, password } = loginSchema.parse(req.body);
const user = await User.findOne({ username });
if (!user || !(await bcrypt.compare(password, user.passwordHash))) {
return res.status(401).json({ error: "Invalid credentials" });
}
res.json({ token: generateToken(user) });
});
// XSS prevention — use textContent
const userComment = getUserInput();
document.getElementById("comments")!.textContent = userComment;
// XSS prevention — DOMPurify for trusted HTML
import DOMPurify from "dompurify";
const clean = DOMPurify.sanitize(userComment);
document.getElementById("comments")!.innerHTML = clean;
// React — sanitize before dangerouslySetInnerHTML
import DOMPurify from "dompurify";
function Comment({ html }: { html: string }) {
return <div dangerouslySetInnerHTML={{ __html: DOMPurify.sanitize(html) }} />;
}
// Safe command execution with execFile (no shell)
import { execFile } from "node:child_process";
app.get("/ping", (req, res) => {
const host = req.query.host as string;
if (!/^[a-zA-Z0-9.\-]+$/.test(host)) {
return res.status(400).json({ error: "Invalid host" });
}
execFile("ping", ["-c", "4", host], (err, stdout) => {
res.send(stdout);
});
});
// Safe expression evaluation — use a parser library, never eval
import { evaluate } from "mathjs";
app.get("/calc", (req, res) => {
const expr = req.query.expression as string;
try {
const result = evaluate(expr); // mathjs sandboxed evaluation
res.json({ result });
} catch {
res.status(400).json({ error: "Invalid expression" });
}
});
// Safe regex — use re2 or validate input length
import RE2 from "re2";
const emailRegex = new RE2(/^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$/);
function validateEmail(input: string): boolean {
if (input.length > 254) return false;
return emailRegex.test(input);
}Mitigation Strategies
- Use parameterized queries or ORM methods — never concatenate user input into SQL
- Validate all inputs with schema libraries (Zod, class-validator, joi)
- Use
textContentinstead ofinnerHTML; sanitize HTML with DOMPurify when required - Use
child_process.execFile()orspawn()with array arguments — neverexec()with user input - Never use
eval(),Function(), orvm.runInNewContext()with untrusted data - Use
re2for regex on untrusted input or enforce strict length limits - Validate and sanitize data for NoSQL queries — reject objects where strings are expected
- Configure CSP headers to prevent inline scripts and restrict script sources
---
A06: Insecure Design
A broad category representing missing or ineffective security controls at the design level. Differs from implementation bugs — an insecure design cannot be fixed by a perfect implementation.
TypeScript/JavaScript-Specific Risks
- No rate limiting on authentication endpoints
- Client-side enforcement of server-side security (hiding admin routes in React router)
- Missing input validation layer at API boundaries
- No abuse case modeling (bots, scalpers, credential stuffing)
- Business logic flaws allowing unlimited resource creation or data export
- WebSocket connections without authentication or rate limiting
- Missing CSRF protection on state-changing endpoints
Vulnerable Design
// No rate limiting — brute-force friendly
app.post("/login", async (req, res) => {
const { username, password } = req.body;
const user = await User.findOne({ where: { username } });
if (!user || !(await bcrypt.compare(password, user.passwordHash))) {
return res.status(401).json({ error: "Invalid credentials" });
}
res.json({ token: generateToken(user) });
});
// Client-side-only route protection
// React router guard — no server-side enforcement
<Route path="/admin" element={isAdmin ? <AdminPanel /> : <Navigate to="/" />} />Secure Design
// Rate limiting on authentication endpoints
import rateLimit from "express-rate-limit";
const loginLimiter = rateLimit({
windowMs: 15 * 60 * 1000, // 15 minutes
max: 5, // 5 attempts per window
message: { error: "Too many login attempts, please try again later" },
standardHeaders: true,
legacyHeaders: false,
});
app.post("/login", loginLimiter, async (req, res) => {
const { username, password } = loginSchema.parse(req.body);
const user = await User.findOne({ where: { username } });
if (!user || !(await bcrypt.compare(password, user.passwordHash))) {
return res.status(401).json({ error: "Invalid credentials" });
}
res.json({ token: generateToken(user) });
});
// Server-side authorization — defense in depth
// Even with client route guards, always enforce on the server
app.get("/api/admin/*", authenticate, authorize("admin"), adminRouter);Mitigation Strategies
- Implement rate limiting on all authentication, password reset, and high-value endpoints
- Always enforce authorization on the server — client-side guards are for UX only
- Use threat modeling for critical business flows (authentication, payment, data export)
- Add CSRF protection via tokens or SameSite cookies
- Validate and limit resource creation (file uploads, API keys, account creation)
- Add abuse detection for bots and automated attacks
- Authenticate and rate-limit WebSocket connections
---
A07: Authentication Failures
When attackers can trick a system into recognizing an invalid or incorrect user as legitimate.
TypeScript/JavaScript-Specific Risks
- JWT
algorithm: "none"— accepting unsigned tokens - JWT HS256 with a public key (algorithm confusion attack)
- Weak session secrets or default signing keys
- Storing JWTs in
localStorage(vulnerable to XSS) - No brute-force protection on login endpoints
- Session IDs not regenerated after login
- Missing token expiration or overly long-lived tokens
- Password reset tokens that don't expire
Vulnerable Code
// JWT — INSECURE: not specifying algorithms allows "none"
import jwt from "jsonwebtoken";
const payload = jwt.verify(token, publicKey); // accepts algorithm: "none"
// JWT in localStorage
localStorage.setItem("token", jwt);
// Sent via Authorization header — accessible to XSS
fetch("/api/data", { headers: { Authorization: `Bearer ${localStorage.getItem("token")}` } });
// No session regeneration after login (express-session)
app.post("/login", async (req, res) => {
const user = await validateCredentials(req.body);
if (user) {
req.session.userId = user.id; // session fixation: ID not regenerated
res.json({ success: true });
}
});
// Weak session secret
app.use(session({ secret: "keyboard cat" }));Secure Code
import jwt from "jsonwebtoken";
// JWT — SECURE: explicit algorithm restriction
const payload = jwt.verify(token, publicKey, {
algorithms: ["RS256"], // explicit allowlist
audience: "https://api.example.com",
issuer: "https://auth.example.com",
clockTolerance: 30, // 30 seconds tolerance
});
// JWT — sign with explicit algorithm
const token = jwt.sign(
{ sub: user.id, role: user.role },
privateKey,
{ algorithm: "RS256", expiresIn: "15m" }
);
// Store tokens in HttpOnly cookies (not localStorage)
res.cookie("token", token, {
httpOnly: true,
secure: true,
sameSite: "strict",
maxAge: 15 * 60 * 1000, // 15 minutes
path: "/",
});
// Session regeneration after login (express-session)
app.post("/login", async (req, res) => {
const user = await validateCredentials(req.body);
if (user) {
req.session.regenerate((err) => {
if (err) return res.status(500).json({ error: "Session error" });
req.session.userId = user.id;
req.session.save((err) => {
if (err) return res.status(500).json({ error: "Session error" });
res.json({ success: true });
});
});
}
});
// Strong session secret
app.use(session({
secret: process.env.SESSION_SECRET!, // from env or secret manager
name: "__Host-sid",
resave: false,
saveUninitialized: false,
cookie: {
httpOnly: true,
secure: true,
sameSite: "strict",
maxAge: 3600000,
},
}));Mitigation Strategies
- Always specify
algorithmsinjwt.verify()— never rely on defaults - Validate
aud,iss, andexpclaims in JWTs - Store tokens in HttpOnly, Secure, SameSite cookies — not
localStorage - Regenerate session IDs after login to prevent session fixation
- Use strong, random session secrets from environment variables
- Implement rate limiting on login, registration, and password reset endpoints
- Enforce multi-factor authentication where possible
- Set appropriate token expiration (short-lived access tokens, refresh token rotation)
---
A08: Software or Data Integrity Failures
Failures related to code and infrastructure that does not protect against invalid or untrusted code or data being treated as trusted.
TypeScript/JavaScript-Specific Risks
- Prototype pollution via
Object.assign(), spread operators, or deep merge with untrusted objects - Unsafe deserialization (
node-serialize,serialize-javascriptwith untrusted data) - CDN scripts without Subresource Integrity (SRI)
- CI/CD pipelines pulling from untrusted sources without verification
JSON.parse()on untrusted input without subsequent schema validation- Mass assignment: blindly passing request body to ORM create/update methods
Vulnerable Code
// Prototype pollution via deep merge
function deepMerge(target: any, source: any): any {
for (const key of Object.keys(source)) {
if (typeof source[key] === "object" && source[key] !== null) {
target[key] = deepMerge(target[key] || {}, source[key]);
} else {
target[key] = source[key]; // __proto__.isAdmin = true
}
}
return target;
}
app.put("/settings", (req, res) => {
const settings = deepMerge(defaultSettings, req.body);
// Attacker sends: { "__proto__": { "isAdmin": true } }
res.json(settings);
});
// Unsafe deserialization
import serialize from "node-serialize";
const obj = serialize.unserialize(req.body.data); // RCE via IIFE in serialized string
// Mass assignment
app.put("/users/:id", async (req, res) => {
await User.update(req.body, { where: { id: req.params.id } }); // updates ANY field including role
res.json({ success: true });
});
// CDN without SRI
// <script src="https://cdn.example.com/lib.js"></script>Secure Code
// Prototype pollution prevention — validate keys
function safeMerge(target: Record<string, unknown>, source: Record<string, unknown>): Record<string, unknown> {
const result = Object.create(null); // no prototype
Object.assign(result, target);
for (const key of Object.keys(source)) {
if (key === "__proto__" || key === "constructor" || key === "prototype") {
continue; // skip dangerous keys
}
result[key] = source[key];
}
return result;
}
// Schema validation prevents prototype pollution
import { z } from "zod";
const settingsSchema = z.object({
theme: z.enum(["light", "dark"]),
language: z.string().max(5),
notifications: z.boolean(),
});
app.put("/settings", (req, res) => {
const settings = settingsSchema.parse(req.body); // rejects unexpected fields
res.json(settings);
});
// Safe deserialization — always use JSON.parse + schema validation
app.post("/data", (req, res) => {
const data = dataSchema.parse(req.body); // JSON parsed by express, validated by Zod
res.json(data);
});
// Allowlisted fields for update (prevent mass assignment)
const updateUserSchema = z.object({
name: z.string().min(1).max(100),
email: z.string().email(),
// role is NOT included — cannot be set by user
});
app.put("/users/:id", authenticate, async (req, res) => {
const data = updateUserSchema.parse(req.body);
await User.update(data, { where: { id: req.params.id, userId: req.user.id } });
res.json({ success: true });
});<!-- CDN with SRI -->
<script
src="https://cdn.example.com/lib.min.js"
integrity="sha384-abc123..."
crossorigin="anonymous"
></script>Mitigation Strategies
- Guard against prototype pollution: validate/strip
__proto__,constructor,prototypekeys, or useObject.create(null) - Use schema validation (Zod, class-validator) on all user input — rejects unexpected fields
- Never use
node-serializeor similar unsafe deserialization on untrusted data - Use SRI for all CDN-hosted scripts and stylesheets
- Allowlist fields for database create/update operations (prevent mass assignment)
- Pin CI/CD actions by commit SHA; verify artifact integrity
---
A09: Security Logging and Alerting Failures
Without logging and monitoring, attacks and breaches cannot be detected. Without alerting, response is delayed.
TypeScript/JavaScript-Specific Risks
- Using
console.login production without structured logging - Logging passwords, tokens, API keys, credit card numbers, or PII
- No logging of authentication events (login, logout, failed attempts)
- Missing alerting on suspicious activity (brute-force, unusual access patterns)
- Log injection via unsanitized user input in log messages
- No audit trail for sensitive operations (permission changes, data export)
Vulnerable Code
// Logging sensitive data
app.post("/login", async (req, res) => {
console.log(`Login attempt: ${req.body.username} / ${req.body.password}`); // password logged!
// ...
});
// No structured logging
console.log("User " + userId + " accessed record " + recordId);
// Log injection
const username = req.body.username; // attacker sends "admin\n[ERROR] Unauthorized access by root"
console.log(`Login attempt by ${username}`); // log forgingSecure Code
import pino from "pino";
const logger = pino({
level: process.env.LOG_LEVEL || "info",
redact: {
paths: ["password", "token", "authorization", "cookie", "creditCard", "ssn"],
censor: "[REDACTED]",
},
serializers: {
req: pino.stdSerializers.req,
err: pino.stdSerializers.err,
},
});
// Structured logging with field filtering
app.post("/login", async (req, res) => {
const { username } = loginSchema.parse(req.body);
logger.info({ event: "login_attempt", username }, "Login attempt");
const user = await validateCredentials(req.body);
if (!user) {
logger.warn(
{ event: "login_failure", username, ip: req.ip },
"Failed login attempt"
);
return res.status(401).json({ error: "Invalid credentials" });
}
logger.info({ event: "login_success", userId: user.id }, "Successful login");
res.json({ token: generateToken(user) });
});
// Log injection prevention — sanitize user input in logs
function sanitizeForLog(input: string): string {
return input.replace(/[\n\r\t]/g, "");
}
// Audit trail for sensitive operations
app.put("/users/:id/role", authenticate, authorize("admin"), async (req, res) => {
const { role } = roleUpdateSchema.parse(req.body);
await User.update({ role }, { where: { id: req.params.id } });
logger.info({
event: "role_change",
targetUser: req.params.id,
newRole: role,
changedBy: req.user.id,
ip: req.ip,
}, "User role updated");
res.json({ success: true });
});Mitigation Strategies
- Use structured logging (pino, winston) — not
console.login production - Redact sensitive fields (passwords, tokens, PII) from all log output
- Log authentication events (login, logout, failed attempts) with user context
- Sanitize user input before including in log messages to prevent log injection
- Set up alerting for brute-force attempts, unusual access patterns, and access control failures
- Maintain audit trails for permission changes, data access, and administrative actions
- Use centralized log management (ELK, Datadog, Grafana Loki) with alerting
---
A10: Mishandling of Exceptional Conditions
Programs fail to prevent, detect, and respond to unusual and unpredictable situations, leading to crashes, unexpected behavior, and vulnerabilities. New category for 2025.
TypeScript/JavaScript-Specific Risks
- Unhandled promise rejections crashing the process (
nodeterminates by default since v15) - Empty
catch {}blocks swallowing errors silently - Failing open on authentication/authorization errors
- Sensitive information (stack traces, paths, config) in error responses
- Missing
finallyblocks for resource cleanup - Uncaught exceptions in async middleware (Express does not catch async errors by default)
- Missing
defaultcase inswitchstatements - Type coercion errors (
undefinedtreated as falsy bypassing checks)
Vulnerable Code
// Empty catch — swallowed error, application may be in broken state
try {
await processPayment(order);
} catch (e) {
// silently ignored — payment may have partially completed
}
// Failing open — auth error grants access
async function checkPermission(userId: string, resource: string): Promise<boolean> {
try {
const result = await db.query("SELECT allowed FROM acl WHERE user_id = $1 AND resource = $2", [userId, resource]);
return result.rows[0]?.allowed === true;
} catch (err) {
return true; // Database error? Grant access anyway — WRONG
}
}
// Sensitive data in error response
app.use((err: Error, req: Request, res: Response, next: NextFunction) => {
res.status(500).json({
error: err.message,
stack: err.stack, // internal paths, line numbers
query: (err as any).sql, // SQL query leaked
});
});
// Unhandled async error in Express
app.get("/data", async (req, res) => {
const data = await fetchData(); // if this rejects, Express 4 doesn't catch it
res.json(data);
});
// Missing default in switch
function getDiscount(tier: string): number {
switch (tier) {
case "gold": return 0.2;
case "silver": return 0.1;
// missing default — undefined discount if tier is unexpected
}
}Secure Code
// Proper error handling with logging and rollback
try {
await processPayment(order);
} catch (err) {
logger.error({ err, orderId: order.id }, "Payment processing failed");
await rollbackOrder(order.id);
throw err; // re-throw to central error handler
}
// Failing closed — deny access on error
async function checkPermission(userId: string, resource: string): Promise<boolean> {
try {
const result = await db.query("SELECT allowed FROM acl WHERE user_id = $1 AND resource = $2", [userId, resource]);
return result.rows[0]?.allowed === true;
} catch (err) {
logger.error({ err, userId, resource }, "Permission check failed");
return false; // Fail closed — deny access on error
}
}
// Safe error response — hide internals
app.use((err: Error, req: Request, res: Response, next: NextFunction) => {
logger.error({ err, url: req.url, method: req.method }, "Unhandled error");
const statusCode = "statusCode" in err ? (err as any).statusCode : 500;
res.status(statusCode).json({
error: statusCode === 500 ? "Internal server error" : err.message,
});
});
// Async error wrapper for Express 4
function asyncHandler(fn: (req: Request, res: Response, next: NextFunction) => Promise<void>) {
return (req: Request, res: Response, next: NextFunction) => {
fn(req, res, next).catch(next);
};
}
app.get("/data", asyncHandler(async (req, res) => {
const data = await fetchData();
res.json(data);
}));
// Express 5+ handles async errors automatically
// Fastify handles async errors automatically
// Global unhandled rejection handler
process.on("unhandledRejection", (reason, promise) => {
logger.fatal({ reason }, "Unhandled promise rejection — shutting down");
process.exit(1);
});
process.on("uncaughtException", (err) => {
logger.fatal({ err }, "Uncaught exception — shutting down");
process.exit(1);
});
// Exhaustive switch with default
function getDiscount(tier: string): number {
switch (tier) {
case "gold": return 0.2;
case "silver": return 0.1;
case "bronze": return 0.05;
default: return 0; // explicit default
}
}
// TypeScript exhaustive check
type Tier = "gold" | "silver" | "bronze";
function getDiscountExhaustive(tier: Tier): number {
switch (tier) {
case "gold": return 0.2;
case "silver": return 0.1;
case "bronze": return 0.05;
default: {
const _exhaustive: never = tier;
throw new Error(`Unknown tier: ${_exhaustive}`);
}
}
}Mitigation Strategies
- Always handle promise rejections — use
catch()or async error wrappers - Fail closed (deny) on authorization/authentication errors — never grant access on failure
- Use centralized error handlers that log the full error but return generic messages to users
- Register
process.on("unhandledRejection")andprocess.on("uncaughtException")handlers - Use
finallyblocks for resource cleanup (connections, file handles) - Add
defaultcases to allswitchstatements; use TypeScript exhaustive checks withnever - Use Express 5+ or Fastify (which handle async errors automatically) or async wrappers for Express 4
- Implement rate limiting and resource quotas to prevent error-based DoS
- Roll back transactions on failure — never leave partial operations in place
TypeScript / JavaScript Secure Coding Reference
Reference for AI agents implementing secure TypeScript/JavaScript code. Use imperative patterns; prefer allowlisting, least privilege, and defense in depth. Covers both server-side (Node.js, Deno, Bun) and client-side (browser) contexts.
Table of Contents
- 1. Input Validation and Sanitization
- 2. Authentication and Authorization Patterns
- 3. Cryptography Best Practices
- 4. Secure Data Handling
- 5. DOM Security and XSS Prevention
- 6. File and Path Operations
- 7. Subprocess and System Interaction
- 8. Serialization and Prototype Pollution
- 9. Web Framework Security
- 10. Error Handling and Information Disclosure
---
1. Input Validation and Sanitization
Validate all inputs at the boundary using allowlists and strict schema validation. Reject anything not explicitly permitted.
Schema Validation with Zod
// ❌ Anti-pattern: manual validation
function processUser(body: any) {
const name = body.name; // no validation
const age = Number(body.age); // NaN if invalid
}
// ✅ Correct: schema validation with Zod
import { z } from "zod";
const userSchema = z.object({
name: z.string().min(1).max(100).regex(/^[a-zA-Z0-9_\- ]+$/),
age: z.number().int().min(0).max(150),
email: z.string().email().max(254),
});
function processUser(body: unknown) {
const user = userSchema.parse(body); // throws ZodError on invalid input
// user is now fully typed and validated
}Schema Validation with class-validator (NestJS)
import { IsString, IsEmail, IsInt, Min, Max, MinLength, MaxLength, Matches } from "class-validator";
class CreateUserDto {
@IsString()
@MinLength(1)
@MaxLength(100)
@Matches(/^[a-zA-Z0-9_\- ]+$/)
name!: string;
@IsInt()
@Min(0)
@Max(150)
age!: number;
@IsEmail()
@MaxLength(254)
email!: string;
}Allowlisting vs Denylisting
// ❌ Anti-pattern: denylisting dangerous characters
function sanitize(value: string): string {
return value.replace(/[<>&'"]/g, "");
}
// ✅ Correct: allowlist permitted characters
function validateUsername(value: string): string {
if (!/^[a-zA-Z0-9_\-]{3,32}$/.test(value)) {
throw new Error("Invalid username");
}
return value;
}Type Coercion Safety
// ❌ Anti-pattern: loose equality allows type coercion bypass
if (req.query.admin == true) { grant(); } // "true" == true → type coercion
if (req.query.id != null) { process(); } // may pass undefined checks unexpectedly
// ✅ Correct: strict equality, explicit type checks
if (req.query.admin === "true" && user.role === "admin") { grant(); }
if (typeof req.query.id === "string" && req.query.id.length > 0) { process(); }ReDoS Prevention
// ❌ Anti-pattern: catastrophic backtracking
const pattern = /^([a-zA-Z0-9_\-\.]+)*@([a-zA-Z0-9_\-\.]+)*\.([a-zA-Z]{2,})$/;
pattern.test(userInput); // exponential time on crafted input
// ✅ Correct: use RE2 (linear-time guarantees) or enforce length limits
import RE2 from "re2";
const safePattern = new RE2(/^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$/);
function safeMatch(pattern: RE2, value: string, maxLen = 1000): boolean {
if (value.length > maxLen) throw new Error("Input too long");
return pattern.test(value);
}File Upload Validation
// ❌ Anti-pattern: trust file extension and user-supplied name
app.post("/upload", (req, res) => {
const file = req.file!;
fs.writeFileSync(`/uploads/${file.originalname}`, file.buffer); // path traversal + type bypass
});
// ✅ Correct: validate type, size, and sanitize name
import { randomUUID } from "node:crypto";
import { fileTypeFromBuffer } from "file-type";
const ALLOWED_TYPES = new Set(["image/png", "image/jpeg", "application/pdf"]);
const MAX_SIZE = 10 * 1024 * 1024; // 10 MB
async function saveUpload(buffer: Buffer, originalName: string, uploadDir: string): Promise<string> {
if (buffer.length > MAX_SIZE) throw new Error("File too large");
const type = await fileTypeFromBuffer(buffer);
if (!type || !ALLOWED_TYPES.has(type.mime)) {
throw new Error(`Disallowed file type: ${type?.mime ?? "unknown"}`);
}
const ext = path.extname(originalName).toLowerCase();
if (![".png", ".jpg", ".jpeg", ".pdf"].includes(ext)) {
throw new Error("Invalid extension");
}
const safeName = `${randomUUID()}${ext}`;
const dest = path.join(uploadDir, safeName);
await fs.promises.writeFile(dest, buffer);
return safeName;
}---
2. Authentication and Authorization Patterns
JWT Validation
// ❌ Anti-pattern: no algorithm restriction, no audience/issuer validation
const payload = jwt.verify(token, secret);
// ✅ Correct: explicit algorithm, audience, issuer
import jwt from "jsonwebtoken";
const payload = jwt.verify(token, publicKey, {
algorithms: ["RS256"],
audience: "https://api.example.com",
issuer: "https://auth.example.com",
clockTolerance: 30,
});Password Hashing
// ❌ Anti-pattern: weak hashing
import crypto from "node:crypto";
const hash = crypto.createHash("sha256").update(password).digest("hex");
// ✅ Correct: bcrypt with appropriate cost factor
import bcrypt from "bcrypt";
const SALT_ROUNDS = 12;
const hash = await bcrypt.hash(password, SALT_ROUNDS);
const isValid = await bcrypt.compare(candidatePassword, hash);
// ✅ Alternative: argon2
import argon2 from "argon2";
const hash2 = await argon2.hash(password, {
type: argon2.argon2id,
memoryCost: 65536,
timeCost: 3,
parallelism: 4,
});
const isValid2 = await argon2.verify(hash2, candidatePassword);Centralized Authorization Middleware
// ❌ Anti-pattern: scattered auth checks
app.get("/admin/users", async (req, res) => {
if (req.user?.role !== "admin") return res.status(403).end();
// ... handler logic
});
// ✅ Correct: centralized middleware
function authorize(...roles: string[]) {
return (req: Request, res: Response, next: NextFunction) => {
if (!req.user) return res.status(401).json({ error: "Unauthorized" });
if (roles.length > 0 && !roles.includes(req.user.role)) {
return res.status(403).json({ error: "Forbidden" });
}
next();
};
}
app.get("/admin/users", authenticate, authorize("admin"), adminUsersHandler);
app.put("/profile", authenticate, authorize(), profileUpdateHandler);Session Management
import session from "express-session";
import RedisStore from "connect-redis";
import { createClient } from "redis";
const redisClient = createClient();
await redisClient.connect();
app.use(session({
store: new RedisStore({ client: redisClient }),
secret: process.env.SESSION_SECRET!,
name: "__Host-sid",
resave: false,
saveUninitialized: false,
cookie: {
httpOnly: true,
secure: true,
sameSite: "strict",
maxAge: 3600000, // 1 hour
path: "/",
},
}));
// Regenerate session ID on login
app.post("/login", async (req, res) => {
const user = await validateCredentials(req.body);
if (!user) return res.status(401).json({ error: "Invalid credentials" });
req.session.regenerate((err) => {
if (err) return res.status(500).json({ error: "Session error" });
req.session.userId = user.id;
req.session.save((err) => {
if (err) return res.status(500).json({ error: "Session error" });
res.json({ success: true });
});
});
});
// Destroy session on logout
app.post("/logout", (req, res) => {
req.session.destroy((err) => {
if (err) return res.status(500).json({ error: "Logout failed" });
res.clearCookie("__Host-sid");
res.json({ success: true });
});
});---
3. Cryptography Best Practices
Secure Random Values
import crypto from "node:crypto";
// ❌ Anti-pattern: predictable values
const token = Math.random().toString(36).substring(2);
const id = Date.now().toString();
// ✅ Correct: cryptographic random
const token = crypto.randomUUID();
const tokenHex = crypto.randomBytes(32).toString("hex");
const tokenUrlSafe = crypto.randomBytes(32).toString("base64url");
// Browser context
const browserToken = globalThis.crypto.randomUUID();
const randomArray = new Uint8Array(32);
globalThis.crypto.getRandomValues(randomArray);Symmetric Encryption
import crypto from "node:crypto";
// ❌ Anti-pattern: deprecated createCipher (no IV, insecure)
const cipher = crypto.createCipher("aes-256-cbc", password);
// ✅ Correct: AES-256-GCM with random IV
function encrypt(plaintext: string, key: Buffer): { iv: string; data: string; tag: string } {
const iv = crypto.randomBytes(16);
const cipher = crypto.createCipheriv("aes-256-gcm", key, iv);
const encrypted = Buffer.concat([cipher.update(plaintext, "utf8"), cipher.final()]);
const tag = cipher.getAuthTag();
return {
iv: iv.toString("hex"),
data: encrypted.toString("hex"),
tag: tag.toString("hex"),
};
}
function decrypt(encrypted: { iv: string; data: string; tag: string }, key: Buffer): string {
const decipher = crypto.createDecipheriv(
"aes-256-gcm",
key,
Buffer.from(encrypted.iv, "hex")
);
decipher.setAuthTag(Buffer.from(encrypted.tag, "hex"));
return decipher.update(encrypted.data, "hex", "utf8") + decipher.final("utf8");
}Constant-Time Comparison
import crypto from "node:crypto";
// ❌ Anti-pattern: timing attack vulnerable
if (providedToken === expectedToken) { /* ... */ }
// ✅ Correct: constant-time comparison
function safeCompare(a: string, b: string): boolean {
if (a.length !== b.length) return false;
return crypto.timingSafeEqual(Buffer.from(a), Buffer.from(b));
}Key Derivation
import crypto from "node:crypto";
// Derive encryption key from password + salt
const salt = crypto.randomBytes(16);
const key = crypto.scryptSync(password, salt, 32); // 32 bytes = 256 bits
// async version:
crypto.scrypt(password, salt, 32, (err, key) => { /* ... */ });---
4. Secure Data Handling
Secrets Management
// ❌ Anti-pattern: hardcoded secrets
const API_KEY = "sk_live_abc123";
const DB_PASSWORD = "supersecret";
// ✅ Correct: environment variables with validation
const requiredEnv = (name: string): string => {
const value = process.env[name];
if (!value) throw new Error(`Missing required environment variable: ${name}`);
return value;
};
const config = {
apiKey: requiredEnv("API_KEY"),
dbUrl: requiredEnv("DATABASE_URL"),
jwtSecret: requiredEnv("JWT_SECRET"),
} as const;
// Never log or expose config valuesCookie Security
// ❌ Anti-pattern: insecure cookies
res.cookie("session", token);
res.cookie("prefs", data, { httpOnly: false });
// ✅ Correct: secure cookie settings
res.cookie("session", token, {
httpOnly: true, // not accessible via JavaScript
secure: true, // HTTPS only
sameSite: "strict", // CSRF protection
maxAge: 3600000, // 1 hour
path: "/",
domain: ".example.com",
});Sensitive Data in Client Bundles
// ❌ Anti-pattern: secrets in client-side code
// .env or hardcoded — these end up in the browser bundle
const STRIPE_SECRET_KEY = process.env.STRIPE_SECRET_KEY; // in Next.js client component
// ✅ Correct: only public keys in client bundles
// Use NEXT_PUBLIC_ prefix (Next.js) for intentionally public values only
const STRIPE_PUBLIC_KEY = process.env.NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY;
// Keep secret keys server-side only (API routes, server components, getServerSideProps)---
5. DOM Security and XSS Prevention
Safe DOM Manipulation
// ❌ Anti-pattern: innerHTML with untrusted content
element.innerHTML = userInput; // XSS
document.write(userInput); // XSS
element.outerHTML = userInput; // XSS
element.insertAdjacentHTML("beforeend", userInput); // XSS
// ✅ Correct: use textContent for plain text
element.textContent = userInput; // safe — rendered as text, not HTML
// ✅ Correct: use DOMPurify when HTML rendering is required
import DOMPurify from "dompurify";
element.innerHTML = DOMPurify.sanitize(userInput);
// ✅ Correct: use DOM API for structured content
const link = document.createElement("a");
link.textContent = userInput;
link.href = sanitizeUrl(userInput); // validate URL scheme
container.appendChild(link);React XSS Prevention
// ❌ Anti-pattern: dangerouslySetInnerHTML without sanitization
function Comment({ html }: { html: string }) {
return <div dangerouslySetInnerHTML={{ __html: html }} />;
}
// ✅ Correct: sanitize with DOMPurify
import DOMPurify from "dompurify";
function Comment({ html }: { html: string }) {
return <div dangerouslySetInnerHTML={{ __html: DOMPurify.sanitize(html) }} />;
}
// ✅ Best: avoid dangerouslySetInnerHTML entirely — use markdown-to-JSX or similar
import ReactMarkdown from "react-markdown";
function Comment({ markdown }: { markdown: string }) {
return <ReactMarkdown>{markdown}</ReactMarkdown>;
}URL Sanitization
// ❌ Anti-pattern: arbitrary URLs including javascript:
<a href={userUrl}>Click</a>
// ✅ Correct: validate URL scheme
function sanitizeUrl(url: string): string {
try {
const parsed = new URL(url);
if (!["https:", "http:", "mailto:"].includes(parsed.protocol)) {
return "#";
}
return parsed.href;
} catch {
return "#";
}
}Content Security Policy
// Express with helmet
import helmet from "helmet";
app.use(helmet.contentSecurityPolicy({
directives: {
defaultSrc: ["'self'"],
scriptSrc: ["'self'"], // no 'unsafe-inline', no 'unsafe-eval'
styleSrc: ["'self'", "'unsafe-inline'"], // inline styles if needed
imgSrc: ["'self'", "data:", "https:"],
connectSrc: ["'self'", "https://api.example.com"],
fontSrc: ["'self'"],
objectSrc: ["'none'"],
frameSrc: ["'none'"],
frameAncestors: ["'none'"],
baseUri: ["'self'"],
formAction: ["'self'"],
upgradeInsecureRequests: [],
},
}));postMessage Security
// ❌ Anti-pattern: no origin check
window.addEventListener("message", (event) => {
processData(event.data); // accepts messages from any origin
});
// ✅ Correct: verify origin
window.addEventListener("message", (event) => {
if (event.origin !== "https://trusted.example.com") return;
const data = messageSchema.parse(event.data); // validate structure too
processData(data);
});
// ✅ Correct: specify target origin when sending
targetWindow.postMessage(data, "https://trusted.example.com");
// NEVER use "*" for targetOrigin with sensitive data---
6. File and Path Operations
Path Traversal Prevention
import path from "node:path";
import fs from "node:fs/promises";
// ❌ Anti-pattern: user input directly in path
app.get("/download", async (req, res) => {
const filePath = path.join("/uploads", req.query.file as string);
res.sendFile(filePath); // ../../../etc/passwd
});
// ✅ Correct: resolve and verify containment
app.get("/download", async (req, res) => {
const filename = req.query.file as string;
const baseDir = path.resolve("/uploads");
const resolved = path.resolve(baseDir, filename);
if (!resolved.startsWith(baseDir + path.sep)) {
return res.status(400).json({ error: "Invalid file path" });
}
try {
await fs.access(resolved);
res.sendFile(resolved);
} catch {
res.status(404).json({ error: "File not found" });
}
});Temp File Security
import os from "node:os";
import fs from "node:fs/promises";
import path from "node:path";
import crypto from "node:crypto";
// ✅ Correct: unique temp directory with cleanup
async function withTempDir<T>(fn: (dir: string) => Promise<T>): Promise<T> {
const tmpDir = await fs.mkdtemp(path.join(os.tmpdir(), "app-"));
try {
return await fn(tmpDir);
} finally {
await fs.rm(tmpDir, { recursive: true, force: true });
}
}---
7. Subprocess and System Interaction
Safe Command Execution
import { execFile, spawn } from "node:child_process";
// ❌ Anti-pattern: shell injection via exec
import { exec } from "node:child_process";
exec(`ping -c 4 ${userInput}`); // command injection
exec(`convert ${inputFile} ${outputFile}`, { shell: true }); // shell injection
// ✅ Correct: execFile with array arguments (no shell)
execFile("ping", ["-c", "4", validatedHost], (err, stdout, stderr) => {
if (err) { /* handle error */ }
});
// ✅ Correct: spawn with array arguments
const child = spawn("convert", [inputFile, outputFile]); // no shell
child.on("error", (err) => { /* handle error */ });
// ✅ Correct: if shell is absolutely necessary, validate input strictly
const HOSTNAME_REGEX = /^[a-zA-Z0-9.\-]+$/;
if (!HOSTNAME_REGEX.test(host)) {
throw new Error("Invalid hostname");
}
execFile("ping", ["-c", "4", host]);Environment Variable Injection Prevention
// ❌ Anti-pattern: passing user input as env vars to subprocess
execFile("cmd", args, { env: { ...process.env, USER_INPUT: req.body.input } });
// ✅ Correct: validate and sanitize environment variables
const sanitizedInput = allowedValues.includes(req.body.input)
? req.body.input
: "default";
execFile("cmd", args, { env: { ...process.env, CONFIG_MODE: sanitizedInput } });---
8. Serialization and Prototype Pollution
Prototype Pollution Prevention
// ❌ Anti-pattern: recursive merge without key filtering
function merge(target: any, source: any): any {
for (const key in source) {
if (typeof source[key] === "object") {
target[key] = merge(target[key] || {}, source[key]);
} else {
target[key] = source[key];
}
}
return target;
}
// Attacker sends: { "__proto__": { "isAdmin": true } }
// ✅ Correct: use Zod to validate shape, or filter dangerous keys
const FORBIDDEN_KEYS = new Set(["__proto__", "constructor", "prototype"]);
function safeMerge(target: Record<string, unknown>, source: Record<string, unknown>): Record<string, unknown> {
const result: Record<string, unknown> = Object.create(null);
for (const key of Object.keys(target)) {
result[key] = target[key];
}
for (const key of Object.keys(source)) {
if (FORBIDDEN_KEYS.has(key)) continue;
result[key] = source[key];
}
return result;
}
// ✅ Best: use schema validation — Zod rejects unknown fields
import { z } from "zod";
const settingsSchema = z.object({
theme: z.enum(["light", "dark"]),
language: z.string().max(5),
}).strict(); // rejects any extra fields including __proto__Safe JSON Handling
// ❌ Anti-pattern: JSON.parse without validation
const data = JSON.parse(untrustedString);
doSomething(data.criticalField); // could be any type
// ✅ Correct: parse then validate with schema
const raw = JSON.parse(untrustedString); // syntactic parsing
const data = mySchema.parse(raw); // semantic validationAvoiding Unsafe Deserialization
// ❌ NEVER: node-serialize, serialize-javascript with untrusted data
import serialize from "node-serialize";
const obj = serialize.unserialize(userInput); // RCE
// ✅ Correct: use JSON.parse + schema validation
const obj = JSON.parse(userInput); // safe: no code execution
const validated = schema.parse(obj); // type-safe and validated---
9. Web Framework Security
Express Security Best Practices
import express from "express";
import helmet from "helmet";
import cors from "cors";
import rateLimit from "express-rate-limit";
const app = express();
// Security headers
app.use(helmet());
// Body size limits
app.use(express.json({ limit: "100kb" }));
app.use(express.urlencoded({ extended: false, limit: "100kb" }));
// Rate limiting
app.use(rateLimit({
windowMs: 15 * 60 * 1000,
max: 100,
standardHeaders: true,
legacyHeaders: false,
}));
// CORS
app.use(cors({
origin: ["https://example.com"],
credentials: true,
methods: ["GET", "POST", "PUT", "DELETE"],
}));
// Disable X-Powered-By (helmet does this too)
app.disable("x-powered-by");
// Trust proxy (when behind reverse proxy)
app.set("trust proxy", 1); // trust first proxyFastify Security Best Practices
import Fastify from "fastify";
import fastifyHelmet from "@fastify/helmet";
import fastifyCors from "@fastify/cors";
import fastifyRateLimit from "@fastify/rate-limit";
const app = Fastify({
logger: true,
bodyLimit: 102400, // 100kb
trustProxy: true,
});
await app.register(fastifyHelmet);
await app.register(fastifyCors, {
origin: ["https://example.com"],
credentials: true,
});
await app.register(fastifyRateLimit, {
max: 100,
timeWindow: "15 minutes",
});NestJS Security Best Practices
import { NestFactory } from "@nestjs/core";
import { ValidationPipe } from "@nestjs/common";
import helmet from "helmet";
const app = await NestFactory.create(AppModule);
app.use(helmet());
app.enableCors({
origin: ["https://example.com"],
credentials: true,
});
app.useGlobalPipes(new ValidationPipe({
whitelist: true, // strip unrecognized properties
forbidNonWhitelisted: true, // reject if unknown props present
transform: true,
}));Next.js Security Best Practices
// next.config.js — security headers
const nextConfig = {
poweredByHeader: false,
headers: async () => [{
source: "/(.*)",
headers: [
{ key: "X-Content-Type-Options", value: "nosniff" },
{ key: "X-Frame-Options", value: "DENY" },
{ key: "Referrer-Policy", value: "strict-origin-when-cross-origin" },
{ key: "Permissions-Policy", value: "camera=(), microphone=(), geolocation=()" },
],
}],
// Do NOT expose source maps in production
productionBrowserSourceMaps: false,
};
// Server-side only secrets (API routes, getServerSideProps, Server Components)
// NEVER use NEXT_PUBLIC_ prefix for secretsCSRF Protection
// Double-submit cookie pattern
import csrf from "csurf";
app.use(csrf({ cookie: { httpOnly: true, secure: true, sameSite: "strict" } }));
// Or use SameSite cookies (modern approach)
// Set SameSite=Strict on session cookies — browser won't send them on cross-origin POST
res.cookie("session", token, {
httpOnly: true,
secure: true,
sameSite: "strict",
});---
10. Error Handling and Information Disclosure
Centralized Error Handler
// ❌ Anti-pattern: inconsistent error handling across routes
app.get("/users", async (req, res) => {
try { /* ... */ } catch (e) { res.status(500).json({ error: (e as Error).stack }); }
});
// ✅ Correct: centralized error handler
class AppError extends Error {
constructor(
message: string,
public statusCode: number = 500,
public isOperational: boolean = true,
) {
super(message);
this.name = "AppError";
}
}
// Error handler middleware (must be last)
app.use((err: Error, req: Request, res: Response, next: NextFunction) => {
if (err instanceof AppError && err.isOperational) {
return res.status(err.statusCode).json({ error: err.message });
}
// Unknown/programming error — log full details, return generic message
logger.error({ err, url: req.url, method: req.method }, "Unexpected error");
res.status(500).json({ error: "Internal server error" });
});Async Error Handling (Express 4)
// Express 4 does NOT catch async errors — they cause unhandled rejections
// ❌ Anti-pattern: unhandled async error
app.get("/data", async (req, res) => {
const data = await fetchData(); // uncaught rejection if this throws
res.json(data);
});
// ✅ Correct: async wrapper
const asyncHandler = (fn: (req: Request, res: Response, next: NextFunction) => Promise<void>) =>
(req: Request, res: Response, next: NextFunction) => fn(req, res, next).catch(next);
app.get("/data", asyncHandler(async (req, res) => {
const data = await fetchData();
res.json(data);
}));
// ✅ Better: use Express 5+ or Fastify (handle async errors natively)Process-Level Error Handlers
// Catch unhandled rejections and uncaught exceptions
process.on("unhandledRejection", (reason, promise) => {
logger.fatal({ reason }, "Unhandled promise rejection");
// Gracefully shut down
server.close(() => process.exit(1));
});
process.on("uncaughtException", (err) => {
logger.fatal({ err }, "Uncaught exception");
// Gracefully shut down
server.close(() => process.exit(1));
});
// Graceful shutdown on signals
for (const signal of ["SIGTERM", "SIGINT"] as const) {
process.on(signal, () => {
logger.info(`Received ${signal}, shutting down gracefully`);
server.close(() => process.exit(0));
});
}Safe Error Responses
// ❌ Anti-pattern: leaking internal details
res.status(500).json({
error: err.message, // may contain internal details
stack: err.stack, // file paths, line numbers
query: (err as any).query, // SQL query
});
// ✅ Correct: generic message for 5xx, specific for 4xx
function errorResponse(err: Error, res: Response): void {
if (err instanceof AppError) {
res.status(err.statusCode).json({ error: err.message });
} else {
res.status(500).json({ error: "Internal server error" });
}
}TypeScript / JavaScript Security Checklist
Actionable verification checklists for secure TypeScript/JavaScript development. Covers both server-side (Node.js, Deno, Bun) and client-side (browser) contexts.
---
1. Code-Level Security Review
Input Validation
- [ ] All external inputs validated with schema validation (Zod, class-validator, AJV)
- [ ] Allowlist validation used instead of denylist
- [ ] Request body size limits enforced (
express.json({ limit }), FastifybodyLimit) - [ ] File uploads validated (MIME type via magic bytes, size limits, extension allowlist)
- [ ] Regular expressions reviewed for ReDoS — use RE2 for untrusted patterns
- [ ] Strict equality (
===) used everywhere — no loose equality (==) - [ ]
parseInt()called with explicit radix:parseInt(val, 10) - [ ] Query parameters and path params validated before use
Injection Prevention
- [ ] Parameterized queries for all SQL (Prisma, Drizzle, Knex, TypeORM
createQueryBuilderwith parameters) - [ ] No string concatenation or template literals in SQL queries
- [ ] MongoDB queries use typed filters — no raw
$where, no unsanitized$gt/$nein query objects - [ ] No
eval(),Function(),vm.runInNewContext()with untrusted input - [ ]
child_process.execFile()/spawn()used instead ofexec()— no shell interpretation - [ ] Template engines configured with auto-escaping enabled
- [ ] GraphQL queries use parameterized variables, depth limiting, and query complexity analysis
DOM / XSS Prevention
- [ ] No
innerHTML,outerHTML,document.write(),insertAdjacentHTML()with unsanitized content - [ ] DOMPurify used when HTML rendering of user content is required
- [ ]
textContentused instead ofinnerHTMLfor plain text - [ ] React: no
dangerouslySetInnerHTMLwithout DOMPurify sanitization - [ ] URL values validated with allowlisted schemes (
https:,http:,mailto:) — blockjavascript: - [ ]
postMessagelisteners verifyevent.originagainst allowlist - [ ] CSP headers configured — no
unsafe-inlineorunsafe-evalinscript-src
Sensitive Data
- [ ] No secrets hardcoded in source (API keys, passwords, tokens, connection strings)
- [ ] No secrets in client-side bundles (no
NEXT_PUBLIC_for secret values) - [ ] Secrets loaded from environment variables or secret managers, validated at startup
- [ ] No sensitive data in error messages, logs, or stack traces returned to clients
- [ ] Tokens stored in HttpOnly cookies, not
localStorageorsessionStorage - [ ]
.envfiles listed in.gitignore
---
2. Architecture-Level Security
Authentication
- [ ] JWT validation specifies explicit
algorithms(e.g.,["RS256"]) — neveralgorithms: undefined - [ ] JWT
audienceandissuervalidated - [ ] Passwords hashed with bcrypt (cost ≥ 12) or argon2id
- [ ] Session IDs regenerated on login (
req.session.regenerate()) - [ ] Session destroyed on logout
- [ ] Account lockout or progressive delays after failed login attempts
- [ ] Multi-factor authentication supported for privileged accounts
Authorization
- [ ] Authorization checks centralized in middleware, not scattered across route handlers
- [ ] Resource ownership validated (users can only access their own resources)
- [ ] Role-based or attribute-based access control implemented consistently
- [ ] Default-deny: endpoints require authentication unless explicitly public
- [ ] IDOR prevention: validate requesting user owns the accessed resource
API Security
- [ ] Rate limiting applied to all endpoints (stricter on auth endpoints)
- [ ] CORS configured with specific origins — no wildcard
*with credentials - [ ] API versioning implemented
- [ ] Response pagination enforced — no unbounded result sets
- [ ] Unnecessary HTTP methods disabled per route
- [ ] Request timeout configured to prevent slowloris
Cryptography
- [ ]
crypto.randomUUID()orcrypto.randomBytes()used — neverMath.random()for security - [ ] AES-256-GCM or ChaCha20-Poly1305 for symmetric encryption
- [ ]
crypto.timingSafeEqual()for constant-time comparison of secrets/tokens - [ ] TLS 1.2+ enforced for all external connections
- [ ] No deprecated crypto:
createCipher(), MD5, SHA1 for security purposes - [ ] Keys derived with scrypt or PBKDF2 when password-based encryption is needed
---
3. Dependency and Supply Chain Security
Package Management
- [ ]
npm audit/yarn audit/pnpm auditrun in CI — builds fail on critical/high vulnerabilities - [ ] Lock files (
package-lock.json,yarn.lock,pnpm-lock.yaml) committed and integrity-checked - [ ] Exact versions or lock files used — no floating ranges (
^,~) for security-critical dependencies - [ ]
npm ciused in CI/CD instead ofnpm install(respects lock file exactly) - [ ] Dependencies reviewed before adoption (check maintainer activity, download count, known issues)
- [ ]
socket.devor similar tool used for supply chain attack detection
Dependency Hygiene
- [ ] Unused dependencies removed
- [ ]
devDependenciesnot installed in production (npm ci --omit=dev) - [ ]
preinstall/postinstallscripts audited — use--ignore-scriptswhen possible - [ ] No dependencies with known prototype pollution or RCE vulnerabilities
- [ ] Renovate or Dependabot configured for automated dependency updates in PRs
- [ ]
node_modulesnever committed to version control
Container / Build Security (if applicable)
- [ ] Multi-stage Docker builds — no dev dependencies or source maps in final image
- [ ] Non-root user in container (
USER node) - [ ]
.dockerignoreexcludes.env,.git,node_modules, build artifacts - [ ] Base image pinned to digest, scanned with trivy or similar
- [ ] No secrets in Dockerfile or build args — use runtime secret injection
---
4. Configuration Security
Server Configuration
- [ ]
helmet(Express) or equivalent security headers middleware applied - [ ]
X-Powered-Byheader disabled - [ ]
trust proxyconfigured correctly when behind reverse proxy - [ ] HTTPS enforced — HTTP redirects to HTTPS, HSTS header set
- [ ] Source maps not served in production (
productionBrowserSourceMaps: false) - [ ] Debug/development endpoints disabled in production (
NODE_ENV=production)
Cookie Configuration
- [ ]
HttpOnly: trueon session and auth cookies - [ ]
Secure: trueon all cookies (HTTPS only) - [ ]
SameSite: "strict"or"lax"— never"none"unless required - [ ] Appropriate
maxAge/expiresset — no indefinite sessions - [ ] Cookie
pathscoped to necessary routes - [ ]
__Host-prefix used for strict cookie security (when applicable)
Security Headers
- [ ]
Content-Security-Policy— restrictive directives, nounsafe-* - [ ]
X-Content-Type-Options: nosniff - [ ]
X-Frame-Options: DENYorSAMEORIGIN - [ ]
Referrer-Policy: strict-origin-when-cross-originorno-referrer - [ ]
Permissions-Policyrestricting camera, microphone, geolocation, etc. - [ ]
Strict-Transport-Securitywithmax-age≥ 31536000 andincludeSubDomains - [ ]
Cross-Origin-Opener-Policy: same-origin
---
5. Deployment Security
Environment
- [ ]
NODE_ENV=productionset in production deployments - [ ] Debug logging disabled or set to appropriate level in production
- [ ] Error details (stack traces, query details) never returned to clients in production
- [ ] Application runs as non-root user
- [ ] File system permissions restrict write access to necessary directories only
Secrets
- [ ] Secrets injected at runtime via environment variables or secret manager
- [ ] No secrets in Docker images, CI logs, or version control
- [ ] Secret rotation process documented and tested
- [ ]
detect-secretsorgitleaksin pre-commit hooks to catch accidental commits
CI/CD Pipeline
- [ ]
npm audit --audit-level=highgate in CI pipeline - [ ] ESLint security plugins run in CI (
eslint-plugin-security,@microsoft/eslint-plugin-sdl) - [ ] Semgrep or similar SAST tool integrated in CI
- [ ] Container image scanning (trivy, Snyk Container) before deployment
- [ ] SBOM (Software Bill of Materials) generated for production builds
- [ ] Artifact attestations (GitHub Attestations, Sigstore) for production artifacts
---
6. Testing and Verification
Security Testing
- [ ] Input validation tested with malicious payloads (injection strings, boundary values)
- [ ] Authentication bypass attempts tested (missing tokens, expired tokens, tampered tokens)
- [ ] Authorization tested (accessing other users' resources, privilege escalation)
- [ ] Rate limiting verified to work under load
- [ ] CORS policy tested — cross-origin requests from unauthorized origins rejected
- [ ] File upload tested with invalid types, oversized files, path traversal filenames
Automated Security Checks
- [ ]
eslint-plugin-security— detectseval(),exec(), non-literal regex, etc. - [ ]
eslint-plugin-no-unsanitized— detects unsafe DOM manipulation - [ ]
@typescript-eslint/no-explicit-any— minimizes untyped escape hatches - [ ] Semgrep rules for TypeScript/JavaScript security patterns
- [ ]
npm audit/ Snyk in CI with severity thresholds - [ ] Pre-commit hooks:
detect-secrets,gitleaks, lint checks
---
7. Security Tools Reference
| Tool | Purpose | Integration |
|---|---|---|
eslint-plugin-security | Static analysis — Node.js security rules | ESLint config |
eslint-plugin-no-unsanitized | Detect unsafe DOM APIs | ESLint config |
@microsoft/eslint-plugin-sdl | Microsoft SDL security rules | ESLint config |
semgrep | SAST — multi-language security patterns | CI pipeline |
npm audit / yarn audit | Dependency vulnerability scanning | CI pipeline |
snyk | Dependency + container + code scanning | CI pipeline / CLI |
socket.dev | Supply chain attack detection | npm registry proxy / CI |
trivy | Container image + dependency scanning | CI pipeline |
detect-secrets | Pre-commit secret detection | Git hooks |
gitleaks | Git history secret scanning | Git hooks / CI |
helmet | Security headers middleware | Express/Fastify |
DOMPurify | HTML sanitization | Client-side / SSR |
zod | Runtime schema validation | Application code |
re2 | Safe regex (linear time) | Application code |
rate-limiter-flexible | Advanced rate limiting | Application code |
---
8. Incident Response
Preparation
- [ ] Security logging captures auth events, access denials, input validation failures
- [ ] Structured logging (pino, winston) with correlation IDs — no sensitive data in logs
- [ ] Log aggregation and alerting configured (anomalous patterns, repeated failures)
- [ ] Dependency vulnerability alerts enabled (GitHub Dependabot, Snyk, socket.dev)
Response Steps
1. Identify — Confirm the vulnerability and assess scope 2. Contain — Disable affected endpoints, revoke compromised tokens/sessions 3. Eradicate — Patch the vulnerability, update dependencies 4. Recover — Redeploy, verify fix, rotate secrets if needed 5. Learn — Post-incident review, update checklists, add regression tests