mirror of
https://github.com/github/awesome-copilot.git
synced 2026-09-02 14:03:11 +00:00
1089 lines
31 KiB
Markdown
1089 lines
31 KiB
Markdown
---
|
|
applyTo: '**'
|
|
description: 'Comprehensive secure coding standards based on OWASP Top 10 2025, with 55+ anti-patterns, detection regex, framework-specific fixes for modern web and backend frameworks, and AI/LLM security guidance.'
|
|
---
|
|
|
|
# Security Standards
|
|
|
|
Comprehensive security rules for web application development. Every anti-pattern includes a severity classification, detection method, OWASP 2025 reference, and corrective code examples.
|
|
|
|
**Severity levels:**
|
|
|
|
- **CRITICAL** — Exploitable vulnerability. Must be fixed before merge.
|
|
- **IMPORTANT** — Significant risk. Should be fixed in the same sprint.
|
|
- **SUGGESTION** — Defense-in-depth improvement. Plan for a future iteration.
|
|
|
|
---
|
|
|
|
## OWASP Top 10 — 2025 Quick Reference
|
|
|
|
| # | Category | Key Mitigation |
|
|
|---|----------|----------------|
|
|
| A01 | Broken Access Control | Auth middleware on every endpoint, RBAC, ownership checks |
|
|
| A02 | Security Misconfiguration | Security headers, no debug in prod, no default credentials |
|
|
| A03 | Software Supply Chain Failures *(NEW)* | `npm audit`, lockfile integrity, SBOM, SLSA provenance |
|
|
| A04 | Cryptographic Failures | Argon2id/bcrypt for passwords, TLS everywhere, no secrets in code |
|
|
| A05 | Injection | Parameterized queries, input validation, no raw HTML with user input |
|
|
| A06 | Insecure Design | Threat modeling, secure design patterns, abuse case testing |
|
|
| A07 | Authentication Failures | Rate-limit login, secure session management, MFA |
|
|
| A08 | Software or Data Integrity Failures | SRI for CDN scripts, signed artifacts, no insecure deserialization |
|
|
| A09 | Security Logging and Alerting Failures | Log security events, no PII in logs, correlation IDs, active alerting |
|
|
| A10 | Mishandling of Exceptional Conditions *(NEW)* | Handle all errors, no stack traces in prod, fail-secure |
|
|
|
|
---
|
|
|
|
## Injection Anti-Patterns (I1-I8)
|
|
|
|
### I1: SQL Injection via String Concatenation
|
|
|
|
- **Severity**: CRITICAL
|
|
- **Detection**: `\$\{.*\}.*(?:SELECT|INSERT|UPDATE|DELETE|FROM|WHERE)`
|
|
- **OWASP**: A05
|
|
|
|
```typescript
|
|
// BAD
|
|
const unsafeResult = await db.query(`SELECT * FROM users WHERE id = ${userId}`);
|
|
|
|
// GOOD — parameterized query
|
|
const safeResult = await db.query('SELECT * FROM users WHERE id = $1', [userId]);
|
|
```
|
|
|
|
### I2: NoSQL Injection (MongoDB Operator Injection)
|
|
|
|
- **Severity**: CRITICAL
|
|
- **Detection**: `\{\s*\$(?:gt|gte|lt|lte|ne|in|nin|regex|where|exists)`
|
|
- **OWASP**: A05
|
|
|
|
```typescript
|
|
// BAD — attacker sends { "password": { "$gt": "" } }
|
|
const user = await User.findOne({ username: req.body.username, password: req.body.password });
|
|
|
|
// GOOD — validate and cast input types
|
|
const username = String(req.body.username);
|
|
const password = String(req.body.password);
|
|
const user = await User.findOne({ username });
|
|
const valid = user && await verifyPassword(user.passwordHash, password);
|
|
```
|
|
|
|
### I3: Command Injection (exec with User Input)
|
|
|
|
- **Severity**: CRITICAL
|
|
- **Detection**: `(?:exec|execSync|execFile|execFileSync)\s*\(.*(?:req\.|params\.|query\.|body\.)`
|
|
- **OWASP**: A05
|
|
|
|
```typescript
|
|
// BAD — shell interpolation, sync call blocks the event loop
|
|
import { execFileSync } from 'node:child_process';
|
|
const unsafeOutput = execFileSync('sh', ['-c', `ls -la ${req.query.dir}`]);
|
|
|
|
// GOOD — async execFile, arguments array, no shell, bounded time/output
|
|
import { execFile } from 'node:child_process';
|
|
import { promisify } from 'node:util';
|
|
const pExecFile = promisify(execFile);
|
|
|
|
const dir = String(req.query.dir ?? '');
|
|
if (!dir || dir.startsWith('-')) throw new Error('Invalid directory');
|
|
const { stdout: safeOutput } = await pExecFile('ls', ['-la', '--', dir], {
|
|
timeout: 5_000, // fail fast on hung processes
|
|
maxBuffer: 1 << 20, // 1 MiB cap to prevent memory exhaustion
|
|
});
|
|
|
|
// BEST — allowlist validation on top of the async, bounded call above
|
|
const allowedDirs = ['/data', '/public'];
|
|
if (!allowedDirs.includes(dir)) throw new Error('Invalid directory');
|
|
```
|
|
|
|
Prefer async `execFile`/`spawn` over `execFileSync` in server handlers: the sync variant blocks Node's event loop and can amplify DoS impact. Always pass a `timeout` and `maxBuffer` to bound execution.
|
|
|
|
### I4: XSS via Unsanitized HTML Rendering
|
|
|
|
- **Severity**: CRITICAL
|
|
- **Detection**: `(?:v-html|\[innerHTML\]|dangerouslySetInner|bypassSecurityTrust)`
|
|
- **OWASP**: A05
|
|
|
|
Applies to all frontend frameworks. Each has an API that bypasses default XSS protection:
|
|
|
|
- **React**: `dangerouslySetInnerHTML` prop with raw user content
|
|
- **Angular**: `[innerHTML]` binding or `bypassSecurityTrustHtml` with unsanitized input
|
|
- **Vue**: `v-html` directive with user-controlled content
|
|
|
|
```typescript
|
|
// GOOD — sanitize with DOMPurify before rendering any raw HTML
|
|
import DOMPurify from 'dompurify';
|
|
const clean = DOMPurify.sanitize(userContent);
|
|
|
|
// BEST — use text interpolation when HTML is not needed
|
|
// React: {userContent}
|
|
// Angular: {{ userContent }}
|
|
// Vue: {{ userContent }}
|
|
```
|
|
|
|
### I5: SSRF via User-Controlled URLs
|
|
|
|
- **Severity**: CRITICAL
|
|
- **Detection**: `fetch\((?:req\.|params\.|query\.|body\.|url|href)`
|
|
- **OWASP**: A01
|
|
|
|
```typescript
|
|
// BAD
|
|
const data = await fetch(req.body.url);
|
|
|
|
// GOOD — scheme allowlist + hostname allowlist + DNS/IP validation (see TOCTOU note)
|
|
import { promises as dns } from 'node:dns';
|
|
|
|
function isPrivateIP(ip: string): boolean {
|
|
// Normalize IPv4-mapped IPv6 (e.g., ::ffff:127.0.0.1 → 127.0.0.1)
|
|
const normalized = ip.startsWith('::ffff:') ? ip.slice(7) : ip;
|
|
// IPv4 private/reserved/loopback ranges
|
|
if (/^(10\.|172\.(1[6-9]|2\d|3[01])\.|192\.168\.|127\.|0\.|169\.254\.)/.test(normalized)) return true;
|
|
// IPv6 loopback, link-local (fe80::/10), and unique-local
|
|
if (/^(::1|fe[89ab]|fc|fd)/i.test(normalized)) return true;
|
|
return false;
|
|
}
|
|
|
|
const parsed = new URL(req.body.url);
|
|
if (parsed.protocol !== 'https:') throw new Error('Only HTTPS allowed');
|
|
const allowedHosts = ['api.example.com', 'cdn.example.com'];
|
|
if (!allowedHosts.includes(parsed.hostname)) throw new Error('Host not allowed');
|
|
// Resolve all A/AAAA records to prevent DNS rebinding via multiple IPs
|
|
const resolved = await dns.lookup(parsed.hostname, { all: true });
|
|
if (resolved.length === 0 || resolved.some(({ address }) => isPrivateIP(address))) {
|
|
throw new Error('Private or reserved IPs not allowed');
|
|
}
|
|
// Note: for production, pin the resolved IP in the HTTP client to prevent
|
|
// TOCTOU rebinding between this check and fetch(). See undici Agent docs.
|
|
const data = await fetch(parsed.toString(), { redirect: 'error' });
|
|
```
|
|
|
|
### I6: Path Traversal in File Operations
|
|
|
|
- **Severity**: CRITICAL
|
|
- **Detection**: `(?:readFile|readFileSync|createReadStream|path\.join)\s*\(.*(?:req\.|params\.|query\.|body\.)`
|
|
- **OWASP**: A01
|
|
|
|
```typescript
|
|
// BAD
|
|
const file = fs.readFileSync(`/data/${req.params.filename}`);
|
|
|
|
// GOOD — resolve and validate within allowed directory
|
|
import path from 'path';
|
|
const basePath = '/data';
|
|
const filePath = path.resolve(basePath, req.params.filename);
|
|
if (!filePath.startsWith(basePath + path.sep)) throw new Error('Path traversal detected');
|
|
const file = fs.readFileSync(filePath);
|
|
```
|
|
|
|
### I7: Template Injection
|
|
|
|
- **Severity**: CRITICAL
|
|
- **Detection**: `(?:render|compile|template)\s*\(.*(?:req\.|params\.|query\.|body\.)`
|
|
- **OWASP**: A05
|
|
|
|
```typescript
|
|
// BAD — user input as template source
|
|
const html = ejs.render(req.body.template, data);
|
|
|
|
// GOOD — predefined templates, user input only as data
|
|
const html = ejs.renderFile('./templates/page.ejs', { content: req.body.content });
|
|
```
|
|
|
|
### I8: XXE Injection (XML External Entity)
|
|
|
|
- **Severity**: CRITICAL
|
|
- **Detection**: `(?:parseXml|DOMParser|xml2js|libxmljs).*(?:req\.|body\.|file)`
|
|
- **OWASP**: A05
|
|
|
|
```typescript
|
|
// GOOD — disable external entities in XML parser
|
|
import { XMLParser } from 'fast-xml-parser';
|
|
const parser = new XMLParser({
|
|
allowBooleanAttributes: true,
|
|
processEntities: false,
|
|
htmlEntities: false,
|
|
});
|
|
const result = parser.parse(req.body.xml);
|
|
```
|
|
|
|
---
|
|
|
|
## Authentication Anti-Patterns (AU1-AU8)
|
|
|
|
### AU1: JWT Algorithm Confusion (alg:none)
|
|
|
|
- **Severity**: CRITICAL
|
|
- **Detection**: `jwt\.verify\((?![^)]*\balgorithms\b)[^)]*\)`
|
|
- **OWASP**: A07
|
|
|
|
```typescript
|
|
// BAD — accepts any algorithm including "none"
|
|
const decoded = jwt.verify(token, secret);
|
|
|
|
// GOOD — enforce specific algorithm
|
|
const decoded = jwt.verify(token, publicKey, { algorithms: ['RS256'] });
|
|
```
|
|
|
|
### AU2: JWT Without Expiration Check
|
|
|
|
- **Severity**: CRITICAL
|
|
- **Detection**: `jwt\.sign\((?![^)]*\b(?:expiresIn|exp)\b)[^)]*\)`
|
|
- **OWASP**: A07
|
|
|
|
```typescript
|
|
// BAD — token never expires
|
|
const token = jwt.sign({ userId: user.id }, secret);
|
|
|
|
// GOOD — short-lived token
|
|
const token = jwt.sign({ userId: user.id }, secret, { expiresIn: '15m' });
|
|
```
|
|
|
|
### AU3: JWT Stored in localStorage
|
|
|
|
- **Severity**: IMPORTANT
|
|
- **Detection**: `localStorage\.setItem\(.*(?:token|jwt|auth|session)`
|
|
- **OWASP**: A07
|
|
|
|
```typescript
|
|
// BAD — accessible via XSS
|
|
localStorage.setItem('accessToken', token);
|
|
|
|
// GOOD — httpOnly cookie set by server
|
|
res.cookie('token', token, { httpOnly: true, secure: true, sameSite: 'strict' });
|
|
```
|
|
|
|
### AU4: Plaintext / Fast Hash for Passwords (MD5/SHA-1/SHA-256)
|
|
|
|
- **Severity**: CRITICAL
|
|
- **Detection**: `(?:createHash|md5|sha1|sha256)\s*\(.*password`
|
|
- **OWASP**: A04
|
|
|
|
```typescript
|
|
// BAD — fast hash, no salt
|
|
const sha256Hash = crypto.createHash('sha256').update(password).digest('hex');
|
|
|
|
// GOOD — Argon2id (OWASP recommended)
|
|
import { hash as argon2Hash, argon2id } from 'argon2';
|
|
const hashed = await argon2Hash(password, { type: argon2id, memoryCost: 65536, timeCost: 3 });
|
|
```
|
|
|
|
### AU5: Missing Brute-Force Protection on Login
|
|
|
|
- **Severity**: CRITICAL
|
|
- **Detection**: `(?:post|router\.post)\s*\(\s*['"]\/(?:login|signin|auth|register|reset)`
|
|
- **OWASP**: A07
|
|
|
|
```typescript
|
|
// BAD — no rate limiting
|
|
app.post('/api/auth/login', loginHandler);
|
|
|
|
// GOOD
|
|
import rateLimit from 'express-rate-limit';
|
|
const authLimiter = rateLimit({ windowMs: 15 * 60 * 1000, max: 5 });
|
|
app.post('/api/auth/login', authLimiter, loginHandler);
|
|
|
|
// GOOD — serverless / multi-instance: the default in-memory store is per process,
|
|
// so back the counter with shared storage (e.g. rate-limit-redis for Express,
|
|
// or a Redis-backed limiter such as @upstash/ratelimit in edge/serverless handlers)
|
|
import { Ratelimit } from '@upstash/ratelimit';
|
|
import { Redis } from '@upstash/redis';
|
|
const loginLimiter = new Ratelimit({ redis: Redis.fromEnv(), limiter: Ratelimit.slidingWindow(5, '15 m'), prefix: 'auth:login' });
|
|
const { success } = await loginLimiter.limit(`login:${clientIp}`);
|
|
if (!success) return new Response('Too Many Requests', { status: 429 });
|
|
```
|
|
|
|
### AU6: Missing Session Regeneration on Login (Session Fixation)
|
|
|
|
- **Severity**: IMPORTANT
|
|
- **Detection**: `(?:session|req\.session)\s*\.\s*(?:userId|user|authenticated)\s*=`
|
|
- **OWASP**: A07
|
|
|
|
```typescript
|
|
// GOOD — regenerate session ID on successful login to prevent fixation
|
|
req.session.regenerate((err) => {
|
|
if (err) return next(err);
|
|
req.session.userId = user.id;
|
|
req.session.save(next);
|
|
});
|
|
```
|
|
|
|
Related: on password change or elevation, also invalidate all other active sessions for the user (e.g., by bumping a `tokenVersion` column and rejecting sessions with a stale version, or by iterating the session store and destroying entries keyed to that user).
|
|
|
|
### AU7: OAuth Without State Parameter
|
|
|
|
- **Severity**: CRITICAL
|
|
- **Detection**: `authorize\?(?![^\n#]*\bstate=)[^\n#]*`
|
|
- **OWASP**: A07
|
|
|
|
```typescript
|
|
// GOOD — include state parameter for CSRF protection
|
|
const state = crypto.randomBytes(32).toString('hex');
|
|
session.oauthState = state;
|
|
const authUrl = `https://provider.com/authorize?client_id=${clientId}&redirect_uri=${redirectUri}&state=${state}`;
|
|
```
|
|
|
|
### AU8: Missing PKCE for Public OAuth Clients
|
|
|
|
- **Severity**: IMPORTANT
|
|
- **Detection**: `(?:authorization_code|code).*(?!.*code_challenge)`
|
|
- **OWASP**: A07
|
|
|
|
Use PKCE (Proof Key for Code Exchange) with S256 challenge method for all public clients (SPAs, mobile).
|
|
|
|
---
|
|
|
|
## Authorization Anti-Patterns (AZ1-AZ6)
|
|
|
|
### AZ1: Missing Auth Middleware on New Endpoints
|
|
|
|
- **Severity**: CRITICAL
|
|
- **Detection**: `(?:app|router)\.\w+\s*\(\s*['"]\/api\/(?:admin|users|settings)`
|
|
- **OWASP**: A01
|
|
|
|
```typescript
|
|
// BAD
|
|
router.delete('/api/users/:id', deleteUser);
|
|
|
|
// GOOD
|
|
router.delete('/api/users/:id', authenticate, authorize('admin'), deleteUser);
|
|
```
|
|
|
|
### AZ2: Client-Side Only Authorization
|
|
|
|
- **Severity**: CRITICAL
|
|
- **Detection**: Component guards without server-side checks
|
|
- **OWASP**: A01
|
|
|
|
Frontend guards are UX only. ALWAYS verify on server.
|
|
|
|
### AZ3: IDOR (Insecure Direct Object Reference)
|
|
|
|
- **Severity**: CRITICAL
|
|
- **Detection**: `params\.(?:id|userId|orderId)` without ownership check
|
|
- **OWASP**: A01
|
|
|
|
```typescript
|
|
// GOOD — verify ownership
|
|
router.get('/api/orders/:orderId', authenticate, async (req, res) => {
|
|
const order = await Order.findById(req.params.orderId);
|
|
if (!order || order.userId !== req.user.id) {
|
|
return res.status(404).json({ error: 'Not found' });
|
|
}
|
|
res.json(order);
|
|
});
|
|
```
|
|
|
|
### AZ4: Mass Assignment
|
|
|
|
- **Severity**: CRITICAL
|
|
- **Detection**: `(?:create|update|findOneAndUpdate)\s*\(\s*req\.body\s*\)`
|
|
- **OWASP**: A01
|
|
|
|
```typescript
|
|
// BAD
|
|
await User.findByIdAndUpdate(id, req.body);
|
|
|
|
// GOOD — explicitly pick allowed fields
|
|
const { name, email, avatar } = req.body;
|
|
await User.findByIdAndUpdate(id, { name, email, avatar });
|
|
```
|
|
|
|
### AZ5: Privilege Escalation via Role Parameter
|
|
|
|
- **Severity**: CRITICAL
|
|
- **Detection**: `req\.body\.role|req\.body\.isAdmin|req\.body\.permissions`
|
|
- **OWASP**: A01
|
|
|
|
```typescript
|
|
// GOOD — ignore role from input
|
|
const { name, email, password } = req.body;
|
|
const user = await User.create({ name, email, password, role: 'user' });
|
|
```
|
|
|
|
### AZ6: Missing Re-Authentication for Sensitive Operations
|
|
|
|
- **Severity**: IMPORTANT
|
|
- **Detection**: `(?:delete|destroy|remove).*(?:account|user|organization)` without re-auth
|
|
- **OWASP**: A01
|
|
|
|
Require current password before account deletion, email change, or other sensitive operations.
|
|
|
|
---
|
|
|
|
## Secrets Anti-Patterns (S1-S6)
|
|
|
|
### S1: Hardcoded API Keys / Tokens
|
|
|
|
- **Severity**: CRITICAL
|
|
- **Detection**: `(?:password|secret|api_key|token|apiKey)\s*[:=]\s*['"][A-Za-z0-9+/=]{8,}['"]`
|
|
- **OWASP**: A04
|
|
|
|
```typescript
|
|
// BAD
|
|
const API_KEY = 'sk_live_abc123def456';
|
|
|
|
// GOOD
|
|
const API_KEY = process.env.API_KEY;
|
|
```
|
|
|
|
### S2: .env Committed to Git
|
|
|
|
- **Severity**: CRITICAL
|
|
- **Detection**: `git ls-files .env` (should return empty)
|
|
- **OWASP**: A04
|
|
|
|
```gitignore
|
|
# .gitignore
|
|
.env
|
|
.env.local
|
|
.env.*.local
|
|
*.pem
|
|
*.key
|
|
```
|
|
|
|
### S3: Server Secrets Exposed to Client
|
|
|
|
- **Severity**: CRITICAL
|
|
- **Detection**: `NEXT_PUBLIC_.*(?:SECRET|PRIVATE|PASSWORD|KEY(?!.*PUBLIC))`
|
|
- **OWASP**: A02
|
|
|
|
```bash
|
|
# BAD
|
|
NEXT_PUBLIC_DATABASE_URL=postgresql://...
|
|
|
|
# GOOD
|
|
DATABASE_URL=postgresql://...
|
|
NEXT_PUBLIC_API_URL=https://api.example.com
|
|
```
|
|
|
|
Angular: do not put secrets in `environment.ts` files bundled into the client.
|
|
|
|
### S4: Default Credentials in Config
|
|
|
|
- **Severity**: CRITICAL
|
|
- **Detection**: `(?:admin|root|default|test).*(?:password|pass|pwd)\s*[:=]\s*['"](?:admin|root|password|1234|test)`
|
|
- **OWASP**: A02
|
|
|
|
Use environment variables with validation (zod schema).
|
|
|
|
### S5: Secrets in CI/CD Pipeline Logs
|
|
|
|
- **Severity**: IMPORTANT
|
|
- **Detection**: `(?:echo|console\.log|print).*(?:\$SECRET|\$TOKEN|\$PASSWORD|process\.env)`
|
|
- **OWASP**: A09
|
|
|
|
Use masked secrets in CI. Never echo environment variables containing secrets.
|
|
|
|
### S6: Sensitive Data in Error Responses / Stack Traces
|
|
|
|
- **Severity**: IMPORTANT
|
|
- **Detection**: `(?:stack|trace|query|sql).*(?:res\.json|res\.send|c\.JSON)`
|
|
- **OWASP**: A10
|
|
|
|
```typescript
|
|
// GOOD — generic error to client, details only in logs
|
|
app.use((err, req, res, _next) => {
|
|
logger.error({ err, path: req.path, method: req.method });
|
|
const isDev = process.env.NODE_ENV === 'development';
|
|
res.status(500).json({
|
|
error: 'Internal Server Error',
|
|
...(isDev && { message: err.message }),
|
|
});
|
|
});
|
|
```
|
|
|
|
---
|
|
|
|
## Headers Anti-Patterns (H1-H8)
|
|
|
|
### H1: Missing Content-Security-Policy
|
|
|
|
- **Severity**: IMPORTANT
|
|
- **Detection**: Absence of `Content-Security-Policy` header
|
|
- **OWASP**: A02
|
|
|
|
### H2: CSP with unsafe-inline and unsafe-eval
|
|
|
|
- **Severity**: IMPORTANT
|
|
- **Detection**: `Content-Security-Policy.*(?:'unsafe-inline'|'unsafe-eval')`
|
|
- **OWASP**: A02
|
|
|
|
Use nonce-based CSP: `script-src 'self' 'nonce-{SERVER_GENERATED}'`
|
|
|
|
### H3: Missing Strict-Transport-Security
|
|
|
|
- **Severity**: IMPORTANT
|
|
- **Detection**: Absence of `Strict-Transport-Security` header
|
|
- **OWASP**: A02
|
|
|
|
Value: `max-age=31536000; includeSubDomains; preload`
|
|
|
|
### H4: Missing X-Content-Type-Options
|
|
|
|
- **Severity**: IMPORTANT
|
|
- **Detection**: Absence of `X-Content-Type-Options: nosniff`
|
|
- **OWASP**: A02
|
|
|
|
### H5: Missing X-Frame-Options
|
|
|
|
- **Severity**: IMPORTANT
|
|
- **Detection**: Absence of `X-Frame-Options` header
|
|
- **OWASP**: A02
|
|
|
|
Value: `DENY`. Also set `Content-Security-Policy: frame-ancestors 'none'`.
|
|
|
|
### H6: Permissive Referrer-Policy
|
|
|
|
- **Severity**: SUGGESTION
|
|
- **Detection**: `Referrer-Policy.*(?:unsafe-url|no-referrer-when-downgrade)`
|
|
- **OWASP**: A02
|
|
|
|
Use: `strict-origin-when-cross-origin`
|
|
|
|
### H7: Missing Permissions-Policy
|
|
|
|
- **Severity**: SUGGESTION
|
|
- **Detection**: Absence of `Permissions-Policy` header
|
|
- **OWASP**: A02
|
|
|
|
Value: `camera=(), microphone=(), geolocation=(), payment=()`
|
|
|
|
### H8: CORS Wildcard with Credentials
|
|
|
|
- **Severity**: CRITICAL
|
|
- **Detection**: `(?:cors|Access-Control-Allow-Origin).*\*`
|
|
- **OWASP**: A02
|
|
|
|
```typescript
|
|
// GOOD
|
|
app.use(cors({
|
|
origin: ['https://app.example.com', 'https://staging.example.com'],
|
|
credentials: true,
|
|
}));
|
|
```
|
|
|
|
---
|
|
|
|
## Frontend Anti-Patterns (FE1-FE8)
|
|
|
|
### FE1: Unsanitized HTML Rendering
|
|
|
|
- **Severity**: CRITICAL
|
|
- **Detection**: `(?:innerHTML|v-html|dangerouslySetInner)` without DOMPurify
|
|
- **OWASP**: A05
|
|
|
|
Always sanitize with DOMPurify before rendering user-controlled HTML. See I4.
|
|
|
|
### FE2: Dynamic Code Evaluation with User Input
|
|
|
|
- **Severity**: CRITICAL
|
|
- **Detection**: `eval\s*\(`
|
|
- **OWASP**: A05
|
|
|
|
Use structured data parsers (JSON.parse) instead.
|
|
|
|
### FE3: postMessage Without Origin Validation
|
|
|
|
- **Severity**: IMPORTANT
|
|
- **Detection**: `addEventListener\s*\(\s*['"]message['"].*(?!.*origin)`
|
|
- **OWASP**: A01
|
|
|
|
```typescript
|
|
window.addEventListener('message', (event) => {
|
|
if (event.origin !== 'https://trusted.example.com') return;
|
|
processData(event.data);
|
|
});
|
|
```
|
|
|
|
### FE4: Prototype Pollution
|
|
|
|
- **Severity**: IMPORTANT
|
|
- **Detection**: `(?:__proto__|constructor\.prototype|Object\.assign)\s*.*(?:req\.|body\.|query\.)`
|
|
- **OWASP**: A05
|
|
|
|
Validate and filter keys from user input before merging into objects.
|
|
|
|
### FE5: Open Redirect
|
|
|
|
- **Severity**: IMPORTANT
|
|
- **Detection**: `(?:window\.location|location\.href|router\.push)\s*=\s*(?:req\.|params\.|query\.)`
|
|
- **OWASP**: A01
|
|
|
|
```typescript
|
|
// GOOD — relative paths only
|
|
const redirect = new URLSearchParams(window.location.search).get('redirect');
|
|
if (redirect?.startsWith('/') && !redirect.startsWith('//')) {
|
|
window.location.href = redirect;
|
|
}
|
|
```
|
|
|
|
### FE6: Sensitive Data in localStorage
|
|
|
|
- **Severity**: IMPORTANT
|
|
- **Detection**: `localStorage\.setItem\(.*(?:token|session|credit|ssn|password)`
|
|
- **OWASP**: A07
|
|
|
|
Use httpOnly cookies for tokens.
|
|
|
|
### FE7: Missing CSRF Token
|
|
|
|
- **Severity**: IMPORTANT
|
|
- **Detection**: POST/PUT/DELETE forms without CSRF token or SameSite cookie
|
|
- **OWASP**: A01
|
|
|
|
Use double-submit cookie or synchronizer token. Next.js Server Actions have built-in CSRF via Origin header.
|
|
|
|
### FE8: Client-Only Input Validation
|
|
|
|
- **Severity**: IMPORTANT
|
|
- **Detection**: Form validation only in frontend
|
|
- **OWASP**: A05
|
|
|
|
ALWAYS validate on server too. Use zod, joi, or class-validator.
|
|
|
|
---
|
|
|
|
## Dependencies Anti-Patterns (D1-D5)
|
|
|
|
### D1: Known Vulnerable Dependency
|
|
|
|
- **Severity**: CRITICAL
|
|
- **Detection**: `npm audit --audit-level=high` exits non-zero
|
|
- **OWASP**: A03
|
|
|
|
### D2: Lockfile Out of Sync
|
|
|
|
- **Severity**: IMPORTANT
|
|
- **Detection**: `npm ci` fails
|
|
- **OWASP**: A08
|
|
|
|
### D3: Typosquatting Risk
|
|
|
|
- **Severity**: IMPORTANT
|
|
- **Detection**: Manual review of new dependency names
|
|
- **OWASP**: A03
|
|
|
|
### D4: Postinstall Scripts in New Dependency
|
|
|
|
- **Severity**: IMPORTANT
|
|
- **Detection**: `"postinstall"` in new dependency's package.json
|
|
- **OWASP**: A03
|
|
|
|
### D5: Unpinned Versions in Production
|
|
|
|
- **Severity**: SUGGESTION
|
|
- **Detection**: `":\s*["']\*["']|":\s*["']latest["']`
|
|
- **OWASP**: A03
|
|
|
|
---
|
|
|
|
## API Anti-Patterns (AP1-AP6)
|
|
|
|
### AP1: New Endpoint Without Rate Limiting
|
|
|
|
- **Severity**: IMPORTANT
|
|
- **OWASP**: A05
|
|
|
|
```typescript
|
|
// BAD — new endpoint shipped without a limiter
|
|
app.post('/api/export', exportHandler);
|
|
|
|
// GOOD — Express: per-route limiter sized to the endpoint's cost
|
|
import rateLimit from 'express-rate-limit';
|
|
const exportLimiter = rateLimit({ windowMs: 60 * 1000, max: 10 });
|
|
app.post('/api/export', exportLimiter, exportHandler);
|
|
|
|
// GOOD — Next.js route handler / serverless or edge: use a store shared across instances
|
|
// (e.g. rate-limit-redis with Express, or @upstash/ratelimit as shown)
|
|
import { Ratelimit } from '@upstash/ratelimit';
|
|
import { Redis } from '@upstash/redis';
|
|
const limiter = new Ratelimit({ redis: Redis.fromEnv(), limiter: Ratelimit.slidingWindow(10, '1 m') });
|
|
export async function POST(req: Request) {
|
|
const ip = req.headers.get('x-forwarded-for')?.split(',')[0]?.trim() ?? 'anonymous';
|
|
const { success } = await limiter.limit(ip);
|
|
if (!success) return new Response('Too Many Requests', { status: 429 });
|
|
// ...
|
|
}
|
|
```
|
|
|
|
### AP2: GraphQL Without Depth Limiting
|
|
|
|
- **Severity**: IMPORTANT
|
|
- **Detection**: `new ApolloServer` without depth/complexity limits
|
|
- **OWASP**: A05
|
|
|
|
```typescript
|
|
import depthLimit from 'graphql-depth-limit';
|
|
const server = new ApolloServer({
|
|
schema,
|
|
validationRules: [depthLimit(5)],
|
|
introspection: process.env.NODE_ENV !== 'production',
|
|
});
|
|
```
|
|
|
|
### AP3: File Upload Without Validation
|
|
|
|
- **Severity**: IMPORTANT
|
|
- **Detection**: `multer|formidable|busboy` without type/size checks
|
|
- **OWASP**: A05
|
|
|
|
```typescript
|
|
const upload = multer({
|
|
dest: 'uploads/',
|
|
limits: { fileSize: 5 * 1024 * 1024 },
|
|
fileFilter: (req, file, cb) => {
|
|
const allowed = ['image/jpeg', 'image/png', 'image/webp'];
|
|
cb(null, allowed.includes(file.mimetype));
|
|
},
|
|
});
|
|
```
|
|
|
|
### AP4: Webhook Without Signature Verification
|
|
|
|
- **Severity**: CRITICAL
|
|
- **OWASP**: A08
|
|
|
|
Always verify webhook signatures (Stripe, GitHub HMAC, etc.).
|
|
|
|
### AP5: API Exposing Internal Info
|
|
|
|
- **Severity**: IMPORTANT
|
|
- **Detection**: `(?:stack|trace|query|sql).*(?:res\.json|res\.send)`
|
|
- **OWASP**: A10
|
|
|
|
### AP6: Missing Request Body Size Limit
|
|
|
|
- **Severity**: IMPORTANT
|
|
- **Detection**: `express\.json\(\)` without `limit`
|
|
- **OWASP**: A05
|
|
|
|
```typescript
|
|
app.use(express.json({ limit: '100kb' }));
|
|
```
|
|
|
|
---
|
|
|
|
## AI/LLM Security Anti-Patterns (AI1-AI3)
|
|
|
|
### AI1: Prompt Injection via User Input
|
|
|
|
- **Severity**: CRITICAL
|
|
- **Detection**: User input concatenated into LLM prompts without sanitization
|
|
- **OWASP**: A05 (Injection)
|
|
|
|
```typescript
|
|
// BAD — user input directly in prompt
|
|
const response = await llm.complete(`Summarize this: ${userInput}`);
|
|
|
|
// GOOD — structured input with system/user message separation
|
|
const response = await llm.complete({
|
|
system: "You are a summarization assistant. Only summarize the provided text.",
|
|
user: userInput,
|
|
});
|
|
```
|
|
|
|
### AI2: LLM Output Used in SQL/Shell Without Sanitization
|
|
|
|
- **Severity**: CRITICAL
|
|
- **Detection**: LLM response passed to `db.query()`, `exec()`, or template literals without validation
|
|
- **OWASP**: A05 (Injection)
|
|
|
|
Never trust LLM output as safe. Treat it as untrusted user input — parameterize queries, escape shell arguments, sanitize HTML before rendering.
|
|
|
|
### AI3: Missing Output Validation from LLM Responses
|
|
|
|
- **Severity**: IMPORTANT
|
|
- **Detection**: LLM response rendered or executed without schema validation
|
|
- **OWASP**: A08 (Software or Data Integrity Failures)
|
|
|
|
Validate LLM output against expected schemas (Zod, JSON Schema) before using in application logic. Reject responses that don't match expected structure.
|
|
|
|
---
|
|
|
|
## Logging Anti-Patterns (L1-L4)
|
|
|
|
### L1: Security Events Not Logged
|
|
|
|
- **Severity**: IMPORTANT
|
|
- **OWASP**: A09
|
|
|
|
Log: auth failures, access denied, rate limit hits, input validation failures, password changes.
|
|
|
|
### L2: Sensitive Data in Logs
|
|
|
|
- **Severity**: CRITICAL
|
|
- **Detection**: `(?:log|logger)\.\w+\(.*(?:password|token|secret|ssn|credit)`
|
|
- **OWASP**: A09
|
|
|
|
```typescript
|
|
import pino from 'pino';
|
|
const logger = pino({ redact: ['req.headers.authorization', 'req.body.password'] });
|
|
```
|
|
|
|
### L3: Missing Trace IDs
|
|
|
|
- **Severity**: SUGGESTION
|
|
- **OWASP**: A09
|
|
|
|
### L4: Log Injection
|
|
|
|
- **Severity**: IMPORTANT
|
|
- **Detection**: `console\.log\(.*\+.*(?:req\.|user\.|body\.)`
|
|
- **OWASP**: A09
|
|
|
|
Use structured logging (JSON, auto-escaped) instead of string concatenation.
|
|
|
|
---
|
|
|
|
## Framework-Specific: React / Next.js (RX1-RX4)
|
|
|
|
### RX1: Server Action Without Auth
|
|
|
|
- **Severity**: CRITICAL
|
|
- **Detection**: `'use server'` function without `auth()` or session check
|
|
- **OWASP**: A01
|
|
|
|
```typescript
|
|
'use server';
|
|
import { auth } from '@/auth';
|
|
export async function deleteUser(id: string) {
|
|
const session = await auth();
|
|
if (!session?.user || session.user.role !== 'admin') throw new Error('Unauthorized');
|
|
await db.user.delete({ where: { id } });
|
|
}
|
|
```
|
|
|
|
### RX2: process.env Without NEXT_PUBLIC_ in Client
|
|
|
|
- **Severity**: IMPORTANT
|
|
- **Detection**: `'use client'` file accessing `process.env` without `NEXT_PUBLIC_`
|
|
- **OWASP**: A02
|
|
|
|
### RX3: RSC Serialization Leaking Data
|
|
|
|
- **Severity**: IMPORTANT
|
|
- **OWASP**: A01
|
|
|
|
Pick only needed fields before passing DB objects to Client Components.
|
|
|
|
### RX4: middleware.ts Not Protecting API Routes
|
|
|
|
- **Severity**: IMPORTANT
|
|
- **Detection**: `config.matcher` not covering `/api/`
|
|
- **OWASP**: A01
|
|
|
|
---
|
|
|
|
## Framework-Specific: Angular (NG1-NG3)
|
|
|
|
### NG1: bypassSecurityTrustHtml with User Input
|
|
|
|
- **Severity**: CRITICAL
|
|
- **Detection**: `bypassSecurityTrust(?:Html|Script|Style|Url|ResourceUrl)`
|
|
- **OWASP**: A05
|
|
|
|
Sanitize with DOMPurify BEFORE calling bypassSecurityTrust.
|
|
|
|
### NG2: Template Expression Injection
|
|
|
|
- **Severity**: IMPORTANT
|
|
- **OWASP**: A05
|
|
|
|
Do not use JitCompilerFactory with user-controlled templates.
|
|
|
|
### NG3: HttpInterceptor Not Attaching Auth
|
|
|
|
- **Severity**: IMPORTANT
|
|
- **OWASP**: A07
|
|
|
|
Use a centralized `HttpInterceptorFn` for auth tokens.
|
|
|
|
---
|
|
|
|
## Framework-Specific: Express (EX1-EX4)
|
|
|
|
### EX1: Missing helmet.js
|
|
|
|
- **Severity**: IMPORTANT
|
|
- **OWASP**: A02
|
|
|
|
```typescript
|
|
import helmet from 'helmet';
|
|
app.use(helmet());
|
|
app.disable('x-powered-by');
|
|
```
|
|
|
|
### EX2: express.json() Without Body Size Limit
|
|
|
|
- **Severity**: IMPORTANT
|
|
- **OWASP**: A05
|
|
|
|
```typescript
|
|
app.use(express.json({ limit: '100kb' }));
|
|
```
|
|
|
|
### EX3: Cookie Without Secure Flags
|
|
|
|
- **Severity**: IMPORTANT
|
|
- **OWASP**: A07
|
|
|
|
```typescript
|
|
res.cookie('session', value, {
|
|
httpOnly: true, secure: true, sameSite: 'strict', maxAge: 3600000, path: '/',
|
|
});
|
|
```
|
|
|
|
### EX4: Error Handler Exposing Stack Trace
|
|
|
|
- **Severity**: IMPORTANT
|
|
- **OWASP**: A10
|
|
|
|
Only expose error details in development mode.
|
|
|
|
---
|
|
|
|
## Framework-Specific: Go (GO1-GO3)
|
|
|
|
### GO1: math/rand for Security Operations
|
|
|
|
- **Severity**: CRITICAL
|
|
- **Detection**: `math/rand` import in security-related files
|
|
- **OWASP**: A04
|
|
|
|
Use `crypto/rand` for cryptographically secure random values.
|
|
|
|
### GO2: TLS InsecureSkipVerify
|
|
|
|
- **Severity**: CRITICAL
|
|
- **Detection**: `InsecureSkipVerify:\s*true`
|
|
- **OWASP**: A04
|
|
|
|
Use system CA pool (default) instead.
|
|
|
|
### GO3: String Interpolation in SQL
|
|
|
|
- **Severity**: CRITICAL
|
|
- **Detection**: `fmt\.Sprintf\s*\(.*(?:SELECT|INSERT|UPDATE|DELETE|FROM|WHERE)`
|
|
- **OWASP**: A05
|
|
|
|
```go
|
|
// GOOD — parameterized
|
|
db.Where("id = ?", userID).Find(&user)
|
|
```
|
|
|
|
---
|
|
|
|
## Security Headers Template
|
|
|
|
### helmet.js (Express)
|
|
|
|
```typescript
|
|
import helmet from 'helmet';
|
|
|
|
app.use(helmet({
|
|
contentSecurityPolicy: {
|
|
directives: {
|
|
defaultSrc: ["'self'"],
|
|
scriptSrc: ["'self'"],
|
|
styleSrc: ["'self'"],
|
|
imgSrc: ["'self'", "data:", "https:"],
|
|
fontSrc: ["'self'"],
|
|
connectSrc: ["'self'"],
|
|
frameAncestors: ["'none'"],
|
|
objectSrc: ["'none'"],
|
|
baseUri: ["'self'"],
|
|
formAction: ["'self'"],
|
|
upgradeInsecureRequests: [],
|
|
},
|
|
},
|
|
hsts: { maxAge: 31536000, includeSubDomains: true, preload: true },
|
|
frameguard: { action: 'deny' },
|
|
referrerPolicy: { policy: 'strict-origin-when-cross-origin' },
|
|
crossOriginOpenerPolicy: { policy: 'same-origin' },
|
|
crossOriginResourcePolicy: { policy: 'same-origin' },
|
|
}));
|
|
app.disable('x-powered-by');
|
|
```
|
|
|
|
---
|
|
|
|
## JWT Validation Checklist
|
|
|
|
1. Verify signature with expected algorithm — reject `alg: none`
|
|
2. Enforce algorithm: `algorithms: ['RS256']` or `['ES256']`
|
|
3. Check `exp` — reject expired tokens
|
|
4. Check `iat` — reject tokens issued too far in the past
|
|
5. Check `aud` — reject tokens not intended for this service
|
|
6. Check `iss` — reject tokens from unknown issuers
|
|
7. Store in httpOnly cookie — not localStorage
|
|
8. Use short-lived access tokens (15 min) + refresh token rotation
|
|
9. Rotate signing keys periodically
|
|
|
|
---
|
|
|
|
## Secure Cookie Flags
|
|
|
|
```
|
|
Set-Cookie: session=value; HttpOnly; Secure; SameSite=Strict; Path=/; Max-Age=3600
|
|
```
|
|
|
|
| Flag | Purpose | When to use |
|
|
|------|---------|-------------|
|
|
| `HttpOnly` | Not accessible via JavaScript (prevents XSS token theft) | Always |
|
|
| `Secure` | Only sent over HTTPS | Always |
|
|
| `SameSite=Strict` | Only sent on same-site requests (strongest CSRF) | Auth/session cookies |
|
|
| `SameSite=Lax` | Sent on top-level navigations (moderate CSRF) | Cookies that need cross-site top-level nav (e.g., OAuth return) |
|
|
| `Path=/` | Limit cookie scope | Always |
|
|
| `Max-Age` | Explicit expiration (prefer over `Expires`) | Always |
|
|
|
|
---
|
|
|
|
## Security Checklist
|
|
|
|
### Authentication and Sessions
|
|
- [ ] Passwords hashed with Argon2id or bcrypt (cost >= 12)
|
|
- [ ] JWT signed with RS256/ES256, algorithm enforced on verify
|
|
- [ ] Access tokens expire in <= 15 minutes
|
|
- [ ] Refresh tokens: one-time use, rotated, stored in httpOnly cookie
|
|
- [ ] Rate limiting on login, registration, and password reset
|
|
- [ ] Session regenerated after authentication
|
|
- [ ] MFA available for privileged accounts
|
|
|
|
### Authorization
|
|
- [ ] Every API endpoint has auth middleware
|
|
- [ ] Ownership checks on all resource access (prevent IDOR)
|
|
- [ ] Server-side authorization (frontend guards are UX only)
|
|
- [ ] Mass assignment prevented (explicit field selection)
|
|
- [ ] Re-authentication required for sensitive operations
|
|
|
|
### Input and Output
|
|
- [ ] All user input validated server-side (zod/joi/class-validator)
|
|
- [ ] Parameterized queries for all database operations
|
|
- [ ] HTML output sanitized (DOMPurify) when rendering user content
|
|
- [ ] Error responses do not expose stack traces in production
|
|
|
|
### Secrets
|
|
- [ ] No hardcoded secrets in source code
|
|
- [ ] `.env` files in `.gitignore`
|
|
- [ ] Server secrets not exposed to client (no NEXT_PUBLIC_ on secrets)
|
|
- [ ] Environment variables validated at startup
|
|
|
|
### Headers
|
|
- [ ] Content-Security-Policy configured (nonce-based preferred)
|
|
- [ ] Strict-Transport-Security with preload
|
|
- [ ] X-Content-Type-Options: nosniff
|
|
- [ ] X-Frame-Options: DENY
|
|
- [ ] Referrer-Policy: strict-origin-when-cross-origin
|
|
- [ ] Permissions-Policy restricting unused APIs
|
|
- [ ] CORS restricted to known origins
|
|
|
|
### Dependencies
|
|
- [ ] `npm audit` (or equivalent) passing in CI
|
|
- [ ] Lockfile committed and verified with `npm ci`
|
|
- [ ] New dependencies reviewed for typosquatting and postinstall scripts
|
|
- [ ] No wildcard or "latest" versions in production
|
|
|
|
### Logging
|
|
- [ ] Security events logged (auth failures, access denied, rate limits)
|
|
- [ ] No sensitive data in logs (passwords, tokens, PII)
|
|
- [ ] Structured logging with correlation IDs
|
|
- [ ] Alerts configured for anomalous patterns
|