@lhi/tdd-audit 1.2.0 → 1.4.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
@@ -4,6 +4,14 @@ const fs = require('fs');
4
4
  const path = require('path');
5
5
  const os = require('os');
6
6
 
7
+ const {
8
+ detectFramework,
9
+ detectAppFramework,
10
+ detectTestBaseDir,
11
+ quickScan,
12
+ printFindings,
13
+ } = require('./lib/scanner');
14
+
7
15
  const args = process.argv.slice(2);
8
16
  const isLocal = args.includes('--local');
9
17
  const isClaude = args.includes('--claude');
@@ -20,206 +28,22 @@ const targetWorkflowDir = isClaude
20
28
  ? path.join(agentBaseDir, agentDirName, 'commands')
21
29
  : path.join(agentBaseDir, agentDirName, 'workflows');
22
30
 
23
- // ─── 1. Framework Detection ──────────────────────────────────────────────────
24
-
25
- function detectFramework() {
26
- // Flutter / Dart — check before package.json since a Flutter project may have both
27
- if (fs.existsSync(path.join(projectDir, 'pubspec.yaml'))) return 'flutter';
28
-
29
- const pkgPath = path.join(projectDir, 'package.json');
30
- if (fs.existsSync(pkgPath)) {
31
- try {
32
- const pkg = JSON.parse(fs.readFileSync(pkgPath, 'utf8'));
33
- const deps = { ...(pkg.dependencies || {}), ...(pkg.devDependencies || {}) };
34
- if (deps.vitest) return 'vitest';
35
- if (deps.jest || deps.supertest) return 'jest';
36
- if (deps.mocha) return 'mocha';
37
- } catch {}
38
- }
39
- if (
40
- fs.existsSync(path.join(projectDir, 'pytest.ini')) ||
41
- fs.existsSync(path.join(projectDir, 'pyproject.toml')) ||
42
- fs.existsSync(path.join(projectDir, 'setup.py')) ||
43
- fs.existsSync(path.join(projectDir, 'requirements.txt'))
44
- ) return 'pytest';
45
- if (fs.existsSync(path.join(projectDir, 'go.mod'))) return 'go';
46
- return 'jest';
47
- }
48
-
49
- // Detect the UI framework for richer scan context (React, Next.js, RN, Expo, Flutter)
50
- function detectAppFramework() {
51
- if (fs.existsSync(path.join(projectDir, 'pubspec.yaml'))) return 'flutter';
52
- const pkgPath = path.join(projectDir, 'package.json');
53
- if (fs.existsSync(pkgPath)) {
54
- try {
55
- const pkg = JSON.parse(fs.readFileSync(pkgPath, 'utf8'));
56
- const deps = { ...(pkg.dependencies || {}), ...(pkg.devDependencies || {}) };
57
- if (deps.expo) return 'expo';
58
- if (deps['react-native']) return 'react-native';
59
- if (deps.next) return 'nextjs';
60
- if (deps.react) return 'react';
61
- } catch {}
62
- }
63
- return null;
64
- }
65
-
66
- const appFramework = detectAppFramework();
67
-
68
- const framework = detectFramework();
69
-
70
- // ─── 2. Test Directory Detection ─────────────────────────────────────────────
71
-
72
- function detectTestBaseDir() {
73
- // Respect an existing convention before inventing one
74
- const candidates = ['__tests__', 'tests', 'test', 'spec'];
75
- for (const dir of candidates) {
76
- if (fs.existsSync(path.join(projectDir, dir))) return dir;
77
- }
78
- // Framework-informed defaults when no directory exists yet
79
- if (framework === 'pytest') return 'tests';
80
- if (framework === 'go') return 'test';
81
- return '__tests__';
82
- }
83
-
84
- const testBaseDir = detectTestBaseDir();
31
+ const appFramework = detectAppFramework(projectDir);
32
+ const framework = detectFramework(projectDir);
33
+ const testBaseDir = detectTestBaseDir(projectDir, framework);
85
34
  const targetTestDir = path.join(projectDir, testBaseDir, 'security');
