@lhi/tdd-audit 1.5.0 → 1.8.2

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.
@@ -0,0 +1,200 @@
1
+ # Vulnerability Patterns Reference
2
+
3
+ All 34 patterns detected by `@lhi/tdd-audit`. Patterns are checked against every scannable source file line-by-line. Prompt/skill patterns are checked separately against `.md` files in agent configuration directories.
4
+
5
+ ---
6
+
7
+ ## CRITICAL
8
+
9
+ ### SQL Injection
10
+ **Grep signature:** template literal SELECT, string-concatenated query, Python f-string/%-format SQL, tagged template DB call
11
+ **Why it matters:** Attacker can read, modify, or delete any data in your database by manipulating the query string.
12
+ **Fix:** Parameterized queries / ORM methods. See [`green-phase.md`](../prompts/green-phase.md#sql-injection).
13
+
14
+ ### Command Injection
15
+ **Grep signature:** `exec(` / `execSync(` with `req.params|body|query`; `subprocess.run(shell=True)`
16
+ **Why it matters:** Attacker can run arbitrary shell commands on your server.
17
+ **Fix:** Use `execFile`/`spawn` with an argument array (no shell interpolation).
18
+
19
+ ### TLS Bypass
20
+ **Grep signature:** `badCertificateCallback = true`, `rejectUnauthorized: false`, `NODE_TLS_REJECT_UNAUTHORIZED=0`
21
+ **Why it matters:** All HTTPS connections become vulnerable to man-in-the-middle attacks.
22
+ **Fix:** Remove the override. For internal CAs, set `NODE_EXTRA_CA_CERTS` or pass the cert to `SecurityContext`.
23
+
24
+ ### Hardcoded Secret
25
+ **Grep signature:** `const API_KEY = "..."`, `let SECRET_KEY = "..."` (≥20 chars)
26
+ **Note:** `skipInTests: true` — matches in test files are marked `likelyFalsePositive`.
27
+ **Why it matters:** Secret is committed to git history and visible to anyone with repo access.
28
+ **Fix:** Move to environment variables. Run `gitleaks` to check if already committed.
29
+
30
+ ### SSRF (Server-Side Request Forgery)
31
+ **Grep signature:** `fetch(req.query.url)`, `axios.get(req.body.url)`, `got(req.params.url)`
32
+ **Why it matters:** Attacker can probe internal services (AWS metadata, Redis, internal APIs) via your server.
33
+ **Fix:** Validate URL against an explicit hostname allowlist. Block private IP ranges.
34
+
35
+ ### Insecure Deserialization
36
+ **Grep signature:** `.unserialize(req.)`, `__proto__ =`, `Object.setPrototypeOf(x, req.`
37
+ **Why it matters:** Attacker can achieve RCE or privilege escalation by crafting a malicious serialized payload.
38
+ **Fix:** Never deserialize user-supplied data. Use JSON with a schema validator instead.
39
+
40
+ ### JWT Alg None
41
+ **Grep signature:** `algorithm: 'none'`
42
+ **Why it matters:** The `alg:none` attack strips the JWT signature entirely, allowing anyone to forge tokens.
43
+ **Fix:** Use `jsonwebtoken` with an explicit `algorithms` allowlist — never include `'none'`.
44
+
45
+ ---
46
+
47
+ ## HIGH
48
+
49
+ ### IDOR (Insecure Direct Object Reference)
50
+ **Grep signature:** `findById(req.params|body|query.`, `findOne({id: req.params|body|query`
51
+ **Why it matters:** Any logged-in user can access another user's private data by guessing or iterating IDs.
52
+ **Fix:** Scope all DB queries to `req.user.id`. Never trust a client-supplied resource ID.
53
+
54
+ ### XSS (Cross-Site Scripting)
55
+ **Grep signature:** `innerHTML =`, `dangerouslySetInnerHTML={{`, `document.write(`, `res.send(\`...\${req.`
56
+ **Why it matters:** Attacker can inject scripts that run in other users' browsers, stealing sessions or redirecting them.
57
+ **Fix:** Escape on output (`escape-html`), sanitize rich HTML (`DOMPurify`), or use a framework that auto-escapes.
58
+
59
+ ### Path Traversal
60
+ **Grep signature:** `readFile/sendFile/createReadStream(req.`, `path.join(req.params|body|query`
61
+ **Why it matters:** Attacker can read files outside the uploads directory (`.env`, `/etc/passwd`).
62
+ **Fix:** `path.resolve()` the final path and assert it starts with the allowed base directory.
63
+
64
+ ### Broken Auth
65
+ **Grep signature:** `jwt.decode(` (without `.verify`), `verify: false`, `secret = "short_string"`
66
+ **Why it matters:** Anyone can forge a valid-looking token and impersonate any user.
67
+ **Fix:** Always use `jwt.verify()` with an explicit secret from environment variables.
68
+
69
+ ### Sensitive Storage
70
+ **Grep signature:** `localStorage.setItem('token'`, `AsyncStorage.setItem('token'`
71
+ **Why it matters:** Tokens stored in unencrypted storage are readable on rooted/jailbroken devices and via XSS.
72
+ **Fix:** Use `expo-secure-store` (React Native/Expo) or `flutter_secure_storage` (Flutter).
73
+
74
+ ### eval() Injection
75
+ **Grep signature:** `eval(route.params`, `eval(searchParams.get`, `eval(req.query|body`
76
+ **Why it matters:** Attacker can execute arbitrary JavaScript in the application context.
77
+ **Fix:** Never use `eval()` with user input. Use `JSON.parse()` for data deserialization.
78
+
79
+ ### Insecure Random
80
+ **Grep signature:** `token = Math.random()`, `sessionId = Math.random()`
81
+ **Why it matters:** `Math.random()` is not cryptographically secure — tokens can be predicted.
82
+ **Fix:** Use `crypto.randomBytes()` (Node.js) or `secrets.token_hex()` (Python).
83
+
84
+ ### Secret Fallback
85
+ **Grep signature:** `process.env.SECRET || "hardcoded_value"`
86
+ **Why it matters:** The hardcoded fallback is committed to source control and used whenever the env var is missing.
87
+ **Fix:** Fail fast if the env var is absent — never fall back to a default secret.
88
+
89
+ ### Open Redirect
90
+ **Grep signature:** `res.redirect(req.query|body|params.`, `window.location = params.`
91
+ **Why it matters:** Attacker can redirect users to phishing sites after a legitimate login flow.
92
+ **Fix:** Allow only relative paths. Reject `http://` / `https://` and `//` prefix destinations.
93
+
94
+ ### NoSQL Injection
95
+ **Grep signature:** `.find(req.body|query)`, `.findOne(req.body|query)`, `$where:`
96
+ **Why it matters:** Attacker can bypass authentication by injecting MongoDB operators (`{ $gt: '' }`).
97
+ **Fix:** Cast query values to strings. Use `express-mongo-sanitize` to strip `$` operators.
98
+
99
+ ### Template Injection
100
+ **Grep signature:** `res.render(req.params|query`, `ejs.render(req.body`, `pug.render(req.body`
101
+ **Why it matters:** Attacker can execute server-side template code, potentially achieving RCE.
102
+ **Fix:** Never pass user input as the template name or raw template string.
103
+
104
+ ### Mass Assignment
105
+ **Grep signature:** `new Model(req.body)`, `.create(req.body)`, `.update({}, req.body)`
106
+ **Why it matters:** Attacker can set privileged fields (`isAdmin`, `role`) by adding them to a POST body.
107
+ **Fix:** Destructure and allowlist only the fields users are permitted to set.
108
+
109
+ ### Prototype Pollution
110
+ **Grep signature:** `_.merge(req.body|query)`, `deepmerge(req.body|query)`, `Object.assign({}, req.body)`
111
+ **Why it matters:** Attacker can inject properties into `Object.prototype`, affecting all objects in the process.
112
+ **Fix:** Sanitize `__proto__` / `constructor` / `prototype` keys before any recursive merge.
113
+
114
+ ### Weak Crypto
115
+ **Grep signature:** `createHash('md5')`, `createHash('sha1')`, `md5(password)`, `sha1(password)`
116
+ **Why it matters:** MD5 and SHA1 hashes are trivially crackable with rainbow tables.
117
+ **Fix:** Use `bcrypt` (cost factor ≥12) or `argon2` for passwords.
118
+
119
+ ### XXE (XML External Entity)
120
+ **Grep signature:** `noent: true`, `expand_entities = True`, `resolve_entities = True`
121
+ **Why it matters:** Attacker can read local files or perform SSRF via XML entity expansion.
122
+ **Fix:** Disable entity expansion in your XML parser. Never enable it for user-supplied XML.
123
+
124
+ ### WebView JS Bridge
125
+ **Grep signature:** `addJavascriptInterface(`, `javaScriptEnabled: true`, `allowFileAccess: true`, `allowUniversalAccessFromFileURLs: true`
126
+ **Why it matters:** Exposed JavaScript bridge or relaxed WebView settings allow XSS-to-native escalation.
127
+ **Fix:** Disable unnecessary WebView capabilities. Never expose a JS bridge to untrusted content.
128
+
129
+ ### Timing-Unsafe Comparison
130
+ **Grep signature:** `token === `, `password ===`, `secret ==` (equality comparison of secrets)
131
+ **Why it matters:** Timing side-channel allows attackers to brute-force tokens bit by bit.
132
+ **Fix:** Use `crypto.timingSafeEqual()` (Node.js) or `hmac.compare_digest()` (Python) for all secret comparisons.
133
+
134
+ ### ReDoS
135
+ **Grep signature:** `new RegExp(req.query|body|params.`
136
+ **Why it matters:** Attacker can craft input that causes catastrophic regex backtracking, DoSing the process.
137
+ **Fix:** Never construct regex from user input. If required, use a regex complexity validator.
138
+
139
+ ---
140
+
141
+ ## MEDIUM
142
+
143
+ ### Sensitive Log
144
+ **Grep signature:** `console.log(token|password|secret|jwt|authorization|apiKey`
145
+ **Note:** `skipInTests: true`
146
+ **Why it matters:** Secrets end up in log aggregation systems, monitoring dashboards, and CI output.
147
+ **Fix:** Remove or redact sensitive fields before logging.
148
+
149
+ ### CORS Wildcard
150
+ **Grep signature:** `cors({ origin: '*' })`, `Access-Control-Allow-Origin: *`
151
+ **Why it matters:** Any origin can make credentialed requests to your API.
152
+ **Fix:** Specify an explicit origin allowlist in your CORS configuration.
153
+
154
+ ### Cleartext Traffic
155
+ **Grep signature:** `baseURL = 'http://...'` (non-localhost)
156
+ **Note:** `skipInTests: true`
157
+ **Why it matters:** API traffic is sent unencrypted and visible to network observers.
158
+ **Fix:** Use `https://` for all non-localhost API base URLs.
159
+
160
+ ### Deep Link Injection
161
+ **Grep signature:** `Linking.getInitialURL()`, `Linking.addEventListener('url'`
162
+ **Why it matters:** Attacker can inject malicious data via crafted deep links if parameters are not validated.
163
+ **Fix:** Validate and sanitize all values extracted from deep link URLs before use.
164
+
165
+ ---
166
+
167
+ ## Prompt / Skill / Agent Patterns
168
+
169
+ These patterns are checked against `.md` files in `prompts/`, `skills/`, `.claude/`, `workflows/`, `CLAUDE.md`, `SKILL.md`, `.cursorrules`, and `.clinerules`.
170
+
171
+ ### Deprecated CSRF Package (CRITICAL)
172
+ **Grep signature:** `\bcsurf\b` (not in a comment line)
173
+ **Why it matters:** `csurf` was deprecated in March 2023 and is unmaintained. Projects that follow instructions referencing it will install a package with unpatched vulnerabilities.
174
+ **Fix:** Replace with `csrf-csrf` (`doubleCsrf` pattern).
175
+
176
+ ### Unpinned npx MCP Server (HIGH)
177
+ **Grep signature:** `"command": "npx"` in MCP server config
178
+ **Why it matters:** `npx` resolves the latest version at runtime. A compromised package version executes arbitrary code in the agent's context.
179
+ **Fix:** Pin MCP servers to exact versions or install locally. Use `node /path/to/server.js` instead of `npx`.
180
+
181
+ ### Cleartext URL in Prompt (MEDIUM)
182
+ **Grep signature:** `http://` (non-localhost) in prompt/skill markdown
183
+ **Why it matters:** Cleartext URLs in agent instructions can mislead the agent into making insecure HTTP requests.
184
+ **Fix:** Replace with `https://` URLs.
185
+
186
+ ---
187
+
188
+ ## Config / Manifest Patterns
189
+
190
+ ### Config Secret (CRITICAL)
191
+ **Files checked:** `app.json`, `app.config.js`, `app.config.ts`
192
+ **Grep signature:** `apiKey: "..."`, `secret: "..."`, `accessToken: "..."` (≥20 chars)
193
+ **Why it matters:** Expo/React Native config files are bundled into the app binary and shipped to users.
194
+ **Fix:** Use `expo-constants` with environment variables at build time. Never embed secrets in config files.
195
+
196
+ ### Android Debuggable (HIGH)
197
+ **Files checked:** `android/app/src/main/AndroidManifest.xml`
198
+ **Grep signature:** `android:debuggable="true"`
199
+ **Why it matters:** Debug builds expose the app to `adb` inspection and arbitrary code injection on the device.
200
+ **Fix:** Remove `android:debuggable` from `AndroidManifest.xml` (the build system sets it correctly per variant).
package/lib/scanner.js CHANGED
@@ -45,6 +45,9 @@ const VULN_PATTERNS = [
45
45
  ];
46
46
 
47
47
  const SCAN_EXTENSIONS = new Set(['.js', '.ts', '.jsx', '.tsx', '.mjs', '.py', '.go', '.dart']);
48
+
49
+ /** Maximum file size to read before skipping (512 KB). Prevents OOM on large generated files. */
50
+ const MAX_SCAN_FILE_BYTES = 512 * 1024;
48
51
  const SKIP_DIRS = new Set(['node_modules', '.git', 'dist', 'build', '.next', 'out', '__pycache__', 'venv', '.venv', 'vendor', '.expo', '.dart_tool', '.pub-cache']);
49
52
 
50
53
  // ─── Prompt / Skill Patterns ──────────────────────────────────────────────────
@@ -214,14 +217,22 @@ function hasSafeAuditStatus(lines) {
214
217
  }
215
218
 
216
219
  /**
217
- * Returns true if the match at matchIndex falls inside a backtick code span.
218
- * Used to suppress PROMPT_PATTERN hits on pattern-documentation table rows.
220
+ * Returns true if the match at matchIndex falls inside a *closed* backtick
221
+ * code span on the same line. A code span is closed only when there is an
222
+ * odd number of backticks before the match AND at least one closing backtick
223
+ * after it on the same line. A lone, unmatched backtick before the pattern
224
+ * does NOT constitute a code span and must NOT suppress the finding.
219
225
  * @param {string} line
220
226
  * @param {number} matchIndex - character index of the match start
221
227
  */
222
228
  function isInsideBackticks(line, matchIndex) {
223
229
  const before = line.slice(0, matchIndex);
224
- return (before.match(/`/g) || []).length % 2 === 1;
230
+ const after = line.slice(matchIndex);
231
+ const backticksBefore = (before.match(/`/g) || []).length;
232
+ const backticksAfter = (after.match(/`/g) || []).length;
233
+ // Suppress only when the span is properly closed: odd opening count + at
234
+ // least one closing backtick exists after the match position.
235
+ return backticksBefore % 2 === 1 && backticksAfter >= 1;
225
236
  }
226
237
 
227
238
  /**
@@ -234,16 +245,32 @@ function isCommentLine(line) {
234
245
 
235
246
  /**
236
247
  * Scan all prompt/skill .md files in projectDir for prompt-specific patterns.
248
+ *
249
+ * Returns a findings array with a non-enumerable `.exempted` property — an
250
+ * array of relative paths for files skipped via `audit_status: safe`. Using
251
+ * a non-enumerable property preserves full backward compatibility: spread,
252
+ * toEqual([]), and quickScan's `...scanPromptFiles()` all continue to work.
253
+ *
237
254
  * @param {string} projectDir - project root
238
- * @returns {Array} findings
255
+ * @returns {Array} findings (with non-enumerable .exempted: string[])
239
256
  */
240
257
  function scanPromptFiles(projectDir) {
241
258
  const findings = [];
259
+ const exempted = [];
242
260
  for (const filePath of walkMdFiles(projectDir)) {
243
261
  if (!isPromptFile(filePath, projectDir)) continue;
244
262
  let lines;
245
- try { lines = fs.readFileSync(filePath, 'utf8').split('\n'); } catch { continue; }
246
- if (hasSafeAuditStatus(lines)) continue;
263
+ try {
264
+ // SEC-06: read first, then check length — eliminates statSync/readFileSync TOCTOU race.
265
+ const content = fs.readFileSync(filePath, 'utf8');
266
+ if (content.length > MAX_SCAN_FILE_BYTES) continue;
267
+ if (content.includes('\0')) continue; // skip binary files (mirrors quickScan guard)
268
+ lines = content.split('\n');
269
+ } catch { continue; }
270
+ if (hasSafeAuditStatus(lines)) {
271
+ exempted.push(path.relative(projectDir, filePath));
272
+ continue;
273
+ }
247
274
  for (let i = 0; i < lines.length; i++) {
248
275
  for (const p of PROMPT_PATTERNS) {
249
276
  const match = p.pattern.exec(lines[i]);
@@ -262,6 +289,8 @@ function scanPromptFiles(projectDir) {
262
289
  }
263
290
  }
264
291
  }
292
+ // Attach exempted as non-enumerable so spread / toEqual([]) are unaffected.
293
+ Object.defineProperty(findings, 'exempted', { value: exempted, enumerable: false, configurable: true });
265
294
  return findings;
266
295
  }
267
296
 
@@ -339,8 +368,10 @@ function quickScan(projectDir) {
339
368
  const inTest = isTestFile(filePath, projectDir);
340
369
  let content;
341
370
  // L1 fix: guard against binary / non-UTF-8 files
371
+ // SEC-06: read first, then check length — eliminates statSync/readFileSync TOCTOU race.
342
372
  try {
343
373
  content = fs.readFileSync(filePath, 'utf8');
374
+ if (content.length > MAX_SCAN_FILE_BYTES) continue;
344
375
  } catch {
345
376
  continue;
346
377
  }
@@ -372,38 +403,47 @@ function quickScan(projectDir) {
372
403
 
373
404
  /**
374
405
  * Print a human-readable findings report to stdout.
375
- * @param {Array} findings
406
+ * @param {Array} findings - array of finding objects
407
+ * @param {string[]} [exempted=[]] - relative paths of files skipped via audit_status:safe
376
408
  */
377
- function printFindings(findings) {
409
+ function printFindings(findings, exempted = []) {
378
410
  if (findings.length === 0) {
379
411
  console.log(' ✅ No obvious vulnerability patterns detected.\n');
380
- return;
381
- }
382
- const real = findings.filter(f => !f.likelyFalsePositive);
383
- const noisy = findings.filter(f => f.likelyFalsePositive);
384
-
385
- const bySeverity = { CRITICAL: [], HIGH: [], MEDIUM: [], LOW: [] };
386
- for (const f of real) (bySeverity[f.severity] || bySeverity.LOW).push(f);
387
- const icons = { CRITICAL: '🔴', HIGH: '🟠', MEDIUM: '🟡', LOW: '🔵' };
388
-
389
- console.log(`\n Found ${real.length} potential issue(s)${noisy.length ? ` (+${noisy.length} in test files — see below)` : ''}:\n`);
390
- for (const [sev, list] of Object.entries(bySeverity)) {
391
- if (!list.length) continue;
392
- for (const f of list) {
393
- const testBadge = f.inTestFile ? ' [test file]' : '';
394
- console.log(` ${icons[sev]} [${sev}] ${f.name} — ${f.file}:${f.line}${testBadge}`);
395
- console.log(` ${f.snippet}`);
412
+ } else {
413
+ const real = findings.filter(f => !f.likelyFalsePositive);
414
+ const noisy = findings.filter(f => f.likelyFalsePositive);
415
+
416
+ const bySeverity = { CRITICAL: [], HIGH: [], MEDIUM: [], LOW: [] };
417
+ for (const f of real) (bySeverity[f.severity] || bySeverity.LOW).push(f);
418
+ const icons = { CRITICAL: '🔴', HIGH: '🟠', MEDIUM: '🟡', LOW: '🔵' };
419
+
420
+ console.log(`\n Found ${real.length} potential issue(s)${noisy.length ? ` (+${noisy.length} in test files — see below)` : ''}:\n`);
421
+ for (const [sev, list] of Object.entries(bySeverity)) {
422
+ if (!list.length) continue;
423
+ for (const f of list) {
424
+ const testBadge = f.inTestFile ? ' [test file]' : '';
425
+ console.log(` ${icons[sev]} [${sev}] ${f.name} ${f.file}:${f.line}${testBadge}`);
426
+ console.log(` ${f.snippet}`);
427
+ }
396
428
  }
397
- }
398
429
 
399
- if (noisy.length) {
400
- console.log('\n ⚪ Likely intentional (in test files — verify manually):');
401
- for (const f of noisy) {
402
- console.log(` ${f.name} — ${f.file}:${f.line}`);
430
+ if (noisy.length) {
431
+ console.log('\n ⚪ Likely intentional (in test files — verify manually):');
432
+ for (const f of noisy) {
433
+ console.log(` ${f.name} — ${f.file}:${f.line}`);
434
+ }
403
435
  }
436
+
437
+ console.log('\n Run /tdd-audit in your agent to remediate.\n');
404
438
  }
405
439
 
406
- console.log('\n Run /tdd-audit in your agent to remediate.\n');
440
+ if (exempted.length) {
441
+ console.log(' ⚠️ Files skipped via audit_status:safe (verify these exemptions are intentional):');
442
+ for (const p of exempted) {
443
+ console.log(` ${p}`);
444
+ }
445
+ console.log('');
446
+ }
407
447
  }
408
448
 
409
449
  module.exports = {
@@ -411,6 +451,7 @@ module.exports = {
411
451
  PROMPT_PATTERNS,
412
452
  SCAN_EXTENSIONS,
413
453
  SKIP_DIRS,
454
+ MAX_SCAN_FILE_BYTES,
414
455
  detectFramework,
415
456
  detectAppFramework,
416
457
  detectTestBaseDir,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lhi/tdd-audit",
3
- "version": "1.5.0",
3
+ "version": "1.8.2",
4
4
  "description": "Security skill installer for Claude Code, Gemini CLI, Cursor, Codex, and OpenCode. Patches vulnerabilities using a Red-Green-Refactor exploit-test protocol.",
5
5
  "main": "index.js",
6
6
  "bin": {
@@ -14,7 +14,8 @@
14
14
  "templates/",
15
15
  "workflows/",
16
16
  "README.md",
17
- "LICENSE"
17
+ "LICENSE",
18
+ "docs/"
18
19
  ],
19
20
  "scripts": {
20
21
  "test": "jest --forceExit",
@@ -1,5 +1,11 @@
1
1
  ---
2
2
  description: Run the complete TDD Remediation Autonomous Audit
3
+ risk: low
4
+ source: personal
5
+ date_added: "2024-01-01"
6
+ audited_by: lcanady
7
+ last_audited: "2026-03-25"
8
+ audit_status: safe
3
9
  ---
4
10
  Please use the TDD Remediation Protocol Auto-Audit skill (located in the `skills/tdd-remediation` folder) to secure this repository.
5
11