secure-coding-rules 2.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +168 -0
- package/bin/cli.js +8 -0
- package/package.json +64 -0
- package/src/adapters/agents.js +54 -0
- package/src/adapters/claude.js +88 -0
- package/src/adapters/copilot.js +82 -0
- package/src/adapters/cursor.js +68 -0
- package/src/adapters/windsurf.js +57 -0
- package/src/index.js +177 -0
- package/src/loader.js +85 -0
- package/src/prompts.js +307 -0
- package/src/templates/core/access-control.md +112 -0
- package/src/templates/core/authentication.md +138 -0
- package/src/templates/core/cryptographic.md +114 -0
- package/src/templates/core/data-integrity.md +144 -0
- package/src/templates/core/error-handling.md +180 -0
- package/src/templates/core/injection.md +125 -0
- package/src/templates/core/logging-alerting.md +156 -0
- package/src/templates/core/secure-design.md +117 -0
- package/src/templates/core/security-config.md +118 -0
- package/src/templates/core/supply-chain.md +127 -0
- package/src/templates/frontend/csp.md +146 -0
- package/src/templates/frontend/csrf-protection.md +151 -0
- package/src/templates/frontend/secure-state.md +167 -0
- package/src/templates/frontend/xss-prevention.md +125 -0
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
# Authentication Security Rules
|
|
2
|
+
|
|
3
|
+
> OWASP Top 10 2025 - A07: Authentication Failures
|
|
4
|
+
|
|
5
|
+
## Rules
|
|
6
|
+
|
|
7
|
+
### 1. Implement Multi-Factor Authentication
|
|
8
|
+
- **DO**: Require MFA for all privileged accounts and offer it for all users. Use TOTP, WebAuthn, or push-based verification.
|
|
9
|
+
- **DON'T**: Rely solely on passwords for authentication, especially for admin and financial operations.
|
|
10
|
+
- **WHY**: Passwords alone are frequently compromised through phishing, credential stuffing, or data breaches.
|
|
11
|
+
|
|
12
|
+
### 2. Enforce Strong Password Policies
|
|
13
|
+
- **DO**: Require a minimum of 8 characters. Check against breached password databases (e.g., Have I Been Pwned API). Allow long passphrases.
|
|
14
|
+
- **DON'T**: Impose arbitrary complexity rules (uppercase + number + symbol) that lead to predictable patterns, or cap password length below 64 characters.
|
|
15
|
+
- **WHY**: NIST guidelines (SP 800-63B) recommend length over complexity. Breached password checks are more effective than complexity rules.
|
|
16
|
+
|
|
17
|
+
### 3. Protect Against Credential Stuffing and Brute Force
|
|
18
|
+
- **DO**: Implement rate limiting, account lockout with exponential backoff, and CAPTCHA after failed attempts.
|
|
19
|
+
- **DON'T**: Allow unlimited login attempts or reveal whether the username or password was incorrect.
|
|
20
|
+
- **WHY**: Credential stuffing uses leaked credentials from other breaches. Rate limiting and generic error messages slow automated attacks.
|
|
21
|
+
|
|
22
|
+
### 4. Secure Session Management
|
|
23
|
+
- **DO**: Generate random session IDs with high entropy. Set cookies with `HttpOnly`, `Secure`, `SameSite=Strict`, and appropriate expiry.
|
|
24
|
+
- **DON'T**: Store session tokens in `localStorage` or expose them in URLs. Never accept session IDs from query parameters.
|
|
25
|
+
- **WHY**: Predictable or exposed session tokens allow session hijacking. Secure cookie attributes prevent XSS and CSRF-based theft.
|
|
26
|
+
|
|
27
|
+
### 5. Implement Secure Password Reset
|
|
28
|
+
- **DO**: Use time-limited, single-use tokens for password reset. Send reset links only to verified email addresses. Invalidate all sessions on password change.
|
|
29
|
+
- **DON'T**: Use knowledge-based questions, send plaintext passwords, or allow token reuse.
|
|
30
|
+
- **WHY**: Weak password reset flows are as dangerous as weak passwords. They are a common target for account takeover.
|
|
31
|
+
|
|
32
|
+
### 6. Validate JWTs Properly
|
|
33
|
+
- **DO**: Verify signature, issuer (`iss`), audience (`aud`), and expiration (`exp`). Use asymmetric algorithms (RS256, ES256) for distributed systems.
|
|
34
|
+
- **DON'T**: Use `alg: "none"`, accept tokens without signature verification, or store sensitive data in JWT payloads.
|
|
35
|
+
- **WHY**: JWT misuse (algorithm confusion, missing validation) leads to authentication bypass and privilege escalation.
|
|
36
|
+
|
|
37
|
+
### 7. Implement Secure OAuth/OIDC Flows
|
|
38
|
+
- **DO**: Use Authorization Code flow with PKCE. Validate the `state` parameter and token claims. Store tokens securely.
|
|
39
|
+
- **DON'T**: Use Implicit flow for SPAs. Skip `state` validation or accept tokens from untrusted sources.
|
|
40
|
+
- **WHY**: OAuth misconfiguration enables token theft, CSRF, and account takeover through redirect manipulation.
|
|
41
|
+
|
|
42
|
+
## Code Examples
|
|
43
|
+
|
|
44
|
+
### Bad Practice
|
|
45
|
+
```javascript
|
|
46
|
+
// Revealing whether user exists
|
|
47
|
+
app.post("/api/login", async (req, res) => {
|
|
48
|
+
const user = await db.findUser(req.body.email);
|
|
49
|
+
if (!user) return res.status(401).json({ error: "User not found" }); // Info leak
|
|
50
|
+
if (!checkPassword(req.body.password, user.password)) {
|
|
51
|
+
return res.status(401).json({ error: "Wrong password" }); // Info leak
|
|
52
|
+
}
|
|
53
|
+
res.json({ token: jwt.sign({ id: user.id }, SECRET) });
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
// Insecure JWT validation
|
|
57
|
+
const payload = jwt.decode(token); // decode without verify!
|
|
58
|
+
if (payload.role === "admin") grantAdminAccess();
|
|
59
|
+
|
|
60
|
+
// Session token in localStorage
|
|
61
|
+
localStorage.setItem("token", response.data.token); // Accessible via XSS
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
### Good Practice
|
|
65
|
+
```javascript
|
|
66
|
+
import jwt from "jsonwebtoken";
|
|
67
|
+
import { rateLimit } from "express-rate-limit";
|
|
68
|
+
|
|
69
|
+
// Rate-limited login with generic error messages
|
|
70
|
+
const loginLimiter = rateLimit({
|
|
71
|
+
windowMs: 15 * 60 * 1000,
|
|
72
|
+
max: 10,
|
|
73
|
+
skipSuccessfulRequests: true,
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
app.post("/api/login", loginLimiter, async (req, res) => {
|
|
77
|
+
const { email, password } = req.body;
|
|
78
|
+
const user = await db.findUser(email);
|
|
79
|
+
|
|
80
|
+
// Constant-time check - same response whether user exists or not
|
|
81
|
+
const isValid = user ? await verifyPassword(password, user.passwordHash) : false;
|
|
82
|
+
if (!isValid) {
|
|
83
|
+
return res.status(401).json({ error: "Invalid credentials" }); // Generic message
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
// Check for MFA
|
|
87
|
+
if (user.mfaEnabled) {
|
|
88
|
+
const mfaToken = crypto.randomBytes(32).toString("hex");
|
|
89
|
+
await storeMfaChallenge(user.id, mfaToken, Date.now() + 300_000);
|
|
90
|
+
return res.json({ requiresMFA: true, mfaToken });
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
setSessionCookie(res, user);
|
|
94
|
+
res.json({ success: true });
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
// Secure session cookie
|
|
98
|
+
function setSessionCookie(res, user) {
|
|
99
|
+
const sessionId = crypto.randomUUID();
|
|
100
|
+
res.cookie("session", sessionId, {
|
|
101
|
+
httpOnly: true,
|
|
102
|
+
secure: true,
|
|
103
|
+
sameSite: "strict",
|
|
104
|
+
maxAge: 3600_000, // 1 hour
|
|
105
|
+
path: "/",
|
|
106
|
+
});
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
// Proper JWT verification
|
|
110
|
+
function verifyToken(token) {
|
|
111
|
+
return jwt.verify(token, PUBLIC_KEY, {
|
|
112
|
+
algorithms: ["ES256"], // Explicit algorithm
|
|
113
|
+
issuer: "https://auth.example.com",
|
|
114
|
+
audience: "https://api.example.com",
|
|
115
|
+
clockTolerance: 30,
|
|
116
|
+
});
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
// Secure password reset
|
|
120
|
+
app.post("/api/password-reset", async (req, res) => {
|
|
121
|
+
const token = crypto.randomBytes(32).toString("hex");
|
|
122
|
+
const hashedToken = crypto.createHash("sha256").update(token).digest("hex");
|
|
123
|
+
await db.storeResetToken(req.body.email, hashedToken, Date.now() + 3600_000);
|
|
124
|
+
await sendResetEmail(req.body.email, token); // Send unhashed token
|
|
125
|
+
res.json({ message: "If the email exists, a reset link has been sent" }); // Generic
|
|
126
|
+
});
|
|
127
|
+
```
|
|
128
|
+
|
|
129
|
+
## Quick Checklist
|
|
130
|
+
- [ ] MFA available and enforced for privileged accounts
|
|
131
|
+
- [ ] Passwords checked against breached password databases
|
|
132
|
+
- [ ] Login rate limiting and account lockout implemented
|
|
133
|
+
- [ ] Generic error messages for authentication failures (no user enumeration)
|
|
134
|
+
- [ ] Session cookies use `HttpOnly`, `Secure`, `SameSite=Strict`
|
|
135
|
+
- [ ] JWTs verified with explicit algorithm, issuer, and audience
|
|
136
|
+
- [ ] Password reset uses time-limited, single-use tokens
|
|
137
|
+
- [ ] All sessions invalidated on password change
|
|
138
|
+
- [ ] OAuth flows use PKCE and validate `state` parameter
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
# Cryptographic Failures Security Rules
|
|
2
|
+
|
|
3
|
+
> OWASP Top 10 2025 - A04: Cryptographic Failures
|
|
4
|
+
|
|
5
|
+
## Rules
|
|
6
|
+
|
|
7
|
+
### 1. Use Strong, Modern Encryption Algorithms
|
|
8
|
+
- **DO**: Use AES-256-GCM for symmetric encryption and RSA-OAEP or ECDH for asymmetric encryption. Use the Web Crypto API or `node:crypto` module.
|
|
9
|
+
- **DON'T**: Use deprecated algorithms like DES, 3DES, RC4, MD5, or SHA-1 for security purposes.
|
|
10
|
+
- **WHY**: Weak algorithms are vulnerable to known attacks and can be broken with modern hardware.
|
|
11
|
+
|
|
12
|
+
### 2. Never Hardcode Secrets or Keys
|
|
13
|
+
- **DO**: Store keys in environment variables, secret managers (AWS Secrets Manager, HashiCorp Vault), or hardware security modules.
|
|
14
|
+
- **DON'T**: Embed encryption keys, API keys, or passwords directly in source code or config files.
|
|
15
|
+
- **WHY**: Hardcoded secrets in source code are easily extracted from repositories and compiled artifacts.
|
|
16
|
+
|
|
17
|
+
### 3. Hash Passwords with Adaptive Algorithms
|
|
18
|
+
- **DO**: Use bcrypt, scrypt, or Argon2id for password hashing with appropriate work factors.
|
|
19
|
+
- **DON'T**: Use plain hashing (SHA-256, MD5) or unsalted hashes for passwords.
|
|
20
|
+
- **WHY**: Adaptive hashing algorithms are designed to resist GPU-based brute force and rainbow table attacks.
|
|
21
|
+
|
|
22
|
+
### 4. Generate Cryptographically Secure Random Values
|
|
23
|
+
- **DO**: Use `crypto.randomBytes()`, `crypto.randomUUID()`, or `crypto.getRandomValues()` for tokens, IDs, and nonces.
|
|
24
|
+
- **DON'T**: Use `Math.random()` for any security-sensitive value (tokens, session IDs, OTPs).
|
|
25
|
+
- **WHY**: `Math.random()` is predictable and not cryptographically secure. Attackers can predict its output.
|
|
26
|
+
|
|
27
|
+
### 5. Encrypt Sensitive Data at Rest and in Transit
|
|
28
|
+
- **DO**: Use TLS 1.2+ for data in transit. Encrypt PII, financial data, and health records at rest with field-level encryption where appropriate.
|
|
29
|
+
- **DON'T**: Store sensitive data in plaintext in databases, logs, or local storage.
|
|
30
|
+
- **WHY**: Data breaches expose plaintext data. Encryption limits the impact of unauthorized access.
|
|
31
|
+
|
|
32
|
+
### 6. Use Authenticated Encryption
|
|
33
|
+
- **DO**: Use AEAD modes like AES-GCM that provide both confidentiality and integrity.
|
|
34
|
+
- **DON'T**: Use ECB mode or CBC without HMAC. Never implement custom encryption schemes.
|
|
35
|
+
- **WHY**: Unauthenticated encryption is vulnerable to padding oracle and ciphertext manipulation attacks.
|
|
36
|
+
|
|
37
|
+
### 7. Manage Key Rotation and Lifecycle
|
|
38
|
+
- **DO**: Implement key rotation policies. Support decryption with old keys while encrypting with current keys.
|
|
39
|
+
- **DON'T**: Use the same encryption key indefinitely without rotation.
|
|
40
|
+
- **WHY**: Key rotation limits the impact of key compromise and meets compliance requirements.
|
|
41
|
+
|
|
42
|
+
## Code Examples
|
|
43
|
+
|
|
44
|
+
### Bad Practice
|
|
45
|
+
```javascript
|
|
46
|
+
import crypto from "node:crypto";
|
|
47
|
+
|
|
48
|
+
// Using Math.random for tokens
|
|
49
|
+
const resetToken = Math.random().toString(36).substring(2);
|
|
50
|
+
|
|
51
|
+
// MD5 for password hashing (no salt, fast hash)
|
|
52
|
+
const hashedPassword = crypto.createHash("md5").update(password).digest("hex");
|
|
53
|
+
|
|
54
|
+
// Hardcoded encryption key
|
|
55
|
+
const ENCRYPTION_KEY = "my-super-secret-key-12345678";
|
|
56
|
+
|
|
57
|
+
// ECB mode (insecure - identical blocks produce identical ciphertext)
|
|
58
|
+
const cipher = crypto.createCipheriv("aes-256-ecb", key, null);
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
### Good Practice
|
|
62
|
+
```javascript
|
|
63
|
+
import crypto from "node:crypto";
|
|
64
|
+
import { hash, verify } from "@node-rs/argon2"; // or bcrypt
|
|
65
|
+
|
|
66
|
+
// Cryptographically secure random token
|
|
67
|
+
const resetToken = crypto.randomBytes(32).toString("hex");
|
|
68
|
+
const sessionId = crypto.randomUUID();
|
|
69
|
+
|
|
70
|
+
// Argon2id password hashing
|
|
71
|
+
async function hashPassword(password) {
|
|
72
|
+
return hash(password, {
|
|
73
|
+
memoryCost: 65536, // 64 MB
|
|
74
|
+
timeCost: 3,
|
|
75
|
+
parallelism: 4,
|
|
76
|
+
});
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
async function verifyPassword(password, hashedPassword) {
|
|
80
|
+
return verify(hashedPassword, password);
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
// AES-256-GCM authenticated encryption
|
|
84
|
+
function encrypt(plaintext, key) {
|
|
85
|
+
const iv = crypto.randomBytes(12); // 96-bit IV for GCM
|
|
86
|
+
const cipher = crypto.createCipheriv("aes-256-gcm", key, iv);
|
|
87
|
+
const encrypted = Buffer.concat([cipher.update(plaintext, "utf8"), cipher.final()]);
|
|
88
|
+
const authTag = cipher.getAuthTag();
|
|
89
|
+
return Buffer.concat([iv, authTag, encrypted]).toString("base64");
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
function decrypt(ciphertext, key) {
|
|
93
|
+
const data = Buffer.from(ciphertext, "base64");
|
|
94
|
+
const iv = data.subarray(0, 12);
|
|
95
|
+
const authTag = data.subarray(12, 28);
|
|
96
|
+
const encrypted = data.subarray(28);
|
|
97
|
+
const decipher = crypto.createDecipheriv("aes-256-gcm", key, iv);
|
|
98
|
+
decipher.setAuthTag(authTag);
|
|
99
|
+
return Buffer.concat([decipher.update(encrypted), decipher.final()]).toString("utf8");
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
// Key from environment / secret manager
|
|
103
|
+
const encryptionKey = Buffer.from(process.env.ENCRYPTION_KEY, "base64");
|
|
104
|
+
```
|
|
105
|
+
|
|
106
|
+
## Quick Checklist
|
|
107
|
+
- [ ] Only modern algorithms used (AES-256-GCM, SHA-256+, Argon2id/bcrypt)
|
|
108
|
+
- [ ] No hardcoded keys or secrets in source code
|
|
109
|
+
- [ ] Passwords hashed with Argon2id, bcrypt, or scrypt
|
|
110
|
+
- [ ] All random values use `crypto.randomBytes()` or `crypto.getRandomValues()`
|
|
111
|
+
- [ ] Sensitive data encrypted at rest and in transit (TLS 1.2+)
|
|
112
|
+
- [ ] Authenticated encryption mode (GCM) used for symmetric encryption
|
|
113
|
+
- [ ] Key rotation policy defined and implemented
|
|
114
|
+
- [ ] No sensitive data in logs, URLs, or client-side storage
|
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
# Software and Data Integrity Security Rules
|
|
2
|
+
|
|
3
|
+
> OWASP Top 10 2025 - A08: Software or Data Integrity Failures
|
|
4
|
+
|
|
5
|
+
## Rules
|
|
6
|
+
|
|
7
|
+
### 1. Verify CI/CD Pipeline Integrity
|
|
8
|
+
- **DO**: Sign commits and artifacts. Use protected branches, required reviews, and immutable build environments.
|
|
9
|
+
- **DON'T**: Allow unapproved changes to CI/CD configuration or build scripts without review.
|
|
10
|
+
- **WHY**: Compromised pipelines can inject malicious code into production builds, affecting all users.
|
|
11
|
+
|
|
12
|
+
### 2. Validate Data Integrity on Deserialization
|
|
13
|
+
- **DO**: Validate and sanitize all deserialized data against strict schemas. Use safe serialization formats (JSON with schema validation).
|
|
14
|
+
- **DON'T**: Deserialize untrusted data without validation, especially with `eval()`, `Function()`, or `node:vm`.
|
|
15
|
+
- **WHY**: Insecure deserialization can lead to Remote Code Execution (RCE) or data tampering.
|
|
16
|
+
|
|
17
|
+
### 3. Implement Integrity Checks for Critical Data
|
|
18
|
+
- **DO**: Use HMAC or digital signatures to verify integrity of sensitive data (tokens, cookies, inter-service messages).
|
|
19
|
+
- **DON'T**: Trust data from cookies, hidden form fields, or client-side storage without integrity verification.
|
|
20
|
+
- **WHY**: Client-side data can be tampered with. Signed data ensures it hasn't been modified.
|
|
21
|
+
|
|
22
|
+
### 4. Secure Auto-Update Mechanisms
|
|
23
|
+
- **DO**: Verify digital signatures on all updates before applying them. Use TLS for update channels.
|
|
24
|
+
- **DON'T**: Download and execute updates without cryptographic verification.
|
|
25
|
+
- **WHY**: Unsigned updates can be replaced with malicious payloads through MITM attacks or compromised update servers.
|
|
26
|
+
|
|
27
|
+
### 5. Protect Database Migrations and Seeds
|
|
28
|
+
- **DO**: Review and version-control all database migrations. Use checksums to verify migration integrity.
|
|
29
|
+
- **DON'T**: Run auto-generated migrations in production without review or allow dynamic schema changes from user input.
|
|
30
|
+
- **WHY**: Malicious migrations can alter database structure, delete data, or create backdoor accounts.
|
|
31
|
+
|
|
32
|
+
### 6. Validate Webhook and API Payloads
|
|
33
|
+
- **DO**: Verify webhook signatures using HMAC. Validate the payload schema and source IP where possible.
|
|
34
|
+
- **DON'T**: Process webhook payloads without signature verification or trust arbitrary callback URLs.
|
|
35
|
+
- **WHY**: Unverified webhooks allow attackers to inject fake events (payment confirmations, deploy triggers).
|
|
36
|
+
|
|
37
|
+
## Code Examples
|
|
38
|
+
|
|
39
|
+
### Bad Practice
|
|
40
|
+
```javascript
|
|
41
|
+
// Deserializing untrusted data unsafely
|
|
42
|
+
const userData = eval(`(${req.body.data})`); // RCE vulnerability
|
|
43
|
+
|
|
44
|
+
// Trusting client-side data without integrity check
|
|
45
|
+
app.post("/api/checkout", (req, res) => {
|
|
46
|
+
const { price } = req.body; // Client can modify price
|
|
47
|
+
processPayment(price);
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
// Processing webhook without signature verification
|
|
51
|
+
app.post("/webhook/payment", (req, res) => {
|
|
52
|
+
const event = req.body;
|
|
53
|
+
if (event.type === "payment_success") {
|
|
54
|
+
fulfillOrder(event.orderId); // Could be forged
|
|
55
|
+
}
|
|
56
|
+
});
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
### Good Practice
|
|
60
|
+
```javascript
|
|
61
|
+
import crypto from "node:crypto";
|
|
62
|
+
import { z } from "zod";
|
|
63
|
+
|
|
64
|
+
// Safe deserialization with schema validation
|
|
65
|
+
const UserDataSchema = z.object({
|
|
66
|
+
name: z.string().max(100),
|
|
67
|
+
email: z.string().email(),
|
|
68
|
+
role: z.enum(["user", "editor"]),
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
app.post("/api/profile", authenticate, (req, res) => {
|
|
72
|
+
const userData = UserDataSchema.parse(JSON.parse(req.body.data));
|
|
73
|
+
updateProfile(req.user.id, userData);
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
// Signed data for client-side integrity
|
|
77
|
+
function signData(data, secret) {
|
|
78
|
+
const payload = JSON.stringify(data);
|
|
79
|
+
const signature = crypto
|
|
80
|
+
.createHmac("sha256", secret)
|
|
81
|
+
.update(payload)
|
|
82
|
+
.digest("hex");
|
|
83
|
+
return { payload, signature };
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
function verifyData(payload, signature, secret) {
|
|
87
|
+
const expected = crypto
|
|
88
|
+
.createHmac("sha256", secret)
|
|
89
|
+
.update(payload)
|
|
90
|
+
.digest("hex");
|
|
91
|
+
if (!crypto.timingSafeEqual(Buffer.from(signature), Buffer.from(expected))) {
|
|
92
|
+
throw new Error("Data integrity verification failed");
|
|
93
|
+
}
|
|
94
|
+
return JSON.parse(payload);
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
// Server-side price validation (never trust client price)
|
|
98
|
+
app.post("/api/checkout", authenticate, async (req, res) => {
|
|
99
|
+
const { itemId, quantity } = req.body;
|
|
100
|
+
const item = await db.getItem(itemId); // Get real price from DB
|
|
101
|
+
const totalPrice = item.price * quantity; // Calculate server-side
|
|
102
|
+
await processPayment(req.user.id, totalPrice);
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
// Webhook signature verification (Stripe example pattern)
|
|
106
|
+
function verifyWebhookSignature(payload, signature, secret) {
|
|
107
|
+
const [timestamp, hash] = parseSignatureHeader(signature);
|
|
108
|
+
|
|
109
|
+
// Prevent replay attacks
|
|
110
|
+
if (Date.now() / 1000 - Number(timestamp) > 300) {
|
|
111
|
+
throw new Error("Webhook timestamp too old");
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
const expected = crypto
|
|
115
|
+
.createHmac("sha256", secret)
|
|
116
|
+
.update(`${timestamp}.${payload}`)
|
|
117
|
+
.digest("hex");
|
|
118
|
+
|
|
119
|
+
if (!crypto.timingSafeEqual(Buffer.from(hash), Buffer.from(expected))) {
|
|
120
|
+
throw new Error("Invalid webhook signature");
|
|
121
|
+
}
|
|
122
|
+
return JSON.parse(payload);
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
app.post("/webhook/payment", express.raw({ type: "application/json" }), (req, res) => {
|
|
126
|
+
const event = verifyWebhookSignature(
|
|
127
|
+
req.body.toString(),
|
|
128
|
+
req.headers["x-signature"],
|
|
129
|
+
process.env.WEBHOOK_SECRET,
|
|
130
|
+
);
|
|
131
|
+
processVerifiedEvent(event);
|
|
132
|
+
res.sendStatus(200);
|
|
133
|
+
});
|
|
134
|
+
```
|
|
135
|
+
|
|
136
|
+
## Quick Checklist
|
|
137
|
+
- [ ] CI/CD pipeline changes require review and approval
|
|
138
|
+
- [ ] All deserialization uses schema validation, never `eval()`
|
|
139
|
+
- [ ] Critical client-facing data is signed with HMAC or digital signatures
|
|
140
|
+
- [ ] Server-side calculation for prices, totals, and business-critical values
|
|
141
|
+
- [ ] Webhook payloads verified with signature before processing
|
|
142
|
+
- [ ] Auto-updates verify digital signatures before applying
|
|
143
|
+
- [ ] Database migrations are version-controlled and reviewed
|
|
144
|
+
- [ ] `timingSafeEqual` used for all signature comparisons
|
|
@@ -0,0 +1,180 @@
|
|
|
1
|
+
# Error Handling Security Rules
|
|
2
|
+
|
|
3
|
+
> OWASP Top 10 2025 - A10: Mishandling of Exceptional Conditions (NEW)
|
|
4
|
+
|
|
5
|
+
## Rules
|
|
6
|
+
|
|
7
|
+
### 1. Implement Fail-Safe Defaults
|
|
8
|
+
- **DO**: Default to the most secure state when errors occur. Deny access, reject transactions, and close connections on failure.
|
|
9
|
+
- **DON'T**: Fail open by granting access or skipping validation when an error occurs.
|
|
10
|
+
- **WHY**: Attackers intentionally trigger errors to bypass security controls. Fail-safe defaults ensure errors never weaken security posture.
|
|
11
|
+
|
|
12
|
+
### 2. Use Structured Error Handling Patterns
|
|
13
|
+
- **DO**: Use try-catch blocks, Result/Either patterns, or error boundary components. Handle every possible error state explicitly.
|
|
14
|
+
- **DON'T**: Ignore promise rejections, leave catch blocks empty, or rely on uncaught exception handlers for control flow.
|
|
15
|
+
- **WHY**: Unhandled errors crash services, leak information, and create unpredictable system states that attackers exploit.
|
|
16
|
+
|
|
17
|
+
### 3. Separate Internal and External Error Messages
|
|
18
|
+
- **DO**: Log detailed errors internally with full context. Return generic, safe error messages to users with a reference ID.
|
|
19
|
+
- **DON'T**: Expose internal error details, stack traces, or database messages to clients.
|
|
20
|
+
- **WHY**: Detailed errors reveal technology stack, file paths, and database structure that aid targeted attacks.
|
|
21
|
+
|
|
22
|
+
### 4. Implement Graceful Degradation
|
|
23
|
+
- **DO**: Design fallback behavior for external service failures. Use circuit breakers, timeouts, and retry with backoff.
|
|
24
|
+
- **DON'T**: Let a single external dependency failure cascade into full application failure.
|
|
25
|
+
- **WHY**: Cascading failures cause outages. Graceful degradation maintains core functionality and prevents denial of service.
|
|
26
|
+
|
|
27
|
+
### 5. Handle All Promise Rejections and Async Errors
|
|
28
|
+
- **DO**: Attach `.catch()` to all promises or use `try-catch` with `await`. Register global `unhandledRejection` handlers as a safety net.
|
|
29
|
+
- **DON'T**: Fire and forget promises or assume async operations will always succeed.
|
|
30
|
+
- **WHY**: Unhandled promise rejections cause silent failures, memory leaks, and in Node.js 15+, process crashes.
|
|
31
|
+
|
|
32
|
+
### 6. Validate Error Objects Before Processing
|
|
33
|
+
- **DO**: Verify error types and properties before accessing them. Use `instanceof` checks or error codes.
|
|
34
|
+
- **DON'T**: Assume all caught errors are `Error` instances or have expected properties.
|
|
35
|
+
- **WHY**: JavaScript can throw any value. Accessing `.message` or `.stack` on non-Error values causes secondary failures.
|
|
36
|
+
|
|
37
|
+
### 7. Implement Request Timeouts and Resource Limits
|
|
38
|
+
- **DO**: Set timeouts for all external requests, database queries, and file operations. Limit request body size and processing time.
|
|
39
|
+
- **DON'T**: Allow requests or operations to run indefinitely without timeout boundaries.
|
|
40
|
+
- **WHY**: Missing timeouts lead to resource exhaustion, blocked event loops, and denial of service.
|
|
41
|
+
|
|
42
|
+
## Code Examples
|
|
43
|
+
|
|
44
|
+
### Bad Practice
|
|
45
|
+
```javascript
|
|
46
|
+
// Failing open - granting access on error
|
|
47
|
+
async function checkPermission(userId, resource) {
|
|
48
|
+
try {
|
|
49
|
+
const allowed = await authService.check(userId, resource);
|
|
50
|
+
return allowed;
|
|
51
|
+
} catch (error) {
|
|
52
|
+
return true; // DANGEROUS: fail-open grants access on auth service failure
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
// Empty catch block hiding errors
|
|
57
|
+
try {
|
|
58
|
+
await processPayment(order);
|
|
59
|
+
} catch (e) {
|
|
60
|
+
// silently swallowed - payment may have partially processed
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
// Leaking error details to client
|
|
64
|
+
app.use((err, req, res, next) => {
|
|
65
|
+
res.status(500).json({
|
|
66
|
+
error: err.message,
|
|
67
|
+
stack: err.stack,
|
|
68
|
+
sql: err.query,
|
|
69
|
+
});
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
// No timeout on external request
|
|
73
|
+
const response = await fetch("https://external-api.com/data"); // Hangs forever if API is down
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
### Good Practice
|
|
77
|
+
```javascript
|
|
78
|
+
// Fail-safe: deny access on error
|
|
79
|
+
async function checkPermission(userId, resource) {
|
|
80
|
+
try {
|
|
81
|
+
return await authService.check(userId, resource);
|
|
82
|
+
} catch (error) {
|
|
83
|
+
logger.error({ userId, resource, error: error.message }, "Auth service failed");
|
|
84
|
+
return false; // Fail-safe: deny access on error
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
// Structured error handling with Result pattern
|
|
89
|
+
class Result {
|
|
90
|
+
constructor(ok, value, error) {
|
|
91
|
+
this.ok = ok;
|
|
92
|
+
this.value = value;
|
|
93
|
+
this.error = error;
|
|
94
|
+
}
|
|
95
|
+
static success(value) { return new Result(true, value, null); }
|
|
96
|
+
static failure(error) { return new Result(false, null, error); }
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
async function processPayment(order) {
|
|
100
|
+
try {
|
|
101
|
+
const result = await paymentGateway.charge(order);
|
|
102
|
+
return Result.success(result);
|
|
103
|
+
} catch (error) {
|
|
104
|
+
logger.error({ orderId: order.id, error: error.message }, "Payment failed");
|
|
105
|
+
return Result.failure(new PaymentError("Payment processing failed", { cause: error }));
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
// Safe error handler separating internal/external messages
|
|
110
|
+
app.use((err, req, res, next) => {
|
|
111
|
+
const errorId = crypto.randomUUID();
|
|
112
|
+
logger.error({ errorId, path: req.path, method: req.method, error: err.message, stack: err.stack });
|
|
113
|
+
const status = err.status ?? 500;
|
|
114
|
+
res.status(status).json({
|
|
115
|
+
error: status < 500 ? err.message : "An internal error occurred",
|
|
116
|
+
errorId,
|
|
117
|
+
});
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
// Circuit breaker for external services
|
|
121
|
+
class CircuitBreaker {
|
|
122
|
+
#failures = 0;
|
|
123
|
+
#lastFailure = 0;
|
|
124
|
+
#state = "closed"; // closed, open, half-open
|
|
125
|
+
|
|
126
|
+
constructor(threshold = 5, resetTimeout = 30_000) {
|
|
127
|
+
this.threshold = threshold;
|
|
128
|
+
this.resetTimeout = resetTimeout;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
async execute(fn) {
|
|
132
|
+
if (this.#state === "open") {
|
|
133
|
+
if (Date.now() - this.#lastFailure > this.resetTimeout) {
|
|
134
|
+
this.#state = "half-open";
|
|
135
|
+
} else {
|
|
136
|
+
throw new Error("Circuit breaker is open");
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
try {
|
|
140
|
+
const result = await fn();
|
|
141
|
+
this.#failures = 0;
|
|
142
|
+
this.#state = "closed";
|
|
143
|
+
return result;
|
|
144
|
+
} catch (error) {
|
|
145
|
+
this.#failures++;
|
|
146
|
+
this.#lastFailure = Date.now();
|
|
147
|
+
if (this.#failures >= this.threshold) this.#state = "open";
|
|
148
|
+
throw error;
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
// Request with timeout
|
|
154
|
+
async function fetchWithTimeout(url, options = {}, timeoutMs = 5000) {
|
|
155
|
+
const controller = new AbortController();
|
|
156
|
+
const timeoutId = setTimeout(() => controller.abort(), timeoutMs);
|
|
157
|
+
try {
|
|
158
|
+
return await fetch(url, { ...options, signal: controller.signal });
|
|
159
|
+
} finally {
|
|
160
|
+
clearTimeout(timeoutId);
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
// Global safety net (not a substitute for proper error handling)
|
|
165
|
+
process.on("unhandledRejection", (reason) => {
|
|
166
|
+
logger.fatal({ reason: String(reason) }, "Unhandled promise rejection");
|
|
167
|
+
process.exitCode = 1;
|
|
168
|
+
});
|
|
169
|
+
```
|
|
170
|
+
|
|
171
|
+
## Quick Checklist
|
|
172
|
+
- [ ] All error paths fail-safe (deny access, reject transactions)
|
|
173
|
+
- [ ] No empty catch blocks in the codebase
|
|
174
|
+
- [ ] Internal errors logged with full detail; external responses are generic
|
|
175
|
+
- [ ] Circuit breakers implemented for external service calls
|
|
176
|
+
- [ ] All promises have `.catch()` or are inside try-catch with await
|
|
177
|
+
- [ ] Global `unhandledRejection` handler registered as safety net
|
|
178
|
+
- [ ] Timeouts set for all HTTP requests, database queries, and I/O operations
|
|
179
|
+
- [ ] Request body size limits enforced
|
|
180
|
+
- [ ] Error objects validated before accessing properties
|