86
35
 
87
- // ─── 3. Quick Scan ───────────────────────────────────────────────────────────
88
-
89
- const VULN_PATTERNS = [
90
- { name: 'SQL Injection', severity: 'CRITICAL', pattern: /(`SELECT[^`]*\$\{|"SELECT[^"]*"\s*\+|execute\(f"|cursor\.execute\(.*%s|\.query\(`[^`]*\$\{)/i },
91
- { name: 'Command Injection', severity: 'CRITICAL', pattern: /\bexec(Sync)?\s*\(.*req\.(params|body|query)|subprocess\.(run|Popen|call)\([^)]*shell\s*=\s*True/i },
92
- { name: 'IDOR', severity: 'HIGH', pattern: /findById\s*\(\s*req\.(params|body|query)\.|findOne\s*\(\s*\{[^}]*id\s*:\s*req\.(params|body|query)/i },
93
- { name: 'XSS', severity: 'HIGH', pattern: /[^/]innerHTML\s*=(?!=)|dangerouslySetInnerHTML\s*=\s*\{\{|document\.write\s*\(|res\.send\s*\(`[^`]*\$\{req\./i },
94
- { name: 'Path Traversal', severity: 'HIGH', pattern: /(readFile|sendFile|createReadStream|open)\s*\(.*req\.(params|body|query)|path\.join\s*\([^)]*req\.(params|body|query)/i },
95
- { name: 'Broken Auth', severity: 'HIGH', pattern: /jwt\.decode\s*\((?![^;]*\.verify)|verify\s*:\s*false|secret\s*=\s*['"][a-z0-9]{1,20}['"]/i },
96
- // Vibecoding / mobile stacks
97
- { name: 'Sensitive Storage', severity: 'HIGH', pattern: /(localStorage|AsyncStorage)\.setItem\s*\(\s*['"](token|password|secret|auth|jwt|api.?key)['"]/i },
98
- { name: 'TLS Bypass', severity: 'CRITICAL', pattern: /badCertificateCallback[^;]*=\s*true|rejectUnauthorized\s*:\s*false|NODE_TLS_REJECT_UNAUTHORIZED\s*=\s*['"]?0/i },
99
- { name: 'Hardcoded Secret', severity: 'CRITICAL', skipInTests: true, pattern: /(?:const|final|var|let|static)\s+(?:API_KEY|PRIVATE_KEY|SECRET_KEY|ACCESS_TOKEN|CLIENT_SECRET)\s*=\s*['"][A-Za-z0-9+/=_\-]{20,}['"]/i },
100
- { name: 'eval() Injection', severity: 'HIGH', pattern: /\beval\s*\([^)]*(?:route\.params|searchParams\.get|req\.(query|body)|params\[)/i },
101
- // Common vibecoding anti-patterns
102
- { name: 'Insecure Random', severity: 'HIGH', pattern: /(?:token|sessionId|nonce|secret|csrf)\w*\s*=.*Math\.random\(\)|Math\.random\(\).*(?:token|session|nonce|secret)/i },
103
- { name: 'Sensitive Log', severity: 'MEDIUM', skipInTests: true, pattern: /console\.(log|info|debug)\([^)]*(?:token|password|secret|jwt|authorization|apiKey|api_key)/i },
104
- { name: 'Secret Fallback', severity: 'HIGH', pattern: /process\.env\.\w+\s*\|\|\s*['"][A-Za-z0-9+/=_\-]{10,}['"]/i },
105
- ];
106
-
107
- const SCAN_EXTENSIONS = new Set(['.js', '.ts', '.jsx', '.tsx', '.mjs', '.py', '.go', '.dart']);
108
- const SKIP_DIRS = new Set(['node_modules', '.git', 'dist', 'build', '.next', 'out', '__pycache__', 'venv', '.venv', 'vendor', '.expo', '.dart_tool', '.pub-cache']);
109
-
110
- function* walkFiles(dir) {
111
- let entries;
112
- try { entries = fs.readdirSync(dir, { withFileTypes: true }); } catch { return; }
113
- for (const entry of entries) {
114
- if (SKIP_DIRS.has(entry.name)) continue;
115
- const fullPath = path.join(dir, entry.name);
116
- if (entry.isDirectory()) yield* walkFiles(fullPath);
117
- else if (SCAN_EXTENSIONS.has(path.extname(entry.name))) yield fullPath;
118
- }
119
- }
120
-
121
- // Returns true for test/spec files — used to down-weight false-positive-prone patterns
122
- function isTestFile(filePath) {
123
- const rel = path.relative(projectDir, filePath).replace(/\\/g, '/');
124
- return /[._-]test\.[a-z]+$|[._-]spec\.[a-z]+$|_test\.dart$|\/tests?\/|\/spec\/|\/test_/.test(rel);
125
- }
126
-
127
- // Scan app.json / app.config.* for embedded secrets (common Expo vibecoding issue)
128
- function scanAppConfig() {
129
- const findings = [];
130
- const configCandidates = ['app.json', 'app.config.js', 'app.config.ts'];
131
- const secretPattern = /['"]?(?:apiKey|api_key|secret|privateKey|accessToken|clientSecret)['"]?\s*[:=]\s*['"][A-Za-z0-9+/=_\-]{20,}['"]/i;
132
-
133
- for (const name of configCandidates) {
134
- const filePath = path.join(projectDir, name);
135
- if (!fs.existsSync(filePath)) continue;
136
- let lines;
137
- try { lines = fs.readFileSync(filePath, 'utf8').split('\n'); } catch { continue; }
138
- for (let i = 0; i < lines.length; i++) {
139
- if (secretPattern.test(lines[i])) {
140
- findings.push({
141
- severity: 'CRITICAL',
142
- name: 'Config Secret',
143
- file: name,
144
- line: i + 1,
145
- snippet: lines[i].trim().slice(0, 80),
146
- inTestFile: false,
147
- });
148
- }
149
- }
150
- }
151
- return findings;
152
- }
153
-
154
- function quickScan() {
155
- const findings = [];
156
- for (const filePath of walkFiles(projectDir)) {
157
- const inTest = isTestFile(filePath);
158
- let lines;
159
- try { lines = fs.readFileSync(filePath, 'utf8').split('\n'); } catch { continue; }
160
- for (let i = 0; i < lines.length; i++) {
161
- for (const vuln of VULN_PATTERNS) {
162
- if (vuln.pattern.test(lines[i])) {
163
- findings.push({
164
- severity: vuln.severity,
165
- name: vuln.name,
166
- file: path.relative(projectDir, filePath),
167
- line: i + 1,
168
- snippet: lines[i].trim().slice(0, 80),
169
- inTestFile: inTest,
170
- likelyFalsePositive: inTest && !!vuln.skipInTests,
171
- });
172
- break; // one finding per line
173
- }
174
- }
175
- }
176
- }
177
- return [...findings, ...scanAppConfig()];
178
- }
179
-
180
- function printFindings(findings) {
181
- if (findings.length === 0) {
182
- console.log(' ✅ No obvious vulnerability patterns detected.\n');
183
- return;
184
- }
185
- const real = findings.filter(f => !f.likelyFalsePositive);
186
- const noisy = findings.filter(f => f.likelyFalsePositive);
187
-
188
- const bySeverity = { CRITICAL: [], HIGH: [], MEDIUM: [], LOW: [] };
189
- for (const f of real) (bySeverity[f.severity] || bySeverity.LOW).push(f);
190
- const icons = { CRITICAL: '🔴', HIGH: '🟠', MEDIUM: '🟡', LOW: '🔵' };
191
-
192
- console.log(`\n Found ${real.length} potential issue(s)${noisy.length ? ` (+${noisy.length} in test files — see below)` : ''}:\n`);
193
- for (const [sev, list] of Object.entries(bySeverity)) {
194
- if (!list.length) continue;
195
- for (const f of list) {
196
- const testBadge = f.inTestFile ? ' [test file]' : '';
197
- console.log(` ${icons[sev]} [${sev}] ${f.name} — ${f.file}:${f.line}${testBadge}`);
198
- console.log(` ${f.snippet}`);
199
- }
200
- }
201
-
202
- if (noisy.length) {
203
- console.log('\n ⚪ Likely intentional (in test files — verify manually):');
204
- for (const f of noisy) {
205
- console.log(` ${f.name} — ${f.file}:${f.line}`);
206
- }
207
- }
208
-
209
- console.log('\n Run /tdd-audit in your agent to remediate.\n');
210
- }
211
-
212
- // ─── 4. Scan-only early exit ──────────────────────────────────────────────────
36
+ // ─── Scan-only early exit ─────────────────────────────────────────────────────
213
37
 
214
38
  if (scanOnly) {
215
39
  process.stdout.write('\n🔍 Scanning for vulnerability patterns...');
216
- const findings = quickScan();
40
+ const findings = quickScan(projectDir);
217
41
  process.stdout.write('\n');
218
42
  printFindings(findings);
219
43
  process.exit(0);
220
44
  }
221
45
 
222
- // ─── 5. Install Skill Files ───────────────────────────────────────────────────
46
+ // ─── Install Skill Files ──────────────────────────────────────────────────────
223
47
 
224
48
  const appLabel = appFramework ? `, app: ${appFramework}` : '';
225
49
  console.log(`\nInstalling TDD Remediation Skill (${isLocal ? 'local' : 'global'}, framework: ${framework}${appLabel}, test dir: ${testBaseDir}/)...\n`);
@@ -232,7 +56,7 @@ for (const item of ['SKILL.md', 'prompts', 'templates']) {
232
56
  if (fs.existsSync(src)) fs.cpSync(src, dest, { recursive: true });
233
57
  }
234
58
 
235
- // ─── 5. Scaffold Security Test Boilerplate ────────────────────────────────────
59
+ // ─── Scaffold Security Test Boilerplate ───────────────────────────────────────
236
60
 
237
61
  if (!fs.existsSync(targetTestDir)) {
238
62
  fs.mkdirSync(targetTestDir, { recursive: true });
@@ -257,7 +81,7 @@ if (!fs.existsSync(destTest) && fs.existsSync(srcTest)) {
257
81
  console.log(`✅ Scaffolded ${path.relative(projectDir, destTest)}`);
258
82
  }
259
83
 
260
- // ─── 6. Install Workflow Shortcode ────────────────────────────────────────────
84
+ // ─── Install Workflow Shortcode ───────────────────────────────────────────────
261
85
 
262
86
  if (!fs.existsSync(targetWorkflowDir)) fs.mkdirSync(targetWorkflowDir, { recursive: true });
263
87
  const srcWorkflow = path.join(__dirname, 'workflows', 'tdd-audit.md');
@@ -267,7 +91,7 @@ if (fs.existsSync(srcWorkflow)) {
267
91
  console.log(`✅ Installed /tdd-audit workflow shortcode`);
268
92
  }
269
93
 
270
- // ─── 7. Inject test:security into package.json ────────────────────────────────
94
+ // ─── Inject test:security into package.json ───────────────────────────────────
271
95
 
272
96
  const pkgPath = path.join(projectDir, 'package.json');
273
97
  if (framework !== 'pytest' && framework !== 'go' && fs.existsSync(pkgPath)) {
@@ -277,10 +101,10 @@ if (framework !== 'pytest' && framework !== 'go' && fs.existsSync(pkgPath)) {
277
101
  pkg.scripts = pkg.scripts || {};
278
102
  const secDir = `${testBaseDir}/security`;
279
103
  pkg.scripts['test:security'] = {
280
- jest: `jest --testPathPattern=${secDir} --forceExit`,
104
+ jest: `jest --testPathPatterns=${secDir} --forceExit`,
281
105
  vitest: `vitest run ${secDir}`,
282
106
  mocha: `mocha '${secDir}/**/*.spec.js'`,
283
- }[framework] || `jest --testPathPattern=${secDir} --forceExit`;
107
+ }[framework] || `jest --testPathPatterns=${secDir} --forceExit`;
284
108
  fs.writeFileSync(pkgPath, JSON.stringify(pkg, null, 2) + '\n');
285
109
  console.log(`✅ Added "test:security" script to package.json`);
286
110
  } else {
@@ -291,31 +115,43 @@ if (framework !== 'pytest' && framework !== 'go' && fs.existsSync(pkgPath)) {
291
115
  }
292
116
  }
293
117
 
294
- // ─── 8. Scaffold CI Workflow ─────────────────────────────────────────────────
118
+ // ─── Scaffold CI Workflows ────────────────────────────────────────────────────
295
119
 
296
120
  const ciWorkflowDir = path.join(projectDir, '.github', 'workflows');
297
- const ciWorkflowPath = path.join(ciWorkflowDir, 'security-tests.yml');
298
-
299
- if (!fs.existsSync(ciWorkflowPath)) {
300
- const ciTemplateMap = {
301
- jest: 'security-tests.node.yml',
302
- vitest: 'security-tests.node.yml',
303
- mocha: 'security-tests.node.yml',
304
- pytest: 'security-tests.python.yml',
305
- go: 'security-tests.go.yml',
306
- flutter: 'security-tests.flutter.yml',
307
- };
308
- const ciTemplatePath = path.join(__dirname, 'templates', 'workflows', ciTemplateMap[framework]);
309
- if (fs.existsSync(ciTemplatePath)) {
310
- fs.mkdirSync(ciWorkflowDir, { recursive: true });
311
- fs.copyFileSync(ciTemplatePath, ciWorkflowPath);
312
- console.log(`✅ Scaffolded .github/workflows/security-tests.yml`);
121
+ fs.mkdirSync(ciWorkflowDir, { recursive: true });
122
+
123
+ const ciWorkflows = [
124
+ {
125
+ destName: 'security-tests.yml',
126
+ templateMap: {
127
+ jest: 'security-tests.node.yml', vitest: 'security-tests.node.yml',
128
+ mocha: 'security-tests.node.yml', pytest: 'security-tests.python.yml',
129
+ go: 'security-tests.go.yml', flutter: 'security-tests.flutter.yml',
130
+ },
131
+ },
132
+ {
133
+ destName: 'ci.yml',
134
+ templateMap: {
135
+ jest: 'ci.node.yml', vitest: 'ci.node.yml', mocha: 'ci.node.yml',
136
+ pytest: 'ci.python.yml', go: 'ci.go.yml', flutter: 'ci.flutter.yml',
137
+ },
138
+ },
139
+ ];
140
+
141
+ for (const { destName, templateMap } of ciWorkflows) {
142
+ const destPath = path.join(ciWorkflowDir, destName);
143
+ if (!fs.existsSync(destPath)) {
144
+ const srcPath = path.join(__dirname, 'templates', 'workflows', templateMap[framework]);
145
+ if (fs.existsSync(srcPath)) {
146
+ fs.copyFileSync(srcPath, destPath);
147
+ console.log(`✅ Scaffolded .github/workflows/${destName}`);
148
+ }
149
+ } else {
150
+ console.log(` .github/workflows/${destName} already exists — skipped`);
313
151
  }
314
- } else {
315
- console.log(` .github/workflows/security-tests.yml already exists — skipped`);
316
152
  }
317
153
 
318
- // ─── 9. Pre-commit Hook (opt-in) ─────────────────────────────────────────────
154
+ // ─── Pre-commit Hook (opt-in) ─────────────────────────────────────────────────
319
155
 
320
156
  if (withHooks) {
321
157
  const gitDir = path.join(projectDir, '.git');
@@ -353,11 +189,11 @@ if (withHooks) {
353
189
  }
354
190
  }
355
191
 
356
- // ─── 10. Quick Scan ──────────────────────────────────────────────────────────
192
+ // ─── Quick Scan ───────────────────────────────────────────────────────────────
357
193
 
358
194
  if (!skipScan) {
359
195
  process.stdout.write('\n🔍 Scanning for vulnerability patterns...');
360
- const findings = quickScan();
196
+ const findings = quickScan(projectDir);
361
197
  process.stdout.write('\n');
362
198
  printFindings(findings);
363
199
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lhi/tdd-audit",
3
- "version": "1.2.0",
3
+ "version": "1.4.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": {
@@ -16,8 +16,10 @@
16
16
  "LICENSE"
17
17
  ],
18
18
  "scripts": {
19
- "test": "node index.js --local --skip-scan && echo 'Smoke test passed'",
20
- "test:security": "jest --testPathPattern=__tests__/security --forceExit"
19
+ "test": "jest --forceExit",
20
+ "test:unit": "jest --testPathPatterns=__tests__/unit --forceExit --coverage",
21
+ "test:security": "jest --testPathPatterns=__tests__/security --forceExit",
22
+ "test:smoke": "node index.js --local --skip-scan && echo 'Smoke test passed'"
21
23
  },
22
24
  "keywords": [
23
25
  "security",
@@ -44,5 +46,8 @@
44
46
  "node": ">=16.7.0"
45
47
  },
46
48
  "author": "Kyra Lee",
47
- "license": "MIT"
49
+ "license": "MIT",
50
+ "devDependencies": {
51
+ "jest": "^30.3.0"
52
+ }
48
53
  }
@@ -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'`
@@ -99,8 +99,12 @@ const request = require('supertest');
99
99
  const app = require('../../app');
100
100
 
101
101
  describe('[VulnType] - Red Phase', () => {
102
+ let server;
103
+ beforeAll(() => { server = app.listen(0); });
104
+ afterAll(() => server.close());
105
+
102
106
  it('SHOULD block [exploit description]', async () => {
103
- const res = await request(app)
107
+ const res = await request(server)
104
108
  .post('/api/vulnerable-endpoint')
105
109
  .send({ input: '<exploit payload>' });
106
110
 
@@ -166,6 +170,90 @@ test('SHOULD NOT store token in plain AsyncStorage', async () => {
166
170
  });
167
171
  ```
168
172
 
173
+ ### SSRF (Server-Side Request Forgery)
174
+ Supply a user-controlled URL pointing to an internal resource (e.g., `http://169.254.169.254/` AWS metadata).
175
+ Assert a 400 or 403 — not a 200 proxying internal content.
176
+ ```javascript
177
+ const res = await request(app)
178
+ .post('/api/fetch-preview')
179
+ .send({ url: 'http://169.254.169.254/latest/meta-data/' });
180
+ expect(res.status).toBe(400); // currently fetches and returns internal data — RED
181
+ ```
182
+
183
+ ### Open Redirect
184
+ Supply a fully external URL as the redirect destination.
185
+ Assert a 400 or that the redirect stays within the same origin.
186
+ ```javascript
187
+ const res = await request(app)
188
+ .get('/auth/callback')
189
+ .query({ redirect: 'https://evil.com/steal-token' });
190
+ expect(res.status).toBe(400); // currently 302 to attacker site — RED
191
+ // OR assert Location header is relative:
192
+ expect(res.headers.location).not.toMatch(/^https?:\/\//);
193
+ ```
194
+
195
+ ### NoSQL Injection
196
+ Supply a MongoDB operator object instead of a plain string value.
197
+ Assert the query is rejected or returns no data.
198
+ ```javascript
199
+ const res = await request(app)
200
+ .post('/api/login')
201
+ .send({ username: { $gt: '' }, password: { $gt: '' } });
202
+ expect(res.status).toBe(400); // currently returns first user record — RED
203
+ ```
204
+
205
+ ### Mass Assignment
206
+ Submit extra fields that should not be user-settable (e.g., `isAdmin`, `role`).
207
+ Assert the privileged field was ignored.
208
+ ```javascript
209
+ const res = await request(app)
210
+ .post('/api/users/register')
211
+ .send({ username: 'attacker', password: 'pass', isAdmin: true });
212
+ expect(res.status).toBe(201);
213
+ const user = await User.findOne({ username: 'attacker' });
214
+ expect(user.isAdmin).toBe(false); // currently set to true — RED
215
+ ```
216
+
217
+ ### Prototype Pollution
218
+ Submit a payload that sets `__proto__` to inject properties into Object.prototype.
219
+ Assert the injected property is not visible on a fresh `{}`.
220
+ ```javascript
221
+ const res = await request(app)
222
+ .post('/api/settings/merge')
223
+ .send({ '__proto__': { polluted: true } });
224
+ expect(res.status).toBe(200);
225
+ expect({}.polluted).toBeUndefined(); // currently true — RED
226
+ ```
227
+
228
+ ### Weak Crypto (Password Hashing)
229
+ Hash a known password and assert the resulting hash is not a raw MD5/SHA1 hex string.
230
+ ```javascript
231
+ const bcrypt = require('bcrypt');
232
+ const user = await User.create({ email: 'x@x.com', password: 'mypassword' });
233
+ // An MD5 hash of 'mypassword' is 34819d7beeabb9260a5c854bc85b3e44
234
+ expect(user.passwordHash).not.toBe('34819d7beeabb9260a5c854bc85b3e44');
235
+ // A proper bcrypt hash starts with $2b$
236
+ expect(user.passwordHash).toMatch(/^\$2[aby]\$/); // currently fails — RED
237
+ ```
238
+
239
+ ### Missing Rate Limiting
240
+ Send 10 rapid login attempts; assert the 11th is throttled (429).
241
+ ```javascript
242
+ for (let i = 0; i < 10; i++) {
243
+ await request(app).post('/api/auth/login').send({ email: 'x@x.com', password: 'wrong' });
244
+ }
245
+ const res = await request(app).post('/api/auth/login').send({ email: 'x@x.com', password: 'wrong' });
246
+ expect(res.status).toBe(429); // currently 401 — rate limit not enforced — RED
247
+ ```
248
+
249
+ ### Missing Security Headers
250
+ Assert a response includes the `X-Content-Type-Options` and `X-Frame-Options` headers set by Helmet.
251
+ ```javascript
252
+ const res = await request(app).get('/');
253
+ expect(res.headers['x-content-type-options']).toBe('nosniff'); // currently absent — RED
254
+ expect(res.headers['x-frame-options']).toBeDefined(); // currently absent — RED
255
+ ```
256
+
169
257
  ### Flutter / Dart (flutter_test)
170
258
  ```dart
171
259
  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', ...)`
@@ -0,0 +1,43 @@
1
+ name: CI
2
+
3
+ on:
4
+ push:
5
+ branches: [main, master, develop]
6
+ pull_request:
7
+ branches: [main, master, develop]
8
+
9
+ jobs:
10
+ test:
11
+ name: Test & Analyze
12
+ runs-on: ubuntu-latest
13
+
14
+ steps:
15
+ - uses: actions/checkout@v4
16
+
17
+ - name: Set up Flutter
18
+ uses: subosito/flutter-action@v2
19
+ with:
20
+ flutter-version: stable
21
+ cache: true
22
+
23
+ - name: Install dependencies
24
+ run: flutter pub get
25
+
26
+ - name: Analyze (dart analyze)
27
+ run: dart analyze --fatal-infos
28
+
29
+ - name: Format check
30
+ run: dart format --output=none --set-exit-if-changed .
31
+
32
+ - name: Unit tests with coverage
33
+ run: flutter test --coverage
34
+
35
+ - name: Security tests
36
+ run: flutter test test/security/
37
+
38
+ - name: Upload coverage
39
+ uses: actions/upload-artifact@v4
40
+ with:
41
+ name: coverage
42
+ path: coverage/lcov.info
43
+ retention-days: 7
@@ -0,0 +1,45 @@
1
+ name: CI
2
+
3
+ on:
4
+ push:
5
+ branches: [main, master, develop]
6
+ pull_request:
7
+ branches: [main, master, develop]
8
+
9
+ jobs:
10
+ test:
11
+ name: Test & Lint
12
+ runs-on: ubuntu-latest
13
+
14
+ strategy:
15
+ matrix:
16
+ go-version: ["1.21", "1.22", "1.23"]
17
+
18
+ steps:
19
+ - uses: actions/checkout@v4
20
+
21
+ - name: Set up Go ${{ matrix.go-version }}
22
+ uses: actions/setup-go@v5
23
+ with:
24
+ go-version: ${{ matrix.go-version }}
25
+ cache: true
26
+
27
+ - name: Lint (staticcheck)
28
+ uses: dominikh/staticcheck-action@v1
29
+ with:
30
+ version: latest
31
+ install-go: false
32
+
33
+ - name: Run tests with coverage
34
+ run: go test ./... -coverprofile=coverage.out -covermode=atomic -v
35
+
36
+ - name: Security tests
37
+ run: go test ./security/... -v
38
+
39
+ - name: Upload coverage
40
+ if: matrix.go-version == '1.22'
41
+ uses: actions/upload-artifact@v4
42
+ with:
43
+ name: coverage
44
+ path: coverage.out
45
+ retention-days: 7
@@ -0,0 +1,45 @@
1
+ name: CI
2
+
3
+ on:
4
+ push:
5
+ branches: [main, master, develop]
6
+ pull_request:
7
+ branches: [main, master, develop]
8
+
9
+ jobs:
10
+ test:
11
+ name: Test & Lint
12
+ runs-on: ubuntu-latest
13
+
14
+ strategy:
15
+ matrix:
16
+ node-version: [18.x, 20.x, 22.x]
17
+
18
+ steps:
19
+ - uses: actions/checkout@v4
20
+
21
+ - name: Use Node.js ${{ matrix.node-version }}
22
+ uses: actions/setup-node@v4
23
+ with:
24
+ node-version: ${{ matrix.node-version }}
25
+ cache: npm
26
+
27
+ - name: Install dependencies
28
+ run: npm ci
29
+
30
+ - name: Lint
31
+ run: npm run lint --if-present
32
+
33
+ - name: Unit tests
34
+ run: npm test
35
+
36
+ - name: Security tests
37
+ run: npm run test:security --if-present
38
+
39
+ - name: Upload coverage
40
+ if: matrix.node-version == '20.x'
41
+ uses: actions/upload-artifact@v4
42
+ with:
43
+ name: coverage
44
+ path: coverage/
45
+ retention-days: 7
@@ -0,0 +1,48 @@
1
+ name: CI
2
+
3
+ on:
4
+ push:
5
+ branches: [main, master, develop]
6
+ pull_request:
7
+ branches: [main, master, develop]
8
+
9
+ jobs:
10
+ test:
11
+ name: Test & Lint
12
+ runs-on: ubuntu-latest
13
+
14
+ strategy:
15
+ matrix:
16
+ python-version: ["3.10", "3.11", "3.12"]
17
+
18
+ steps:
19
+ - uses: actions/checkout@v4
20
+
21
+ - name: Set up Python ${{ matrix.python-version }}
22
+ uses: actions/setup-python@v5
23
+ with:
24
+ python-version: ${{ matrix.python-version }}
25
+ cache: pip
26
+
27
+ - name: Install dependencies
28
+ run: |
29
+ python -m pip install --upgrade pip
30
+ pip install -r requirements.txt
31
+ pip install pytest pytest-cov ruff
32
+
33
+ - name: Lint (ruff)
34
+ run: ruff check .
35
+
36
+ - name: Unit tests with coverage
37
+ run: pytest --cov=. --cov-report=xml -v
38
+
39
+ - name: Security tests
40
+ run: pytest tests/security/ -v
41
+
42
+ - name: Upload coverage
43
+ if: matrix.python-version == '3.11'
44
+ uses: actions/upload-artifact@v4
45
+ with:
46
+ name: coverage
47
+ path: coverage.xml
48
+ retention-days: 7
@@ -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.