Security guidance for React + Express + MongoDB + JWT stacks. See examples.md for unsafe/safe code pairs, checklists.md for quick checklists, references.md for all sources.
Core Philosophy — Confidence-Based Review
Before flagging any issue, trace the data flow and confirm the input source.
| Confidence | Criteria | Action |
|---|---|---|
| HIGH | Vulnerable pattern + attacker-controlled input confirmed | Report with file, line, exploit, and fix |
| MEDIUM | Vulnerable pattern, input source unclear | Note for manual verification |
| LOW | Theoretical / best-practice deviation | Do not report — mention only if asked |
Do NOT flag: test files, dead code, server-controlled values (env vars, config constants), framework-mitigated patterns (React JSX escaping, Mongoose parameterized queries), development-only code gated by NODE_ENV.
Golden rule:
fetch(process.env.API_URL)= safe.fetch(req.query.url)= vulnerable. Always ask: "Can an attacker control this value?"
OWASP Top 10:2025
| # | Category | Key Risk in This Stack |
|---|---|---|
| A01 | Broken Access Control | Missing auth middleware, IDOR, no ownership checks |
| A02 | Security Misconfiguration | Helmet disabled, CORS wildcard, stack traces in prod |
| A03 | Supply Chain Failures | Compromised npm packages, typosquatting |
| A04 | Cryptographic Failures | Weak JWT secret, low bcrypt cost, hardcoded secrets |
| A05 | Injection | MongoDB operator injection, XSS, command injection |
| A06 | Insecure Design | Missing rate limiting, no threat model |
| A07 | Authentication Failures | jwt.decode() instead of verify(), brute force |
| A08 | Integrity Failures | Mass assignment via req.body spread, unvalidated uploads |
| A09 | Logging Failures | Passwords in logs, missing auth event audit trail |
| A10 | Exceptional Conditions | Fail-open auth, stack trace leaks, missing async error handling |
A01 — Broken Access Control
- Deny by default — every route is protected unless explicitly public
- Apply
authmiddleware as a barrier (viarouter.use(auth)) rather than per-route to prevent omissions - Always check ownership on update/delete — being authenticated does not mean authorized for all resources
- Compare
resource.author.toString() === req.user.userIdplus admin role escape hatch - React route guards are UX only — server must enforce all access control
- Test both horizontal (user A → user B's data) and vertical (author → admin endpoints) escalation
A02 — Security Misconfiguration
- Helmet: Enable with
crossOriginResourcePolicy: 'cross-origin'for uploads. If CSP disabled, implement custom CSP restrictingscript-srcto'self' - CORS: Explicit origin allowlist with
credentials: true. Neverorigin: '*'ororigin: truewith credentials - Error handler: Generic message in production, stack traces only when
NODE_ENV === 'development' - Vite env vars:
VITE_*prefixed vars are public in the client bundle — never put secrets there - MongoDB: Require auth + TLS in production, never expose port 27017 to internet, use
serverSelectionTimeoutMS - Defaults: Change seed credentials, disable debug middleware in production
A03 — Supply Chain Failures
- Run
npm auditbefore every release. Commitpackage-lock.jsonalways. - Pin exact versions for security-sensitive packages
- Before adding a dependency: check CVEs, maintenance activity (< 6 months), download count (> 10K/week), scope of access (network/fs/env)
- Watch for typosquatting (
expresvsexpress) - Vite plugins run with full Node.js access during build — use only well-known publishers
A04 — Cryptographic Failures
- Passwords: bcrypt with salt rounds >= 10 (never MD5, SHA-1/256, or plain text). Preferred: Argon2id > bcrypt > scrypt
- JWT secret: Minimum 256-bit (32 bytes) cryptographically random, from
process.env.JWT_SECRET - JWT signing: Explicitly set
algorithm: 'HS256'andexpiresIn. Never allownonealgorithm - JWT verification: Always
jwt.verify(), neverjwt.decode()(decode doesn't check signature) - JWT payload: Never store sensitive data (it's base64-encoded, not encrypted)
- Transit: HTTPS in production,
Strict-Transport-Securityheader via Helmet - At rest: Encrypt sensitive fields if storing PII. Never store raw credentials in MongoDB
A05 — Injection
MongoDB NoSQL Injection
- Cast input types explicitly:
String(req.body.email)— neutralizes operator objects like{ "$gt": "" } - Never put user input in
$where,$expr, or$function(server-side JS execution) - Never construct regex from user input without escaping (
str.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')) - Use Mongoose strict schemas with type definitions. Consider
mongo-sanitizeto strip$keys
Cross-Site Scripting (XSS)
- React auto-escapes JSX — that's your default safety net
- Never use
dangerouslySetInnerHTMLwithout DOMPurify sanitization (allowlist tags/attributes) - Validate URLs before
href/src— rejectjavascript:protocol, allow onlyhttp:/https: - Blog content and AI-generated content are high-risk for stored XSS — sanitize on input AND output
- Set CSP headers to mitigate any XSS that slips through
Command Injection
- Never use
exec()orspawn({ shell: true })with user input - Use
execFile()which passes arguments directly without shell interpretation
A06 — Insecure Design
Rate limiting strategy:
| Endpoint | Limit | Window |
|---|---|---|
| Login | 5 req | 15 min |
| Comments | 5 req | 1 min |
| AI generation | 3 req | 1 min |
| File upload | 10 req | 1 min |
| General API | 100 req | 1 min |
- AI content generation: Sanitize AI output before storing (XSS risk), validate prompt length, set request timeouts, never expose API keys to client, log prompts for audit
- Comments: Rate limit + sanitize + moderation (
isApprovedflag)
A07 — Authentication Failures
- Login: Find user by email with
isActive: true, compare with bcrypt, return generic "Invalid credentials" for both wrong email and wrong password (prevents enumeration) - JWT claims:
{ userId, email, name, role }— minimum viable - Token extraction: Support
Bearer <token>from Authorization header - Auth middleware must be fail-closed:
next()only called on successfuljwt.verify(). Missingreturnbefore error response = fail-open vulnerability - Rate limit login:
loginLimiteron login endpoint (5 attempts / 15 min) - Password model: Hash in
pre('save')hook only if modified,comparePasswordinstance method,toJSONstrips password from all responses - Consider account lockout, progressive delays, CAPTCHA after N failures
A08 — Software and Data Integrity
- Mass assignment prevention: Destructure only expected fields from
req.body— neverModel.create(req.body)(attacker can setrole: 'admin') - Mongoose schemas: Use
enumfor fixed values,requiredon mandatory fields,minlength/maxlength,strict: true(default) - File upload integrity: MIME type allowlist, check magic bytes for high-security. MIME types can be spoofed
- CSP: Restrict
script-src,frame-ancestors 'none',object-src 'none'
A09 — Logging and Alerting
Log these: Login success/failure, token rejection, admin actions, file uploads, rate limit hits, 5xx errors, 403 denials Never log: Passwords, JWT tokens, API keys, credit cards, SSNs
- Redact sensitive fields (
password,token,secret,authorization) before logging — replace with*** - Use structured JSON logging for production (searchable, integrates with aggregation)
- Log auth events with userId, IP, timestamp, and action
A10 — Exceptional Conditions
- Fail-closed: Errors must deny access, not grant it.
catchblocks mustreturnerror response before anynext()call - Global error handler: Must have 4 params
(err, req, res, next). Return generic message in production. - Async wrapper: Use
asyncHandlerto catch Promise rejections — eliminates need for try-catch in every controller - DB connection:
serverSelectionTimeoutMS: 5000,process.exit(1)on connection failure - 404 handler: Catch-all for unmatched routes (
app.use('*', ...))
File Upload Security
- MIME allowlist:
['image/jpeg', 'image/png', 'image/gif', 'image/webp']— whitelist, not blacklist - Size limit:
5 * 1024 * 1024(5MB) viamulter.limits - Filename: Server-generated (
blog-{timestamp}-{random9digits}.{ext}) — never user-provided - Path traversal: Use
path.basename()to strip directory components. Validatepath.resolve()starts with upload directory before deletion - Cleanup: Delete uploaded file when resource is deleted, validate path is within uploads dir
- Serving:
crossOriginResourcePolicy: 'cross-origin'for static files, no directory listing
Secret Detection
Scan for these patterns in all code and config:
| Type | Pattern |
|---|---|
| AWS Key | AKIA[0-9A-Z]{16} |
| Google API | AIza[0-9A-Za-z_-]{35} |
| JWT/Generic | (secret|key|token|password)\s*[:=]\s*['"][^'"]{8,} |
| MongoDB URI | mongodb(\+srv)?://[^:]+:[^@]+@ |
| Private Key | -----BEGIN .* PRIVATE KEY----- |
| GitHub Token | gh[ps]_[A-Za-z0-9]{36,} |
| npm Token | npm_[A-Za-z0-9]{36} |
| Slack Token | xox[bpsa]-[0-9a-zA-Z-]+ |
Never commit: .env, .env.local, .env.production, logs/, uploads/
Agentic AI Security (OWASP 2026)
Relevant to AI content generation features (Gemini API):
- ASI01 Goal Hijacking: Sanitize prompt input, set max length, strip control characters
- ASI02 Tool Misuse: AI should not have access to system tools without explicit scoping
- ASI03 Identity Abuse: API keys server-side only, short-lived if possible
- ASI05 Code Execution: Never execute AI-generated code without review
- ASI09 Trust Exploitation: Label AI-generated content, validate before storing
- Sanitize AI output before storing — it could contain XSS, malicious links, or script tags
Framework Security Quirks
JavaScript/Node.js
- Prototype pollution via
__proto__/constructor.prototype— validate object keys JSON.parse()throws on malformed input — always wrap in try-catchRegExp(userInput)enables ReDoS — escape special characterspath.join()with user input allows traversal — usepath.basename()firstsetTimeout(string)is implicit eval — always pass a function
MongoDB/Mongoose
- Query operator injection via JSON body
{ password: { "$gt": "" } }— cast toString() $whereclause = server-side JS execution — never use with user input- Unbounded queries exhaust memory — always
.limit()and.skip() strict: falseaccepts arbitrary fields — keep defaultstrict: true
React
dangerouslySetInnerHTMLbypasses escaping — require DOMPurifyhref={userUrl}allowsjavascript:XSS — validate protocolVITE_*env vars are public — never prefix secrets
Express
- Middleware ordering matters — auth bypass if
router.use(auth)placed after unprotected routes req.queryvalues are always strings — validate and cast types- Error handler must have exactly 4 params — missing
nextbreaks error handling - Set
trust proxybehind reverse proxy — otherwise rate limiter gets wrong IP
Security Review Process
- Detect context — API endpoint, auth logic, DB query, file handling, frontend, config, or dependency change
- Load relevant rules — Only the OWASP categories that apply to this context
- Trace data flow — Where does the input come from? Is it attacker-controlled?
- Check upstream controls — Middleware, framework defaults, validation already applied?
- Verify exploitability — Can an attacker actually reach and control this?
- Report HIGH confidence only — Include file, line, exploit scenario, specific fix
Severity Classification
| Severity | Criteria | Stack Examples |
|---|---|---|
| CRITICAL | Direct exploit, no auth required | NoSQL injection auth bypass, hardcoded prod secrets, missing auth on admin endpoint, RCE |
| HIGH | Exploitable with conditions | Stored XSS in blog content, IDOR on delete, JWT with weak secret, passwords in logs |
| MEDIUM | Specific conditions, limited impact | Missing rate limiter, CORS misconfiguration, verbose prod errors, missing input validation |
| LOW | Defense-in-depth | Low bcrypt cost, missing security event logging, sequential ObjectId exposure |
ASVS 5.0 Quick Reference
Level 1 (All Apps): 12+ char passwords, breached list check, login rate limiting, 128+ bit session entropy, HTTPS, server-side validation, generic error messages
Level 2 (Sensitive): + MFA for sensitive ops, crypto key management, comprehensive security logging, schema-based input validation, CSRF protection
Level 3 (Critical): + HSM key storage, documented threat model, anomaly detection, penetration testing, supply chain verification