@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 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: unparameterized SQL queries, missing ownership checks, unsafe HTML rendering, command injection sinks, sensitive data in storage, TLS bypasses, hardcoded secrets. Full search patterns are in [auto-audit.md](./prompts/auto-audit.md).
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lhi/tdd-audit",
3
- "version": "1.2.0",
3
+ "version": "1.3.0",
4
4
  "description": "Anti-Gravity Skill for TDD Remediation. Patches security vulnerabilities using a Red-Green-Refactor protocol with automated exploit tests.",
5
5
  "main": "index.js",
6
6
  "bin": {
@@ -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
 
@@ -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'`
@@ -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', ...)`
@@ -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. **Report** a final Remediation Summary table when all issues are addressed.
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.