@lhi/tdd-audit 1.2.0 → 1.3.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/SKILL.md +2 -1
- package/index.js +40 -1
- package/package.json +1 -1
- package/prompts/auto-audit.md +80 -0
- package/prompts/green-phase.md +271 -0
- package/prompts/hardening-phase.md +243 -0
- package/prompts/red-phase.md +84 -0
- package/prompts/refactor-phase.md +10 -0
- package/workflows/tdd-audit.md +8 -1
package/SKILL.md
CHANGED
|
@@ -14,12 +14,13 @@ If the user asks you to "Run the TDD Remediation Auto-Audit" or asks you to impl
|
|
|
14
14
|
- **React / Next.js**: `pages/api/`, `app/api/`, `components/`, `hooks/`, `context/`, `store/`
|
|
15
15
|
- **React Native / Expo**: `screens/`, `navigation/`, `app/`, `app.json`, `app.config.js`
|
|
16
16
|
- **Flutter / Dart**: `lib/screens/`, `lib/services/`, `lib/api/`, `lib/repositories/`, `pubspec.yaml`
|
|
17
|
-
Search for anti-patterns:
|
|
17
|
+
Search for anti-patterns across the full vulnerability surface: SQL/NoSQL/Template injection, IDOR, XSS, command injection, path traversal, SSRF, open redirects, broken auth, mass assignment, prototype pollution, weak crypto, sensitive storage, TLS bypasses, hardcoded secrets, missing rate limiting, missing security headers, CORS wildcards, XXE, insecure deserialization, WebView JS bridge exposure. Full search patterns are in [auto-audit.md](./prompts/auto-audit.md).
|
|
18
18
|
2. **Plan**: Present a structured list of vulnerabilities (grouped by severity: CRITICAL / HIGH / MEDIUM / LOW) and get confirmation before making any changes.
|
|
19
19
|
3. **Self-Implement**: For *each* confirmed vulnerability, autonomously execute the complete 3-phase protocol:
|
|
20
20
|
- **[Phase 1 (Red)](./prompts/red-phase.md)**: Write the exploit test ensuring it fails.
|
|
21
21
|
- **[Phase 2 (Green)](./prompts/green-phase.md)**: Write the security patch ensuring the test passes.
|
|
22
22
|
- **[Phase 3 (Refactor)](./prompts/refactor-phase.md)**: Run the full test suite and ensure no business logic broke.
|
|
23
|
+
4. **[Phase 4 (Hardening)](./prompts/hardening-phase.md)**: After all vulnerabilities are remediated, apply proactive defense-in-depth controls: security headers (Helmet), CSP, CSRF protection, rate limiting audit, dependency vulnerability scan, secret history scan, production error handling, and SRI for third-party scripts.
|
|
23
24
|
Move methodically through vulnerabilities one by one, CRITICAL-first. Do not advance until the current vulnerability is fully remediated.
|
|
24
25
|
|
|
25
26
|
---
|
package/index.js
CHANGED
|
@@ -102,6 +102,23 @@ const VULN_PATTERNS = [
|
|
|
102
102
|
{ name: 'Insecure Random', severity: 'HIGH', pattern: /(?:token|sessionId|nonce|secret|csrf)\w*\s*=.*Math\.random\(\)|Math\.random\(\).*(?:token|session|nonce|secret)/i },
|
|
103
103
|
{ name: 'Sensitive Log', severity: 'MEDIUM', skipInTests: true, pattern: /console\.(log|info|debug)\([^)]*(?:token|password|secret|jwt|authorization|apiKey|api_key)/i },
|
|
104
104
|
{ name: 'Secret Fallback', severity: 'HIGH', pattern: /process\.env\.\w+\s*\|\|\s*['"][A-Za-z0-9+/=_\-]{10,}['"]/i },
|
|
105
|
+
// SSRF, redirects, injection
|
|
106
|
+
{ name: 'SSRF', severity: 'CRITICAL', pattern: /\b(?:fetch|axios\.(?:get|post|put|patch|delete|request)|got|https?\.get)\s*\(\s*req\.(?:query|body|params)\./i },
|
|
107
|
+
{ name: 'Open Redirect', severity: 'HIGH', pattern: /res\.redirect\s*\(\s*req\.(?:query|body|params)\.|window\.location(?:\.href)?\s*=\s*(?:params\.|route\.params\.|searchParams\.get)/i },
|
|
108
|
+
{ name: 'NoSQL Injection', severity: 'HIGH', pattern: /\.(?:find|findOne|findById|updateOne|deleteOne)\s*\(\s*req\.(?:body|query|params)\b|\$where\s*:\s*['"`]/i },
|
|
109
|
+
{ name: 'Template Injection', severity: 'HIGH', pattern: /res\.render\s*\(\s*req\.(?:params|body|query)\.|(?:ejs|pug|nunjucks|handlebars)\.render(?:File)?\s*\([^)]*req\.(?:body|params|query)/i },
|
|
110
|
+
{ name: 'Insecure Deserialization',severity: 'CRITICAL', pattern: /\.unserialize\s*\(.*req\.|__proto__\s*[=:][^=]|Object\.setPrototypeOf\s*\([^,]+,\s*req\./i },
|
|
111
|
+
// Assignment / pollution
|
|
112
|
+
{ name: 'Mass Assignment', severity: 'HIGH', pattern: /new\s+\w+\s*\(\s*req\.body\b|\.create\s*\(\s*req\.body\b|\.update(?:One)?\s*\(\s*\{[^}]*\},\s*req\.body\b/i },
|
|
113
|
+
{ name: 'Prototype Pollution', severity: 'HIGH', pattern: /(?:_\.merge|lodash\.merge|deepmerge|hoek\.merge)\s*\([^)]*req\.(?:body|query|params)/i },
|
|
114
|
+
// Crypto / config
|
|
115
|
+
{ name: 'Weak Crypto', severity: 'HIGH', pattern: /createHash\s*\(\s*['"](?:md5|sha1)['"]\)|(?:md5|sha1)\s*\(\s*(?:password|passwd|pwd|secret)/i },
|
|
116
|
+
{ name: 'CORS Wildcard', severity: 'MEDIUM', pattern: /cors\s*\(\s*\{\s*origin\s*:\s*['"]?\*['"]?|'Access-Control-Allow-Origin',\s*['"]?\*/i },
|
|
117
|
+
{ name: 'Cleartext Traffic', severity: 'MEDIUM', skipInTests: true, pattern: /(?:baseURL|apiUrl|API_URL|endpoint|baseUrl)\s*[:=]\s*['"]http:\/\/(?!localhost|127\.0\.0\.1)/i },
|
|
118
|
+
{ name: 'XXE', severity: 'HIGH', pattern: /noent\s*:\s*true|expand_entities\s*=\s*True|resolve_entities\s*=\s*True/i },
|
|
119
|
+
// Mobile / WebView
|
|
120
|
+
{ name: 'WebView JS Bridge', severity: 'HIGH', pattern: /addJavascriptInterface\s*\(|javaScriptEnabled\s*:\s*true|allowFileAccess\s*:\s*true|allowUniversalAccessFromFileURLs\s*:\s*true/i },
|
|
121
|
+
{ name: 'Deep Link Injection', severity: 'MEDIUM', pattern: /Linking\.getInitialURL\s*\(\)|Linking\.addEventListener\s*\(\s*['"]url['"]/i },
|
|
105
122
|
];
|
|
106
123
|
|
|
107
124
|
const SCAN_EXTENSIONS = new Set(['.js', '.ts', '.jsx', '.tsx', '.mjs', '.py', '.go', '.dart']);
|
|
@@ -151,6 +168,28 @@ function scanAppConfig() {
|
|
|
151
168
|
return findings;
|
|
152
169
|
}
|
|
153
170
|
|
|
171
|
+
function scanAndroidManifest() {
|
|
172
|
+
const findings = [];
|
|
173
|
+
const manifestPath = path.join(projectDir, 'android', 'app', 'src', 'main', 'AndroidManifest.xml');
|
|
174
|
+
if (!fs.existsSync(manifestPath)) return findings;
|
|
175
|
+
let lines;
|
|
176
|
+
try { lines = fs.readFileSync(manifestPath, 'utf8').split('\n'); } catch { return findings; }
|
|
177
|
+
for (let i = 0; i < lines.length; i++) {
|
|
178
|
+
if (/android:debuggable\s*=\s*["']true["']/i.test(lines[i])) {
|
|
179
|
+
findings.push({
|
|
180
|
+
severity: 'HIGH',
|
|
181
|
+
name: 'Android Debuggable',
|
|
182
|
+
file: 'android/app/src/main/AndroidManifest.xml',
|
|
183
|
+
line: i + 1,
|
|
184
|
+
snippet: lines[i].trim().slice(0, 80),
|
|
185
|
+
inTestFile: false,
|
|
186
|
+
likelyFalsePositive: false,
|
|
187
|
+
});
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
return findings;
|
|
191
|
+
}
|
|
192
|
+
|
|
154
193
|
function quickScan() {
|
|
155
194
|
const findings = [];
|
|
156
195
|
for (const filePath of walkFiles(projectDir)) {
|
|
@@ -174,7 +213,7 @@ function quickScan() {
|
|
|
174
213
|
}
|
|
175
214
|
}
|
|
176
215
|
}
|
|
177
|
-
return [...findings, ...scanAppConfig()];
|
|
216
|
+
return [...findings, ...scanAppConfig(), ...scanAndroidManifest()];
|
|
178
217
|
}
|
|
179
218
|
|
|
180
219
|
function printFindings(findings) {
|
package/package.json
CHANGED
package/prompts/auto-audit.md
CHANGED
|
@@ -134,6 +134,86 @@ SharedPreferences.*setString.*token # token in unencrypted SharedPreferences
|
|
|
134
134
|
Platform\.environment\[ # env access in Flutter — check for secrets
|
|
135
135
|
```
|
|
136
136
|
|
|
137
|
+
**SSRF (Server-Side Request Forgery)**
|
|
138
|
+
```
|
|
139
|
+
fetch\(.*req\.(query|body|params) # fetch with user-controlled URL
|
|
140
|
+
axios\.(get|post)\(.*req\.body # axios with user-controlled target
|
|
141
|
+
got\(.*req\.(query|params) # got with user-controlled URL
|
|
142
|
+
```
|
|
143
|
+
|
|
144
|
+
**Open Redirect**
|
|
145
|
+
```
|
|
146
|
+
res\.redirect\(.*req\.(query|body) # redirecting to user-supplied URL
|
|
147
|
+
window\.location.*=.*params\. # client-side redirect from route params
|
|
148
|
+
router\.push\(.*searchParams # Next.js/RN push with user param
|
|
149
|
+
```
|
|
150
|
+
|
|
151
|
+
**NoSQL Injection**
|
|
152
|
+
```
|
|
153
|
+
\.find\(\s*req\.(body|query) # MongoDB find with raw request object
|
|
154
|
+
\.findOne\(\s*req\.(body|query) # MongoDB findOne with raw request object
|
|
155
|
+
\$where.*: # $where operator (executes JS in Mongo)
|
|
156
|
+
```
|
|
157
|
+
|
|
158
|
+
**Mass Assignment**
|
|
159
|
+
```
|
|
160
|
+
new.*Model\(.*req\.body # passing full req.body to constructor
|
|
161
|
+
\.create\(.*req\.body # ORM create with unsanitized body
|
|
162
|
+
\.update.*req\.body # ORM update with unsanitized body
|
|
163
|
+
```
|
|
164
|
+
|
|
165
|
+
**Prototype Pollution**
|
|
166
|
+
```
|
|
167
|
+
_\.merge\(.*req\.(body|query) # lodash merge with user input
|
|
168
|
+
deepmerge\(.*req\.(body|query) # deepmerge with user input
|
|
169
|
+
Object\.assign\(\{\}.*req\.body # Object.assign from user input
|
|
170
|
+
```
|
|
171
|
+
|
|
172
|
+
**Weak Cryptography**
|
|
173
|
+
```
|
|
174
|
+
createHash\(['"]md5['"] # MD5 for anything security-related
|
|
175
|
+
createHash\(['"]sha1['"] # SHA1 for anything security-related
|
|
176
|
+
md5\(.*password # MD5-hashed password
|
|
177
|
+
sha1\(.*password # SHA1-hashed password
|
|
178
|
+
```
|
|
179
|
+
|
|
180
|
+
**Missing Security Headers / Rate Limiting**
|
|
181
|
+
```
|
|
182
|
+
app\.(use|listen) # check: is helmet() present before routes?
|
|
183
|
+
router\.(post|put|delete) # mutation routes — check for rateLimit middleware
|
|
184
|
+
app\.post\('/login # login route — must have rate limit
|
|
185
|
+
app\.post\('/register # register route — must have rate limit
|
|
186
|
+
```
|
|
187
|
+
|
|
188
|
+
**CORS Misconfiguration**
|
|
189
|
+
```
|
|
190
|
+
cors\(\{.*origin.*\* # wildcard CORS origin
|
|
191
|
+
Access-Control-Allow-Origin.*\* # wildcard CORS header
|
|
192
|
+
```
|
|
193
|
+
|
|
194
|
+
**Template Injection**
|
|
195
|
+
```
|
|
196
|
+
res\.render\(.*req\.(params|query) # user-controlled template name
|
|
197
|
+
ejs\.render\(.*req\.body # ejs render with user input
|
|
198
|
+
pug\.render\(.*req\.body # pug render with user input
|
|
199
|
+
```
|
|
200
|
+
|
|
201
|
+
**Cleartext Traffic / XXE**
|
|
202
|
+
```
|
|
203
|
+
baseURL.*=.*['"]http://(?!localhost) # non-HTTPS API base URL
|
|
204
|
+
noent.*:.*true # XML entity expansion enabled
|
|
205
|
+
resolve_entities.*True # Python lxml entity expansion
|
|
206
|
+
```
|
|
207
|
+
|
|
208
|
+
**Dependency Audit**
|
|
209
|
+
```
|
|
210
|
+
# Run manually — not grep-based:
|
|
211
|
+
# npm audit --audit-level=high
|
|
212
|
+
# pip-audit
|
|
213
|
+
# govulncheck ./...
|
|
214
|
+
# bundle audit
|
|
215
|
+
```
|
|
216
|
+
|
|
137
217
|
### 0c. Present Findings
|
|
138
218
|
Before touching any code, output a structured **Audit Report** with this format:
|
|
139
219
|
|
package/prompts/green-phase.md
CHANGED
|
@@ -341,6 +341,277 @@ dependencies:
|
|
|
341
341
|
|
|
342
342
|
---
|
|
343
343
|
|
|
344
|
+
### SSRF (Server-Side Request Forgery)
|
|
345
|
+
|
|
346
|
+
**Root cause:** The server makes outbound HTTP requests to a URL supplied by the user without validation.
|
|
347
|
+
|
|
348
|
+
**Fix:** Validate the URL against an explicit allowlist of allowed hostnames. Never make requests to private/internal IP ranges.
|
|
349
|
+
|
|
350
|
+
```javascript
|
|
351
|
+
const { URL } = require('url');
|
|
352
|
+
|
|
353
|
+
const ALLOWED_ORIGINS = new Set(['api.trusted.com', 'cdn.example.com']);
|
|
354
|
+
|
|
355
|
+
function validateExternalUrl(rawUrl) {
|
|
356
|
+
let parsed;
|
|
357
|
+
try { parsed = new URL(rawUrl); } catch { throw new Error('Invalid URL'); }
|
|
358
|
+
if (!['http:', 'https:'].includes(parsed.protocol)) throw new Error('Protocol not allowed');
|
|
359
|
+
if (!ALLOWED_ORIGINS.has(parsed.hostname)) throw new Error('Host not allowed');
|
|
360
|
+
return parsed.toString();
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
// In the route handler:
|
|
364
|
+
const safeUrl = validateExternalUrl(req.body.url); // throws on violation
|
|
365
|
+
const response = await fetch(safeUrl);
|
|
366
|
+
```
|
|
367
|
+
|
|
368
|
+
**Libraries:** No extra library needed; use the built-in `URL` class.
|
|
369
|
+
|
|
370
|
+
---
|
|
371
|
+
|
|
372
|
+
### Open Redirect
|
|
373
|
+
|
|
374
|
+
**Root cause:** The server redirects the user to a URL supplied in a query parameter without validating the destination.
|
|
375
|
+
|
|
376
|
+
**Fix:** Only allow relative paths or explicitly allowlisted origins.
|
|
377
|
+
|
|
378
|
+
```javascript
|
|
379
|
+
function safeRedirect(res, destination) {
|
|
380
|
+
// Allow only relative paths (no scheme, no host)
|
|
381
|
+
if (/^https?:\/\//i.test(destination)) {
|
|
382
|
+
return res.status(400).json({ error: 'External redirects not allowed' });
|
|
383
|
+
}
|
|
384
|
+
// Prevent protocol-relative URLs (//evil.com)
|
|
385
|
+
if (destination.startsWith('//')) {
|
|
386
|
+
return res.status(400).json({ error: 'Invalid redirect destination' });
|
|
387
|
+
}
|
|
388
|
+
return res.redirect(destination.startsWith('/') ? destination : `/${destination}`);
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
// Usage:
|
|
392
|
+
safeRedirect(res, req.query.redirect || '/dashboard');
|
|
393
|
+
```
|
|
394
|
+
|
|
395
|
+
---
|
|
396
|
+
|
|
397
|
+
### NoSQL Injection
|
|
398
|
+
|
|
399
|
+
**Root cause:** A user-supplied value that should be a string is passed directly to MongoDB, allowing operator injection (`{ $gt: '' }`).
|
|
400
|
+
|
|
401
|
+
**Fix:** Enforce that query values are primitive strings. Reject objects from user input in query fields.
|
|
402
|
+
|
|
403
|
+
```javascript
|
|
404
|
+
// Middleware: sanitize mongo-operator injection
|
|
405
|
+
function sanitizeBody(req, res, next) {
|
|
406
|
+
const hasDollar = (obj) =>
|
|
407
|
+
Object.keys(obj || {}).some(k => k.startsWith('$') || (typeof obj[k] === 'object' && hasDollar(obj[k])));
|
|
408
|
+
if (hasDollar(req.body) || hasDollar(req.query)) {
|
|
409
|
+
return res.status(400).json({ error: 'Invalid input' });
|
|
410
|
+
}
|
|
411
|
+
next();
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
app.use(sanitizeBody);
|
|
415
|
+
```
|
|
416
|
+
|
|
417
|
+
**Library alternative:** `express-mongo-sanitize` strips `$` and `.` from user input automatically.
|
|
418
|
+
```javascript
|
|
419
|
+
const mongoSanitize = require('express-mongo-sanitize');
|
|
420
|
+
app.use(mongoSanitize());
|
|
421
|
+
```
|
|
422
|
+
|
|
423
|
+
---
|
|
424
|
+
|
|
425
|
+
### Mass Assignment
|
|
426
|
+
|
|
427
|
+
**Root cause:** `req.body` is passed directly to an ORM constructor or update method, allowing users to set any field including privileged ones.
|
|
428
|
+
|
|
429
|
+
**Fix:** Always destructure and explicitly allowlist the fields you accept from the user.
|
|
430
|
+
|
|
431
|
+
```javascript
|
|
432
|
+
// BEFORE (vulnerable)
|
|
433
|
+
const user = await User.create(req.body);
|
|
434
|
+
|
|
435
|
+
// AFTER — explicit allowlist
|
|
436
|
+
const { username, email, password } = req.body;
|
|
437
|
+
const user = await User.create({ username, email, password });
|
|
438
|
+
|
|
439
|
+
// For updates:
|
|
440
|
+
const { displayName, bio } = req.body; // only fields users can change
|
|
441
|
+
await User.updateOne({ _id: req.user.id }, { displayName, bio });
|
|
442
|
+
```
|
|
443
|
+
|
|
444
|
+
```python
|
|
445
|
+
# FastAPI — use a Pydantic schema with only allowed fields
|
|
446
|
+
class UserCreate(BaseModel):
|
|
447
|
+
username: str
|
|
448
|
+
email: EmailStr
|
|
449
|
+
password: str
|
|
450
|
+
# isAdmin NOT here — cannot be set by users
|
|
451
|
+
|
|
452
|
+
@app.post('/users')
|
|
453
|
+
async def create_user(data: UserCreate):
|
|
454
|
+
user = User(**data.dict()) # safe: Pydantic strips unlisted fields
|
|
455
|
+
```
|
|
456
|
+
|
|
457
|
+
---
|
|
458
|
+
|
|
459
|
+
### Prototype Pollution
|
|
460
|
+
|
|
461
|
+
**Root cause:** A recursive merge function applied to user-supplied input can overwrite `Object.prototype` properties.
|
|
462
|
+
|
|
463
|
+
**Fix:** Use a null-prototype target for merges, or sanitize `__proto__` / `constructor` keys before merging.
|
|
464
|
+
|
|
465
|
+
```javascript
|
|
466
|
+
// Option A: sanitize keys before merge (drop __proto__, constructor, prototype)
|
|
467
|
+
function safeMerge(target, source) {
|
|
468
|
+
const clean = JSON.parse(
|
|
469
|
+
JSON.stringify(source, (key, val) =>
|
|
470
|
+
['__proto__', 'constructor', 'prototype'].includes(key) ? undefined : val
|
|
471
|
+
)
|
|
472
|
+
);
|
|
473
|
+
return Object.assign(target, clean);
|
|
474
|
+
}
|
|
475
|
+
|
|
476
|
+
// Option B: use Object.create(null) as the target so there is no prototype to pollute
|
|
477
|
+
const settings = safeMerge(Object.create(null), req.body);
|
|
478
|
+
```
|
|
479
|
+
|
|
480
|
+
**Library:** `lodash` ≥ 4.17.21 has this patched. If using `deepmerge`, pass `{ clone: true }` and pre-sanitize keys.
|
|
481
|
+
|
|
482
|
+
---
|
|
483
|
+
|
|
484
|
+
### Weak Cryptography (Password Hashing)
|
|
485
|
+
|
|
486
|
+
**Root cause:** Passwords are hashed with MD5 or SHA1 — fast algorithms that are trivially brute-forced.
|
|
487
|
+
|
|
488
|
+
**Fix:** Use `bcrypt` or `argon2`. Never use MD5/SHA1/SHA256 directly for passwords.
|
|
489
|
+
|
|
490
|
+
```javascript
|
|
491
|
+
// BEFORE (vulnerable)
|
|
492
|
+
const crypto = require('crypto');
|
|
493
|
+
const hash = crypto.createHash('md5').update(password).digest('hex');
|
|
494
|
+
|
|
495
|
+
// AFTER — bcrypt
|
|
496
|
+
const bcrypt = require('bcrypt');
|
|
497
|
+
const SALT_ROUNDS = 12; // increase over time as hardware improves
|
|
498
|
+
|
|
499
|
+
// On registration:
|
|
500
|
+
const passwordHash = await bcrypt.hash(password, SALT_ROUNDS);
|
|
501
|
+
await User.create({ email, passwordHash });
|
|
502
|
+
|
|
503
|
+
// On login:
|
|
504
|
+
const valid = await bcrypt.compare(req.body.password, user.passwordHash);
|
|
505
|
+
if (!valid) return res.status(401).json({ error: 'Invalid credentials' });
|
|
506
|
+
```
|
|
507
|
+
|
|
508
|
+
```python
|
|
509
|
+
# AFTER — bcrypt (Python)
|
|
510
|
+
import bcrypt
|
|
511
|
+
|
|
512
|
+
# Hash on registration:
|
|
513
|
+
hashed = bcrypt.hashpw(password.encode(), bcrypt.gensalt(rounds=12))
|
|
514
|
+
|
|
515
|
+
# Verify on login:
|
|
516
|
+
if not bcrypt.checkpw(password.encode(), stored_hash):
|
|
517
|
+
raise HTTPException(status_code=401, detail="Invalid credentials")
|
|
518
|
+
```
|
|
519
|
+
|
|
520
|
+
**Install:** `npm install bcrypt` / `pip install bcrypt`
|
|
521
|
+
|
|
522
|
+
---
|
|
523
|
+
|
|
524
|
+
### Missing Rate Limiting
|
|
525
|
+
|
|
526
|
+
**Root cause:** Authentication and sensitive mutation endpoints have no throttle, enabling brute-force and credential-stuffing attacks.
|
|
527
|
+
|
|
528
|
+
**Fix:** Apply `express-rate-limit` (Node.js) to auth routes. Use a stricter window on login than on general API routes.
|
|
529
|
+
|
|
530
|
+
```javascript
|
|
531
|
+
const rateLimit = require('express-rate-limit');
|
|
532
|
+
|
|
533
|
+
const loginLimiter = rateLimit({
|
|
534
|
+
windowMs: 15 * 60 * 1000, // 15 minutes
|
|
535
|
+
max: 10, // 10 attempts per window per IP
|
|
536
|
+
standardHeaders: true,
|
|
537
|
+
legacyHeaders: false,
|
|
538
|
+
message: { error: 'Too many login attempts. Try again in 15 minutes.' },
|
|
539
|
+
});
|
|
540
|
+
|
|
541
|
+
const apiLimiter = rateLimit({
|
|
542
|
+
windowMs: 60 * 1000,
|
|
543
|
+
max: 100,
|
|
544
|
+
});
|
|
545
|
+
|
|
546
|
+
app.use('/api/', apiLimiter);
|
|
547
|
+
app.post('/api/auth/login', loginLimiter, loginHandler);
|
|
548
|
+
app.post('/api/auth/register', loginLimiter, registerHandler);
|
|
549
|
+
```
|
|
550
|
+
|
|
551
|
+
```python
|
|
552
|
+
# FastAPI — slowapi
|
|
553
|
+
from slowapi import Limiter
|
|
554
|
+
from slowapi.util import get_remote_address
|
|
555
|
+
|
|
556
|
+
limiter = Limiter(key_func=get_remote_address)
|
|
557
|
+
|
|
558
|
+
@app.post('/auth/login')
|
|
559
|
+
@limiter.limit('10/15minutes')
|
|
560
|
+
async def login(request: Request, data: LoginRequest):
|
|
561
|
+
...
|
|
562
|
+
```
|
|
563
|
+
|
|
564
|
+
**Install:** `npm install express-rate-limit` / `pip install slowapi`
|
|
565
|
+
|
|
566
|
+
---
|
|
567
|
+
|
|
568
|
+
### Missing Security Headers
|
|
569
|
+
|
|
570
|
+
**Root cause:** Responses lack HTTP security headers, leaving browsers unprotected against clickjacking, MIME-sniffing, and other attacks.
|
|
571
|
+
|
|
572
|
+
**Fix:** Install `helmet` as the first middleware. Configure CSP explicitly.
|
|
573
|
+
|
|
574
|
+
```javascript
|
|
575
|
+
const helmet = require('helmet');
|
|
576
|
+
|
|
577
|
+
// Minimal (all helmet defaults — good for most apps)
|
|
578
|
+
app.use(helmet());
|
|
579
|
+
|
|
580
|
+
// With explicit CSP:
|
|
581
|
+
app.use(
|
|
582
|
+
helmet({
|
|
583
|
+
contentSecurityPolicy: {
|
|
584
|
+
directives: {
|
|
585
|
+
defaultSrc: ["'self'"],
|
|
586
|
+
scriptSrc: ["'self'"],
|
|
587
|
+
styleSrc: ["'self'", "'unsafe-inline'"], // tighten further if possible
|
|
588
|
+
imgSrc: ["'self'", 'data:', 'https:'],
|
|
589
|
+
connectSrc: ["'self'"],
|
|
590
|
+
fontSrc: ["'self'"],
|
|
591
|
+
objectSrc: ["'none'"],
|
|
592
|
+
upgradeInsecureRequests: [],
|
|
593
|
+
},
|
|
594
|
+
},
|
|
595
|
+
})
|
|
596
|
+
);
|
|
597
|
+
```
|
|
598
|
+
|
|
599
|
+
```python
|
|
600
|
+
# FastAPI — secure
|
|
601
|
+
from secure import Secure
|
|
602
|
+
secure_headers = Secure()
|
|
603
|
+
|
|
604
|
+
@app.middleware('http')
|
|
605
|
+
async def set_secure_headers(request, call_next):
|
|
606
|
+
response = await call_next(request)
|
|
607
|
+
secure_headers.framework.fastapi(response)
|
|
608
|
+
return response
|
|
609
|
+
```
|
|
610
|
+
|
|
611
|
+
**Install:** `npm install helmet` / `pip install secure`
|
|
612
|
+
|
|
613
|
+
---
|
|
614
|
+
|
|
344
615
|
### TLS Bypass Fix (Node.js + Flutter/Dart)
|
|
345
616
|
|
|
346
617
|
**Root cause:** TLS certificate verification is explicitly disabled, allowing man-in-the-middle attacks.
|
|
@@ -0,0 +1,243 @@
|
|
|
1
|
+
# TDD Remediation: Proactive Hardening (Phase 4)
|
|
2
|
+
|
|
3
|
+
Once all known vulnerabilities are remediated, Phase 4 goes beyond patching holes to building layers of defense that make future vulnerabilities harder to introduce and easier to catch.
|
|
4
|
+
|
|
5
|
+
This phase is **additive and non-breaking** — apply each control independently, confirm the test suite remains green after each.
|
|
6
|
+
|
|
7
|
+
---
|
|
8
|
+
|
|
9
|
+
## 4a. Security Headers (Helmet)
|
|
10
|
+
|
|
11
|
+
If `helmet` is not already installed:
|
|
12
|
+
|
|
13
|
+
```bash
|
|
14
|
+
npm install helmet
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
Apply as the **first** middleware in your Express/Fastify app:
|
|
18
|
+
|
|
19
|
+
```javascript
|
|
20
|
+
const helmet = require('helmet');
|
|
21
|
+
app.use(helmet()); // sets X-Content-Type-Options, X-Frame-Options, HSTS, and more
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
For Next.js, add headers in `next.config.js`:
|
|
25
|
+
|
|
26
|
+
```javascript
|
|
27
|
+
const securityHeaders = [
|
|
28
|
+
{ key: 'X-Content-Type-Options', value: 'nosniff' },
|
|
29
|
+
{ key: 'X-Frame-Options', value: 'SAMEORIGIN' },
|
|
30
|
+
{ key: 'X-XSS-Protection', value: '1; mode=block' },
|
|
31
|
+
{ key: 'Referrer-Policy', value: 'strict-origin-when-cross-origin' },
|
|
32
|
+
{ key: 'Permissions-Policy', value: 'camera=(), microphone=(), geolocation=()' },
|
|
33
|
+
{ key: 'Strict-Transport-Security', value: 'max-age=63072000; includeSubDomains; preload' },
|
|
34
|
+
];
|
|
35
|
+
|
|
36
|
+
module.exports = {
|
|
37
|
+
async headers() {
|
|
38
|
+
return [{ source: '/(.*)', headers: securityHeaders }];
|
|
39
|
+
},
|
|
40
|
+
};
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
**Verify:** `curl -I https://localhost:3000/` — confirm headers are present.
|
|
44
|
+
|
|
45
|
+
---
|
|
46
|
+
|
|
47
|
+
## 4b. Content Security Policy (CSP)
|
|
48
|
+
|
|
49
|
+
A strict CSP is the most effective mitigation against XSS — even if a sanitization step is bypassed.
|
|
50
|
+
|
|
51
|
+
```javascript
|
|
52
|
+
app.use(
|
|
53
|
+
helmet.contentSecurityPolicy({
|
|
54
|
+
directives: {
|
|
55
|
+
defaultSrc: ["'self'"],
|
|
56
|
+
scriptSrc: ["'self'"], // no 'unsafe-inline' — use nonces for inline scripts
|
|
57
|
+
styleSrc: ["'self'", "'unsafe-inline'"],
|
|
58
|
+
imgSrc: ["'self'", 'data:', 'https:'],
|
|
59
|
+
connectSrc: ["'self'"],
|
|
60
|
+
fontSrc: ["'self'"],
|
|
61
|
+
objectSrc: ["'none'"],
|
|
62
|
+
frameAncestors: ["'none'"], // equivalent to X-Frame-Options: DENY
|
|
63
|
+
upgradeInsecureRequests: [],
|
|
64
|
+
},
|
|
65
|
+
})
|
|
66
|
+
);
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
**Test:** Use `https://csp-evaluator.withgoogle.com/` to score your policy.
|
|
70
|
+
|
|
71
|
+
---
|
|
72
|
+
|
|
73
|
+
## 4c. CSRF Protection
|
|
74
|
+
|
|
75
|
+
For any app that uses cookie-based sessions (not pure JWT/Authorization header flows):
|
|
76
|
+
|
|
77
|
+
```javascript
|
|
78
|
+
// Express — csurf (or csrf for ESM)
|
|
79
|
+
const csrf = require('csurf');
|
|
80
|
+
const csrfProtection = csrf({ cookie: true });
|
|
81
|
+
|
|
82
|
+
app.use(csrfProtection);
|
|
83
|
+
app.get('/form', (req, res) => res.render('form', { csrfToken: req.csrfToken() }));
|
|
84
|
+
|
|
85
|
+
// In the HTML form:
|
|
86
|
+
// <input type="hidden" name="_csrf" value="<%= csrfToken %>" />
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
For single-page apps using `fetch`, use the double-submit cookie pattern or a same-site cookie with `SameSite=Strict`.
|
|
90
|
+
|
|
91
|
+
```javascript
|
|
92
|
+
// SameSite cookies (simple and effective for modern browsers)
|
|
93
|
+
res.cookie('session', token, {
|
|
94
|
+
httpOnly: true,
|
|
95
|
+
secure: true,
|
|
96
|
+
sameSite: 'strict',
|
|
97
|
+
});
|
|
98
|
+
```
|
|
99
|
+
|
|
100
|
+
---
|
|
101
|
+
|
|
102
|
+
## 4d. Rate Limiting Audit
|
|
103
|
+
|
|
104
|
+
Verify these route categories all have rate limiting applied:
|
|
105
|
+
|
|
106
|
+
| Route type | Recommended limit |
|
|
107
|
+
|---|---|
|
|
108
|
+
| `/login`, `/register`, `/forgot-password` | 10 requests / 15 min / IP |
|
|
109
|
+
| `/api/` general endpoints | 100 requests / 1 min / IP |
|
|
110
|
+
| File upload endpoints | 5 requests / 1 min / IP |
|
|
111
|
+
| Password reset confirmation | 5 requests / 15 min / IP |
|
|
112
|
+
|
|
113
|
+
```bash
|
|
114
|
+
# Quick check — grep for unprotected POST routes
|
|
115
|
+
grep -rn "app\.post\|router\.post" src/ --include="*.js" | grep -v "limiter\|rateLimit"
|
|
116
|
+
```
|
|
117
|
+
|
|
118
|
+
---
|
|
119
|
+
|
|
120
|
+
## 4e. Dependency Vulnerability Audit
|
|
121
|
+
|
|
122
|
+
Run your ecosystem's audit tool and fix HIGH/CRITICAL findings:
|
|
123
|
+
|
|
124
|
+
```bash
|
|
125
|
+
# Node.js
|
|
126
|
+
npm audit --audit-level=high
|
|
127
|
+
npm audit fix # auto-fix where safe
|
|
128
|
+
|
|
129
|
+
# Python
|
|
130
|
+
pip install pip-audit
|
|
131
|
+
pip-audit
|
|
132
|
+
|
|
133
|
+
# Go
|
|
134
|
+
go install golang.org/x/vuln/cmd/govulncheck@latest
|
|
135
|
+
govulncheck ./...
|
|
136
|
+
|
|
137
|
+
# Ruby
|
|
138
|
+
gem install bundler-audit
|
|
139
|
+
bundle audit check --update
|
|
140
|
+
|
|
141
|
+
# Dart / Flutter
|
|
142
|
+
flutter pub outdated
|
|
143
|
+
dart pub deps # review transitive deps
|
|
144
|
+
```
|
|
145
|
+
|
|
146
|
+
Add dependency audits to CI so new vulnerabilities are caught on every PR:
|
|
147
|
+
|
|
148
|
+
```yaml
|
|
149
|
+
# .github/workflows/security-tests.yml (add this step)
|
|
150
|
+
- name: Dependency Audit
|
|
151
|
+
run: npm audit --audit-level=high
|
|
152
|
+
```
|
|
153
|
+
|
|
154
|
+
---
|
|
155
|
+
|
|
156
|
+
## 4f. Secrets in Git History
|
|
157
|
+
|
|
158
|
+
Scan for secrets that were committed and then removed — they still exist in git history.
|
|
159
|
+
|
|
160
|
+
```bash
|
|
161
|
+
# Using trufflehog (recommended)
|
|
162
|
+
npx trufflehog git file://. --only-verified
|
|
163
|
+
|
|
164
|
+
# Using gitleaks
|
|
165
|
+
brew install gitleaks # or download from github.com/gitleaks/gitleaks
|
|
166
|
+
gitleaks detect --source . -v
|
|
167
|
+
```
|
|
168
|
+
|
|
169
|
+
If secrets are found in history:
|
|
170
|
+
1. **Rotate the secret immediately** — treat it as compromised.
|
|
171
|
+
2. Use `git filter-repo` (not `filter-branch`) to rewrite history.
|
|
172
|
+
3. Force-push and notify all team members to re-clone.
|
|
173
|
+
|
|
174
|
+
Add a pre-commit hook to prevent future secret commits:
|
|
175
|
+
|
|
176
|
+
```bash
|
|
177
|
+
# .git/hooks/pre-commit (or use the --with-hooks flag when installing tdd-audit)
|
|
178
|
+
npx gitleaks protect --staged -v
|
|
179
|
+
```
|
|
180
|
+
|
|
181
|
+
---
|
|
182
|
+
|
|
183
|
+
## 4g. Error Handling Hardening
|
|
184
|
+
|
|
185
|
+
Production error responses must never reveal stack traces, file paths, or internal state.
|
|
186
|
+
|
|
187
|
+
```javascript
|
|
188
|
+
// Express — production error handler (place last, after all routes)
|
|
189
|
+
app.use((err, req, res, next) => {
|
|
190
|
+
const isDev = process.env.NODE_ENV !== 'production';
|
|
191
|
+
console.error(err); // log internally — never expose to client
|
|
192
|
+
res.status(err.status || 500).json({
|
|
193
|
+
error: isDev ? err.message : 'Internal server error',
|
|
194
|
+
...(isDev && { stack: err.stack }),
|
|
195
|
+
});
|
|
196
|
+
});
|
|
197
|
+
```
|
|
198
|
+
|
|
199
|
+
```python
|
|
200
|
+
# FastAPI
|
|
201
|
+
from fastapi.responses import JSONResponse
|
|
202
|
+
from fastapi.exceptions import RequestValidationError
|
|
203
|
+
|
|
204
|
+
@app.exception_handler(Exception)
|
|
205
|
+
async def generic_exception_handler(request, exc):
|
|
206
|
+
# Log internally
|
|
207
|
+
logger.error(f"Unhandled exception: {exc}", exc_info=True)
|
|
208
|
+
return JSONResponse(status_code=500, content={"error": "Internal server error"})
|
|
209
|
+
```
|
|
210
|
+
|
|
211
|
+
---
|
|
212
|
+
|
|
213
|
+
## 4h. Subresource Integrity (SRI)
|
|
214
|
+
|
|
215
|
+
For any third-party scripts or stylesheets loaded via CDN, add integrity hashes to prevent supply-chain injection:
|
|
216
|
+
|
|
217
|
+
```html
|
|
218
|
+
<!-- Generate hash: openssl dgst -sha384 -binary script.js | openssl base64 -A -->
|
|
219
|
+
<script
|
|
220
|
+
src="https://cdn.example.com/lib.min.js"
|
|
221
|
+
integrity="sha384-<hash>"
|
|
222
|
+
crossorigin="anonymous"
|
|
223
|
+
></script>
|
|
224
|
+
```
|
|
225
|
+
|
|
226
|
+
**Tool:** https://www.srihash.org/ generates the integrity attribute from any public URL.
|
|
227
|
+
|
|
228
|
+
---
|
|
229
|
+
|
|
230
|
+
## 4i. Hardening Verification Checklist
|
|
231
|
+
|
|
232
|
+
After Phase 4, confirm all of the following:
|
|
233
|
+
|
|
234
|
+
- [ ] `helmet()` applied before all routes; `X-Content-Type-Options: nosniff` in every response
|
|
235
|
+
- [ ] CSP header present; validated with csp-evaluator
|
|
236
|
+
- [ ] CSRF protection on all state-mutating routes (or SameSite=Strict cookies)
|
|
237
|
+
- [ ] Rate limiting on auth routes (429 returned after threshold — covered by red-phase test)
|
|
238
|
+
- [ ] `npm audit` / `pip-audit` / `govulncheck` shows 0 HIGH/CRITICAL issues
|
|
239
|
+
- [ ] `gitleaks` or `trufflehog` shows no verified secrets in history
|
|
240
|
+
- [ ] Production error handler returns generic messages; no stack traces in 5xx responses
|
|
241
|
+
- [ ] SRI hashes on all third-party CDN resources
|
|
242
|
+
- [ ] `*.env` files in `.gitignore`; no `.env` committed to git
|
|
243
|
+
- [ ] All cookies use `httpOnly: true`, `secure: true`, `sameSite: 'strict'` or `'lax'`
|
package/prompts/red-phase.md
CHANGED
|
@@ -166,6 +166,90 @@ test('SHOULD NOT store token in plain AsyncStorage', async () => {
|
|
|
166
166
|
});
|
|
167
167
|
```
|
|
168
168
|
|
|
169
|
+
### SSRF (Server-Side Request Forgery)
|
|
170
|
+
Supply a user-controlled URL pointing to an internal resource (e.g., `http://169.254.169.254/` AWS metadata).
|
|
171
|
+
Assert a 400 or 403 — not a 200 proxying internal content.
|
|
172
|
+
```javascript
|
|
173
|
+
const res = await request(app)
|
|
174
|
+
.post('/api/fetch-preview')
|
|
175
|
+
.send({ url: 'http://169.254.169.254/latest/meta-data/' });
|
|
176
|
+
expect(res.status).toBe(400); // currently fetches and returns internal data — RED
|
|
177
|
+
```
|
|
178
|
+
|
|
179
|
+
### Open Redirect
|
|
180
|
+
Supply a fully external URL as the redirect destination.
|
|
181
|
+
Assert a 400 or that the redirect stays within the same origin.
|
|
182
|
+
```javascript
|
|
183
|
+
const res = await request(app)
|
|
184
|
+
.get('/auth/callback')
|
|
185
|
+
.query({ redirect: 'https://evil.com/steal-token' });
|
|
186
|
+
expect(res.status).toBe(400); // currently 302 to attacker site — RED
|
|
187
|
+
// OR assert Location header is relative:
|
|
188
|
+
expect(res.headers.location).not.toMatch(/^https?:\/\//);
|
|
189
|
+
```
|
|
190
|
+
|
|
191
|
+
### NoSQL Injection
|
|
192
|
+
Supply a MongoDB operator object instead of a plain string value.
|
|
193
|
+
Assert the query is rejected or returns no data.
|
|
194
|
+
```javascript
|
|
195
|
+
const res = await request(app)
|
|
196
|
+
.post('/api/login')
|
|
197
|
+
.send({ username: { $gt: '' }, password: { $gt: '' } });
|
|
198
|
+
expect(res.status).toBe(400); // currently returns first user record — RED
|
|
199
|
+
```
|
|
200
|
+
|
|
201
|
+
### Mass Assignment
|
|
202
|
+
Submit extra fields that should not be user-settable (e.g., `isAdmin`, `role`).
|
|
203
|
+
Assert the privileged field was ignored.
|
|
204
|
+
```javascript
|
|
205
|
+
const res = await request(app)
|
|
206
|
+
.post('/api/users/register')
|
|
207
|
+
.send({ username: 'attacker', password: 'pass', isAdmin: true });
|
|
208
|
+
expect(res.status).toBe(201);
|
|
209
|
+
const user = await User.findOne({ username: 'attacker' });
|
|
210
|
+
expect(user.isAdmin).toBe(false); // currently set to true — RED
|
|
211
|
+
```
|
|
212
|
+
|
|
213
|
+
### Prototype Pollution
|
|
214
|
+
Submit a payload that sets `__proto__` to inject properties into Object.prototype.
|
|
215
|
+
Assert the injected property is not visible on a fresh `{}`.
|
|
216
|
+
```javascript
|
|
217
|
+
const res = await request(app)
|
|
218
|
+
.post('/api/settings/merge')
|
|
219
|
+
.send({ '__proto__': { polluted: true } });
|
|
220
|
+
expect(res.status).toBe(200);
|
|
221
|
+
expect({}.polluted).toBeUndefined(); // currently true — RED
|
|
222
|
+
```
|
|
223
|
+
|
|
224
|
+
### Weak Crypto (Password Hashing)
|
|
225
|
+
Hash a known password and assert the resulting hash is not a raw MD5/SHA1 hex string.
|
|
226
|
+
```javascript
|
|
227
|
+
const bcrypt = require('bcrypt');
|
|
228
|
+
const user = await User.create({ email: 'x@x.com', password: 'mypassword' });
|
|
229
|
+
// An MD5 hash of 'mypassword' is 34819d7beeabb9260a5c854bc85b3e44
|
|
230
|
+
expect(user.passwordHash).not.toBe('34819d7beeabb9260a5c854bc85b3e44');
|
|
231
|
+
// A proper bcrypt hash starts with $2b$
|
|
232
|
+
expect(user.passwordHash).toMatch(/^\$2[aby]\$/); // currently fails — RED
|
|
233
|
+
```
|
|
234
|
+
|
|
235
|
+
### Missing Rate Limiting
|
|
236
|
+
Send 10 rapid login attempts; assert the 11th is throttled (429).
|
|
237
|
+
```javascript
|
|
238
|
+
for (let i = 0; i < 10; i++) {
|
|
239
|
+
await request(app).post('/api/auth/login').send({ email: 'x@x.com', password: 'wrong' });
|
|
240
|
+
}
|
|
241
|
+
const res = await request(app).post('/api/auth/login').send({ email: 'x@x.com', password: 'wrong' });
|
|
242
|
+
expect(res.status).toBe(429); // currently 401 — rate limit not enforced — RED
|
|
243
|
+
```
|
|
244
|
+
|
|
245
|
+
### Missing Security Headers
|
|
246
|
+
Assert a response includes the `X-Content-Type-Options` and `X-Frame-Options` headers set by Helmet.
|
|
247
|
+
```javascript
|
|
248
|
+
const res = await request(app).get('/');
|
|
249
|
+
expect(res.headers['x-content-type-options']).toBe('nosniff'); // currently absent — RED
|
|
250
|
+
expect(res.headers['x-frame-options']).toBeDefined(); // currently absent — RED
|
|
251
|
+
```
|
|
252
|
+
|
|
169
253
|
### Flutter / Dart (flutter_test)
|
|
170
254
|
```dart
|
|
171
255
|
import 'package:flutter_test/flutter_test.dart';
|
|
@@ -32,6 +32,16 @@ Go through this checklist before closing the vulnerability:
|
|
|
32
32
|
- [ ] **Offline token refresh still works** — `SecureStore.getItemAsync` is called in the right lifecycle (not before `SecureStore.isAvailableAsync()` on web)
|
|
33
33
|
- [ ] **Deep link params validated** — any `route.params` passed to API calls are sanitized or type-checked
|
|
34
34
|
|
|
35
|
+
**New vulnerability class additions:**
|
|
36
|
+
- [ ] **SSRF allowlist verified** — `validateExternalUrl` throws on internal IPs and non-allowlisted hosts; confirm `169.254.x.x` and `10.x.x.x` are blocked
|
|
37
|
+
- [ ] **Open redirect uses relative-only check** — `/^https?:\/\//` and `//` prefix both rejected; confirm legitimate in-app redirects still work
|
|
38
|
+
- [ ] **NoSQL injection sanitized** — `express-mongo-sanitize` or equivalent applied globally; confirm `{ $gt: '' }` payloads return 400
|
|
39
|
+
- [ ] **Mass assignment uses field allowlist** — no `req.body` passed directly to ORM; confirm privileged fields (`isAdmin`, `role`) cannot be set by user
|
|
40
|
+
- [ ] **Prototype pollution sanitizes keys** — `__proto__`, `constructor`, `prototype` keys stripped before any merge; confirm `{}.polluted` is still `undefined` after merge
|
|
41
|
+
- [ ] **Passwords use bcrypt/argon2** — no `createHash('md5')` or `createHash('sha1')` for passwords; `bcrypt.compare` used on login
|
|
42
|
+
- [ ] **Rate limiting active on auth routes** — `/login` and `/register` return 429 after threshold; general API routes have a broader limit
|
|
43
|
+
- [ ] **Helmet applied before all routes** — `X-Content-Type-Options: nosniff` and `X-Frame-Options` present in response; CSP header present
|
|
44
|
+
|
|
35
45
|
**Flutter additions:**
|
|
36
46
|
- [ ] **`flutter_secure_storage` in `pubspec.yaml`** — dependency present and `flutter pub get` ran
|
|
37
47
|
- [ ] **No remaining `SharedPreferences` calls for sensitive keys** — grep for `prefs.getString('token')`, `prefs.setString('password', ...)`
|
package/workflows/tdd-audit.md
CHANGED
|
@@ -11,6 +11,13 @@ Follow the full Auto-Audit protocol from `auto-audit.md`:
|
|
|
11
11
|
- Write the exploit test (Red — must fail)
|
|
12
12
|
- Apply the patch (Green — test must pass)
|
|
13
13
|
- Run the full suite (Refactor — no regressions)
|
|
14
|
-
4. **
|
|
14
|
+
4. **Harden** the codebase proactively after all vulnerabilities are patched:
|
|
15
|
+
- Security headers (Helmet / CSP)
|
|
16
|
+
- Rate limiting on auth routes
|
|
17
|
+
- Dependency vulnerability audit (npm audit / pip-audit / govulncheck)
|
|
18
|
+
- Secret history scan (gitleaks / trufflehog)
|
|
19
|
+
- Production error handling (no stack traces)
|
|
20
|
+
- CSRF protection and secure cookie flags
|
|
21
|
+
5. **Report** a final Remediation Summary table when all issues are addressed.
|
|
15
22
|
|
|
16
23
|
Do not skip steps. Do not advance to the next vulnerability until the current one is fully proven closed by a passing test.
|