@lhi/tdd-audit 1.1.1 → 1.2.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/README.md CHANGED
@@ -34,6 +34,7 @@ node index.js
34
34
  | `--claude` | Use `.claude/` instead of `.agents/` as the skill directory |
35
35
  | `--with-hooks` | Install a pre-commit hook that blocks commits if security tests fail |
36
36
  | `--skip-scan` | Skip the automatic vulnerability scan on install |
37
+ | `--scan-only` | Run the vulnerability scan without installing anything |
37
38
 
38
39
  **Install to a Claude Code project with pre-commit protection:**
39
40
  ```bash
package/SKILL.md CHANGED
@@ -9,7 +9,12 @@ Applying Test-Driven Development (TDD) to code that has already been generated r
9
9
 
10
10
  ## Autonomous Audit Mode
11
11
  If the user asks you to "Run the TDD Remediation Auto-Audit" or asks you to implement this on your own:
12
- 1. **Explore**: Proactively use `Glob`, `Grep`, and `Read` to scan the repository. Focus on `controllers/`, `routes/`, `api/`, `middleware/`, and database files. Search for anti-patterns: unparameterized SQL queries, missing ownership checks, unsafe HTML rendering, and command injection sinks. Full search patterns are in [auto-audit.md](./prompts/auto-audit.md).
12
+ 1. **Explore**: Proactively use `Glob`, `Grep`, and `Read` to scan the repository. Focus on:
13
+ - **Backend/API**: `controllers/`, `routes/`, `api/`, `handlers/`, `middleware/`, `services/`, `models/`
14
+ - **React / Next.js**: `pages/api/`, `app/api/`, `components/`, `hooks/`, `context/`, `store/`
15
+ - **React Native / Expo**: `screens/`, `navigation/`, `app/`, `app.json`, `app.config.js`
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).
13
18
  2. **Plan**: Present a structured list of vulnerabilities (grouped by severity: CRITICAL / HIGH / MEDIUM / LOW) and get confirmation before making any changes.
14
19
  3. **Self-Implement**: For *each* confirmed vulnerability, autonomously execute the complete 3-phase protocol:
15
20
  - **[Phase 1 (Red)](./prompts/red-phase.md)**: Write the exploit test ensuring it fails.
package/index.js CHANGED
@@ -9,17 +9,23 @@ const isLocal = args.includes('--local');
9
9
  const isClaude = args.includes('--claude');
10
10
  const withHooks = args.includes('--with-hooks');
11
11
  const skipScan = args.includes('--skip-scan');
12
+ const scanOnly = args.includes('--scan-only');
12
13
 
13
14
  const agentBaseDir = isLocal ? process.cwd() : os.homedir();
14
15
  const agentDirName = isClaude ? '.claude' : '.agents';
15
16
  const projectDir = process.cwd();
16
17
 
17
18
  const targetSkillDir = path.join(agentBaseDir, agentDirName, 'skills', 'tdd-remediation');
18
- const targetWorkflowDir = path.join(agentBaseDir, agentDirName, 'workflows');
19
+ const targetWorkflowDir = isClaude
20
+ ? path.join(agentBaseDir, agentDirName, 'commands')
21
+ : path.join(agentBaseDir, agentDirName, 'workflows');
19
22
 
20
23
  // ─── 1. Framework Detection ──────────────────────────────────────────────────
21
24
 
22
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
+
23
29
  const pkgPath = path.join(projectDir, 'package.json');
24
30
  if (fs.existsSync(pkgPath)) {
25
31
  try {
@@ -40,6 +46,25 @@ function detectFramework() {
40
46
  return 'jest';
41
47
  }
42
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
+
43
68
  const framework = detectFramework();
44
69
 
45
70
  // ─── 2. Test Directory Detection ─────────────────────────────────────────────
@@ -68,10 +93,19 @@ const VULN_PATTERNS = [
68
93
  { name: 'XSS', severity: 'HIGH', pattern: /[^/]innerHTML\s*=(?!=)|dangerouslySetInnerHTML\s*=\s*\{\{|document\.write\s*\(|res\.send\s*\(`[^`]*\$\{req\./i },
69
94
  { name: 'Path Traversal', severity: 'HIGH', pattern: /(readFile|sendFile|createReadStream|open)\s*\(.*req\.(params|body|query)|path\.join\s*\([^)]*req\.(params|body|query)/i },
70
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 },
71
105
  ];
72
106
 
73
- const SCAN_EXTENSIONS = new Set(['.js', '.ts', '.jsx', '.tsx', '.mjs', '.py', '.go']);
74
- const SKIP_DIRS = new Set(['node_modules', '.git', 'dist', 'build', '.next', 'out', '__pycache__', 'venv', '.venv', 'vendor']);
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']);
75
109
 
76
110
  function* walkFiles(dir) {
77
111
  let entries;
@@ -84,9 +118,43 @@ function* walkFiles(dir) {
84
118
  }
85
119
  }
86
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
+
87
154
  function quickScan() {
88
155
  const findings = [];
89
156
  for (const filePath of walkFiles(projectDir)) {
157
+ const inTest = isTestFile(filePath);
90
158
  let lines;
91
159
  try { lines = fs.readFileSync(filePath, 'utf8').split('\n'); } catch { continue; }
92
160
  for (let i = 0; i < lines.length; i++) {
@@ -98,13 +166,15 @@ function quickScan() {
98
166
  file: path.relative(projectDir, filePath),
99
167
  line: i + 1,
100
168
  snippet: lines[i].trim().slice(0, 80),
169
+ inTestFile: inTest,
170
+ likelyFalsePositive: inTest && !!vuln.skipInTests,
101
171
  });
102
172
  break; // one finding per line
103
173
  }
104
174
  }
105
175
  }
106
176
  }
107
- return findings;
177
+ return [...findings, ...scanAppConfig()];
108
178
  }
109
179
 
110
180
  function printFindings(findings) {
@@ -112,24 +182,47 @@ function printFindings(findings) {
112
182
  console.log(' ✅ No obvious vulnerability patterns detected.\n');
113
183
  return;
114
184
  }
185
+ const real = findings.filter(f => !f.likelyFalsePositive);
186
+ const noisy = findings.filter(f => f.likelyFalsePositive);
187
+
115
188
  const bySeverity = { CRITICAL: [], HIGH: [], MEDIUM: [], LOW: [] };
116
- for (const f of findings) (bySeverity[f.severity] || bySeverity.LOW).push(f);
189
+ for (const f of real) (bySeverity[f.severity] || bySeverity.LOW).push(f);
117
190
  const icons = { CRITICAL: '🔴', HIGH: '🟠', MEDIUM: '🟡', LOW: '🔵' };
118
191
 
119
- console.log(`\n Found ${findings.length} potential issue(s):\n`);
192
+ console.log(`\n Found ${real.length} potential issue(s)${noisy.length ? ` (+${noisy.length} in test files — see below)` : ''}:\n`);
120
193
  for (const [sev, list] of Object.entries(bySeverity)) {
121
194
  if (!list.length) continue;
122
195
  for (const f of list) {
123
- console.log(` ${icons[sev]} [${sev}] ${f.name} ${f.file}:${f.line}`);
196
+ const testBadge = f.inTestFile ? ' [test file]' : '';
197
+ console.log(` ${icons[sev]} [${sev}] ${f.name} — ${f.file}:${f.line}${testBadge}`);
124
198
  console.log(` ${f.snippet}`);
125
199
  }
126
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
+
127
209
  console.log('\n Run /tdd-audit in your agent to remediate.\n');
128
210
  }
129
211
 
130
- // ─── 4. Install Skill Files ───────────────────────────────────────────────────
212
+ // ─── 4. Scan-only early exit ──────────────────────────────────────────────────
213
+
214
+ if (scanOnly) {
215
+ process.stdout.write('\n🔍 Scanning for vulnerability patterns...');
216
+ const findings = quickScan();
217
+ process.stdout.write('\n');
218
+ printFindings(findings);
219
+ process.exit(0);
220
+ }
221
+
222
+ // ─── 5. Install Skill Files ───────────────────────────────────────────────────
131
223
 
132
- console.log(`\nInstalling TDD Remediation Skill (${isLocal ? 'local' : 'global'}, framework: ${framework}, test dir: ${testBaseDir}/)...\n`);
224
+ const appLabel = appFramework ? `, app: ${appFramework}` : '';
225
+ console.log(`\nInstalling TDD Remediation Skill (${isLocal ? 'local' : 'global'}, framework: ${framework}${appLabel}, test dir: ${testBaseDir}/)...\n`);
133
226
 
134
227
  if (!fs.existsSync(targetSkillDir)) fs.mkdirSync(targetSkillDir, { recursive: true });
135
228
 
@@ -147,11 +240,12 @@ if (!fs.existsSync(targetTestDir)) {
147
240
  }
148
241
 
149
242
  const testTemplateMap = {
150
- jest: 'sample.exploit.test.js',
151
- vitest: 'sample.exploit.test.vitest.js',
152
- mocha: 'sample.exploit.test.js',
153
- pytest: 'sample.exploit.test.pytest.py',
154
- go: 'sample.exploit.test.go',
243
+ jest: 'sample.exploit.test.js',
244
+ vitest: 'sample.exploit.test.vitest.js',
245
+ mocha: 'sample.exploit.test.js',
246
+ pytest: 'sample.exploit.test.pytest.py',
247
+ go: 'sample.exploit.test.go',
248
+ flutter: 'sample.exploit.test.dart',
155
249
  };
156
250
 
157
251
  const testTemplateName = testTemplateMap[framework];
@@ -204,11 +298,12 @@ const ciWorkflowPath = path.join(ciWorkflowDir, 'security-tests.yml');
204
298
 
205
299
  if (!fs.existsSync(ciWorkflowPath)) {
206
300
  const ciTemplateMap = {
207
- jest: 'security-tests.node.yml',
208
- vitest: 'security-tests.node.yml',
209
- mocha: 'security-tests.node.yml',
210
- pytest: 'security-tests.python.yml',
211
- go: 'security-tests.go.yml',
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',
212
307
  };
213
308
  const ciTemplatePath = path.join(__dirname, 'templates', 'workflows', ciTemplateMap[framework]);
214
309
  if (fs.existsSync(ciTemplatePath)) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lhi/tdd-audit",
3
- "version": "1.1.1",
3
+ "version": "1.2.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": {
@@ -31,7 +31,14 @@
31
31
  "audit",
32
32
  "claude",
33
33
  "ai-agent",
34
- "skill"
34
+ "skill",
35
+ "react",
36
+ "nextjs",
37
+ "react-native",
38
+ "expo",
39
+ "flutter",
40
+ "mobile-security",
41
+ "vibecoding"
35
42
  ],
36
43
  "engines": {
37
44
  "node": ">=16.7.0"
@@ -6,11 +6,29 @@ When invoked in Auto-Audit mode, proactively secure the user's entire repository
6
6
 
7
7
  ### 0a. Explore the Architecture
8
8
  Use `Glob` and `Read` to understand the project structure. Focus on:
9
+
10
+ **Backend / API**
9
11
  - `controllers/`, `routes/`, `api/`, `handlers/` — request entry points
10
12
  - `services/`, `models/`, `db/`, `repositories/` — data access
11
13
  - `middleware/`, `utils/`, `helpers/`, `lib/` — shared utilities
12
14
  - Config files: `*.env`, `config.js`, `settings.py` — secrets and security settings
13
15
 
16
+ **React / Next.js**
17
+ - `pages/api/`, `app/api/` — Next.js API routes (check for missing auth)
18
+ - `components/`, `app/`, `pages/` — UI components (check for `dangerouslySetInnerHTML`, `eval`)
19
+ - `hooks/`, `context/`, `store/` — state management (check for sensitive data leakage)
20
+
21
+ **React Native / Expo**
22
+ - `screens/`, `navigation/`, `app/` — screen components (check `route.params` usage)
23
+ - `services/`, `api/`, `utils/` — API calls (check TLS config, token storage)
24
+ - `app.json`, `app.config.js` — Expo config (check for embedded keys)
25
+
26
+ **Flutter / Dart**
27
+ - `lib/screens/`, `lib/pages/`, `lib/views/` — UI layer
28
+ - `lib/services/`, `lib/api/`, `lib/repositories/` — data layer (check HTTP client config)
29
+ - `lib/utils/`, `lib/helpers/` — shared utilities
30
+ - `pubspec.yaml` — dependency audit
31
+
14
32
  ### 0b. Search for Anti-Patterns
15
33
  Use `Grep` with the following patterns to surface candidates. Read the matched files to confirm before reporting.
16
34
 
@@ -71,6 +89,51 @@ router\.(post|put|delete) # mutation routes (check for rate-limit middleware)
71
89
  app\.post\( # POST handlers (check for rate-limit middleware)
72
90
  ```
73
91
 
92
+ **Sensitive Storage (React / React Native / Expo)**
93
+ ```
94
+ AsyncStorage\.setItem.*token # token stored in unencrypted AsyncStorage
95
+ localStorage\.setItem.*token # token stored in localStorage (XSS-accessible)
96
+ AsyncStorage\.setItem.*password # password stored in plain AsyncStorage
97
+ SecureStore vs AsyncStorage # confirm sensitive values use expo-secure-store
98
+ ```
99
+
100
+ **TLS / Certificate Bypass**
101
+ ```
102
+ rejectUnauthorized.*false # Node.js TLS verification disabled
103
+ badCertificateCallback.*true # Dart/Flutter TLS bypass
104
+ NODE_TLS_REJECT_UNAUTHORIZED=0 # env-level TLS disable
105
+ ```
106
+
107
+ **Hardcoded Secrets (vibecoded apps)**
108
+ ```
109
+ API_KEY\s*=\s*['"][A-Za-z0-9]{20,} # hardcoded API key in source
110
+ PRIVATE_KEY\s*=\s*['"] # private key in source
111
+ SECRET_KEY\s*=\s*['"] # secret embedded in code
112
+ process\.env\.\w+\s*\|\|\s*['"] # env var with hardcoded fallback
113
+ ```
114
+
115
+ **Next.js API Route Auth**
116
+ ```
117
+ export.*async.*handler # Next.js API route — check for missing auth guard
118
+ export default.*req.*res # pages/api handler — verify authentication
119
+ getServerSideProps.*params # SSR with params — check for injection
120
+ ```
121
+
122
+ **React Native / Expo Navigation Injection**
123
+ ```
124
+ route\.params\.\w+.*query # route param passed to DB/API query
125
+ route\.params\.\w+.*fetch # route param used in fetch URL
126
+ navigation\.navigate.*params # user-controlled navigation params
127
+ ```
128
+
129
+ **Flutter / Dart**
130
+ ```
131
+ http\.get\( # raw http call — check for TLS config
132
+ http\.post\( # raw http call — check for TLS config
133
+ SharedPreferences.*setString.*token # token in unencrypted SharedPreferences
134
+ Platform\.environment\[ # env access in Flutter — check for secrets
135
+ ```
136
+
74
137
  ### 0c. Present Findings
75
138
  Before touching any code, output a structured **Audit Report** with this format:
76
139
 
@@ -208,3 +208,167 @@ function requireAuth(req, res, next) {
208
208
  }
209
209
  }
210
210
  ```
211
+
212
+ ---
213
+
214
+ ### React: XSS via dangerouslySetInnerHTML
215
+
216
+ **Root cause:** User-generated content is passed directly to `dangerouslySetInnerHTML` without sanitization.
217
+
218
+ **Fix:** Sanitize with DOMPurify before rendering. Never pass raw user input to `dangerouslySetInnerHTML`.
219
+
220
+ ```tsx
221
+ // BEFORE (vulnerable)
222
+ <div dangerouslySetInnerHTML={{ __html: userContent }} />
223
+
224
+ // AFTER
225
+ import DOMPurify from 'dompurify';
226
+
227
+ const clean = DOMPurify.sanitize(userContent, {
228
+ ALLOWED_TAGS: ['b', 'i', 'em', 'strong', 'a'],
229
+ ALLOWED_ATTR: ['href'],
230
+ });
231
+ <div dangerouslySetInnerHTML={{ __html: clean }} />
232
+ ```
233
+
234
+ **Install:** `npm install dompurify @types/dompurify`
235
+ For SSR (Next.js): `npm install isomorphic-dompurify` instead.
236
+
237
+ ---
238
+
239
+ ### Next.js: Missing Auth on API Routes
240
+
241
+ **Root cause:** API route handlers in `pages/api/` or `app/api/` are publicly accessible with no authentication check.
242
+
243
+ **Fix — Option A (per-route wrapper):**
244
+ ```typescript
245
+ // lib/withAuth.ts
246
+ import jwt from 'jsonwebtoken';
247
+ import type { NextApiHandler, NextApiRequest, NextApiResponse } from 'next';
248
+
249
+ export function withAuth(handler: NextApiHandler): NextApiHandler {
250
+ return async (req: NextApiRequest, res: NextApiResponse) => {
251
+ const token = req.headers.authorization?.split(' ')[1];
252
+ if (!token) return res.status(401).json({ error: 'Unauthorized' });
253
+ try {
254
+ (req as any).user = jwt.verify(token, process.env.JWT_SECRET!);
255
+ return handler(req, res);
256
+ } catch {
257
+ return res.status(401).json({ error: 'Invalid token' });
258
+ }
259
+ };
260
+ }
261
+
262
+ // pages/api/user.ts
263
+ import { withAuth } from '../../lib/withAuth';
264
+ export default withAuth((req, res) => res.json({ user: (req as any).user }));
265
+ ```
266
+
267
+ **Fix — Option B (global middleware, preferred for App Router):**
268
+ ```typescript
269
+ // middleware.ts (root of project — protects all /api routes)
270
+ import { NextResponse, type NextRequest } from 'next/server';
271
+ import { jwtVerify } from 'jose';
272
+
273
+ export async function middleware(request: NextRequest) {
274
+ const token = request.headers.get('authorization')?.split(' ')[1];
275
+ if (!token) return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
276
+ try {
277
+ await jwtVerify(token, new TextEncoder().encode(process.env.JWT_SECRET!));
278
+ return NextResponse.next();
279
+ } catch {
280
+ return NextResponse.json({ error: 'Invalid token' }, { status: 401 });
281
+ }
282
+ }
283
+
284
+ export const config = { matcher: '/api/:path*' };
285
+ ```
286
+
287
+ ---
288
+
289
+ ### React Native / Expo: Sensitive Storage Migration
290
+
291
+ **Root cause:** Auth tokens stored in `AsyncStorage` are unencrypted and readable on rooted/jailbroken devices.
292
+
293
+ **Fix:** Replace with `expo-secure-store`, which uses iOS Keychain and Android EncryptedSharedPreferences.
294
+
295
+ ```javascript
296
+ // BEFORE (vulnerable)
297
+ import AsyncStorage from '@react-native-async-storage/async-storage';
298
+ await AsyncStorage.setItem('token', userToken);
299
+ const token = await AsyncStorage.getItem('token');
300
+
301
+ // AFTER
302
+ import * as SecureStore from 'expo-secure-store';
303
+ await SecureStore.setItemAsync('token', userToken);
304
+ const token = await SecureStore.getItemAsync('token');
305
+ // On logout:
306
+ await SecureStore.deleteItemAsync('token');
307
+ ```
308
+
309
+ **Install:** `npx expo install expo-secure-store`
310
+ **Note:** `SecureStore` is device-bound and not available in Expo Go web preview — check `SecureStore.isAvailableAsync()` for web fallbacks.
311
+
312
+ ---
313
+
314
+ ### Flutter: Sensitive Storage Migration
315
+
316
+ **Root cause:** Auth tokens stored in `SharedPreferences` are plain text in app storage — readable on rooted/jailbroken devices.
317
+
318
+ **Fix:** Replace with `flutter_secure_storage`, which uses iOS Keychain and Android EncryptedSharedPreferences.
319
+
320
+ ```dart
321
+ // BEFORE (vulnerable)
322
+ final prefs = await SharedPreferences.getInstance();
323
+ await prefs.setString('token', userToken);
324
+ final token = prefs.getString('token');
325
+
326
+ // AFTER
327
+ import 'package:flutter_secure_storage/flutter_secure_storage.dart';
328
+
329
+ const _storage = FlutterSecureStorage();
330
+ await _storage.write(key: 'token', value: userToken);
331
+ final token = await _storage.read(key: 'token');
332
+ // On logout:
333
+ await _storage.delete(key: 'token');
334
+ ```
335
+
336
+ **pubspec.yaml:**
337
+ ```yaml
338
+ dependencies:
339
+ flutter_secure_storage: ^9.0.0
340
+ ```
341
+
342
+ ---
343
+
344
+ ### TLS Bypass Fix (Node.js + Flutter/Dart)
345
+
346
+ **Root cause:** TLS certificate verification is explicitly disabled, allowing man-in-the-middle attacks.
347
+
348
+ **Fix:** Remove the bypass entirely. For internal CAs, provide the cert — don't disable verification.
349
+
350
+ ```javascript
351
+ // BEFORE (vulnerable — Node.js)
352
+ const https = require('https');
353
+ const agent = new https.Agent({ rejectUnauthorized: false }); // ❌
354
+
355
+ // AFTER — remove the override; default is rejectUnauthorized: true ✅
356
+ const agent = new https.Agent();
357
+
358
+ // For internal/self-signed CAs in staging environments:
359
+ // NODE_EXTRA_CA_CERTS=/path/to/internal-ca.crt node server.js
360
+ ```
361
+
362
+ ```dart
363
+ // BEFORE (vulnerable — Flutter/Dart)
364
+ final client = HttpClient()
365
+ ..badCertificateCallback = (cert, host, port) => true; // ❌
366
+
367
+ // AFTER — remove the callback (default validates certs) ✅
368
+ final client = HttpClient();
369
+
370
+ // For a private CA in integration tests only:
371
+ final context = SecurityContext()
372
+ ..setTrustedCertificates('test/certs/ca.crt');
373
+ final client = HttpClient(context: context);
374
+ ```
@@ -120,3 +120,73 @@ def test_vuln_type_exploit(client, attacker_token):
120
120
  )
121
121
  assert response.status_code == 403 # currently 200 — RED
122
122
  ```
123
+
124
+ ### React / Next.js (Vitest + Testing Library)
125
+ ```typescript
126
+ // Sensitive storage: token must NOT land in localStorage
127
+ import { render, fireEvent, waitFor } from '@testing-library/react';
128
+ import LoginForm from '../../components/LoginForm';
129
+
130
+ test('SHOULD NOT store auth token in localStorage', async () => {
131
+ render(<LoginForm />);
132
+ fireEvent.submit(screen.getByRole('form'));
133
+ await waitFor(() => {
134
+ expect(localStorage.getItem('token')).toBeNull(); // currently set — RED
135
+ });
136
+ });
137
+
138
+ // XSS: dangerouslySetInnerHTML must not accept unsanitized input
139
+ test('SHOULD sanitize user content before rendering', () => {
140
+ const xssPayload = '<script>alert(1)</script>';
141
+ const { container } = render(<CommentBody content={xssPayload} />);
142
+ expect(container.innerHTML).not.toContain('<script>'); // currently reflected — RED
143
+ });
144
+ ```
145
+
146
+ ### React Native / Expo (Jest)
147
+ ```javascript
148
+ // Route param injection: params must be validated before API use
149
+ import { renderRouter, screen } from 'expo-router/testing-library';
150
+
151
+ test('SHOULD NOT pass raw route params to API query', async () => {
152
+ const maliciousParam = "1 UNION SELECT * FROM users";
153
+ // Render the screen with a crafted route param
154
+ renderRouter({ initialUrl: `/item/${encodeURIComponent(maliciousParam)}` });
155
+ // Assert the API was NOT called with the raw param
156
+ expect(mockApiClient.getItem).not.toHaveBeenCalledWith(maliciousParam); // currently called — RED
157
+ });
158
+
159
+ // Sensitive storage: tokens must use SecureStore, not AsyncStorage
160
+ import AsyncStorage from '@react-native-async-storage/async-storage';
161
+
162
+ test('SHOULD NOT store token in plain AsyncStorage', async () => {
163
+ await simulateLogin({ username: 'user', password: 'pass' });
164
+ const stored = await AsyncStorage.getItem('token');
165
+ expect(stored).toBeNull(); // currently stored in plain AsyncStorage — RED
166
+ });
167
+ ```
168
+
169
+ ### Flutter / Dart (flutter_test)
170
+ ```dart
171
+ import 'package:flutter_test/flutter_test.dart';
172
+ import 'package:shared_preferences/shared_preferences.dart';
173
+
174
+ void main() {
175
+ // Sensitive storage: token must NOT be in unencrypted SharedPreferences
176
+ test('SHOULD NOT store auth token in SharedPreferences', () async {
177
+ SharedPreferences.setMockInitialValues({});
178
+ await simulateLogin(username: 'user', password: 'password');
179
+ final prefs = await SharedPreferences.getInstance();
180
+ expect(prefs.getString('token'), isNull,
181
+ reason: 'Tokens must not be in unencrypted SharedPreferences — use flutter_secure_storage'); // currently stored — RED
182
+ });
183
+
184
+ // TLS bypass: HTTP client must not disable certificate validation
185
+ test('SHOULD enforce TLS certificate verification', () {
186
+ final client = buildHttpClient(); // the app's HTTP client factory
187
+ // Inspect that no badCertificateCallback bypasses verification
188
+ expect(client.badCertificateCallback, isNull,
189
+ reason: 'badCertificateCallback must not be set to bypass TLS'); // currently bypassed — RED
190
+ });
191
+ }
192
+ ```
@@ -22,6 +22,22 @@ Go through this checklist before closing the vulnerability:
22
22
  - [ ] **Performance acceptable** — the patch doesn't add unbounded DB queries or blocking I/O
23
23
  - [ ] **No secrets in code** — patch doesn't hardcode keys, tokens, or credentials
24
24
 
25
+ **React / Next.js additions:**
26
+ - [ ] **`dangerouslySetInnerHTML` removed or wrapped** — confirm DOMPurify is imported and called before all remaining usages
27
+ - [ ] **Next.js middleware matcher is correct** — `/api/:path*` or tighter; public routes (health checks, webhooks) still reachable
28
+ - [ ] **`app.json` / `.env.local` clean** — no API keys or secrets committed; `*.env` is in `.gitignore`
29
+
30
+ **React Native / Expo additions:**
31
+ - [ ] **`AsyncStorage` fully migrated** — no remaining `setItem('token', ...)` calls; `expo-secure-store` in `package.json`
32
+ - [ ] **Offline token refresh still works** — `SecureStore.getItemAsync` is called in the right lifecycle (not before `SecureStore.isAvailableAsync()` on web)
33
+ - [ ] **Deep link params validated** — any `route.params` passed to API calls are sanitized or type-checked
34
+
35
+ **Flutter additions:**
36
+ - [ ] **`flutter_secure_storage` in `pubspec.yaml`** — dependency present and `flutter pub get` ran
37
+ - [ ] **No remaining `SharedPreferences` calls for sensitive keys** — grep for `prefs.getString('token')`, `prefs.setString('password', ...)`
38
+ - [ ] **TLS `badCertificateCallback` fully removed** — grep the entire `lib/` directory for `badCertificateCallback`
39
+ - [ ] **iOS entitlements updated if needed** — `flutter_secure_storage` requires Keychain Sharing capability on iOS
40
+
25
41
  ### Step 3: Clean the patch
26
42
  - Remove any debugging `console.log` or `print` statements added during patching
27
43
  - Extract reusable security logic into middleware or utility functions if it appears in more than one place
@@ -0,0 +1,52 @@
1
+ /// TDD Remediation: Red Phase Sample Test (Flutter / Dart)
2
+ ///
3
+ /// Replace the boilerplate below with the specific exploit you are verifying.
4
+ /// This test MUST fail initially (Red Phase). Once you apply the fix, it MUST pass (Green Phase).
5
+ ///
6
+ /// Run with: flutter test test/security/
7
+
8
+ import 'package:flutter_test/flutter_test.dart';
9
+ // import 'package:shared_preferences/shared_preferences.dart';
10
+ // import 'package:flutter_secure_storage/flutter_secure_storage.dart';
11
+ // import '../../lib/services/auth_service.dart'; // update with your auth service path
12
+
13
+ void main() {
14
+ group('Security Vulnerability Remediation - Red Phase', () {
15
+
16
+ // ── Example 1: Sensitive Storage ─────────────────────────────────────────
17
+ // test('SHOULD NOT store auth token in plain SharedPreferences', () async {
18
+ // SharedPreferences.setMockInitialValues({});
19
+ //
20
+ // // Act: simulate what the app does after login
21
+ // // await AuthService().login(username: 'user', password: 'pass');
22
+ //
23
+ // // Assert: token must NOT be in unencrypted SharedPreferences
24
+ // final prefs = await SharedPreferences.getInstance();
25
+ // expect(prefs.getString('token'), isNull,
26
+ // reason: 'Use flutter_secure_storage instead'); // currently stored — RED
27
+ // });
28
+
29
+ // ── Example 2: TLS Bypass ─────────────────────────────────────────────────
30
+ // test('SHOULD enforce TLS certificate verification', () {
31
+ // final client = buildHttpClient(); // your app's HTTP client factory
32
+ // expect(client.badCertificateCallback, isNull,
33
+ // reason: 'badCertificateCallback must not bypass TLS'); // currently bypassed — RED
34
+ // });
35
+
36
+ // ── Example 3: Navigation Param Injection ─────────────────────────────────
37
+ // test('SHOULD NOT use raw route params in API calls', () async {
38
+ // const maliciousId = "1; DROP TABLE users";
39
+ // // Simulate screen loading with a crafted route argument
40
+ // // final result = await ItemService().fetchItem(id: maliciousId);
41
+ // //
42
+ // // Assert the input was validated / rejected
43
+ // // expect(result, isNull); // currently fetches — RED
44
+ // });
45
+
46
+ test('PLACEHOLDER — replace with your exploit assertion', () {
47
+ // Remove this placeholder and uncomment one of the examples above.
48
+ expect(true, isTrue);
49
+ });
50
+
51
+ });
52
+ }
@@ -0,0 +1,59 @@
1
+ /**
2
+ * TDD Remediation: Red Phase Sample Test (React / Next.js — Testing Library + Vitest)
3
+ *
4
+ * For UI-layer security tests: XSS via dangerouslySetInnerHTML, sensitive data
5
+ * rendering, unauthenticated route access, client-side auth bypass, etc.
6
+ *
7
+ * Replace the boilerplate below with the specific exploit you are verifying.
8
+ * This test MUST fail initially (Red Phase). Once you apply the fix, it MUST pass (Green Phase).
9
+ *
10
+ * Run with: vitest run __tests__/security/
11
+ */
12
+
13
+ import { describe, it, expect, vi, beforeEach } from 'vitest';
14
+ import { render, screen, waitFor } from '@testing-library/react';
15
+ // import ComponentUnderTest from '../../components/ComponentUnderTest'; // update path
16
+
17
+ describe('Security Vulnerability Remediation - Red Phase (UI)', () => {
18
+
19
+ // ── Example 1: XSS via dangerouslySetInnerHTML ───────────────────────────
20
+ // it('SHOULD sanitize user content before rendering as HTML', () => {
21
+ // const xssPayload = '<script>window.__xss = true</script>';
22
+ // render(<CommentBody content={xssPayload} />);
23
+ //
24
+ // // Script tag must not be injected into the DOM
25
+ // expect(document.querySelector('script')).toBeNull(); // currently rendered — RED
26
+ // expect((window as any).__xss).toBeUndefined();
27
+ // });
28
+
29
+ // ── Example 2: Sensitive data must not appear in rendered output ─────────
30
+ // it('SHOULD NOT expose auth token in the DOM', () => {
31
+ // render(<UserProfile token="super-secret-jwt" />);
32
+ // expect(screen.queryByText('super-secret-jwt')).toBeNull(); // currently visible — RED
33
+ // });
34
+
35
+ // ── Example 3: Protected route must reject unauthenticated users ─────────
36
+ // it('SHOULD NOT render protected content without a valid session', () => {
37
+ // render(<ProtectedPage />, { wrapper: UnauthenticatedProvider });
38
+ // expect(screen.queryByRole('main')).toBeNull(); // currently renders — RED
39
+ // expect(screen.getByText(/sign in/i)).toBeInTheDocument();
40
+ // });
41
+
42
+ // ── Example 4: Form input must be sanitized before submission ────────────
43
+ // it('SHOULD strip script tags from form input before submit', async () => {
44
+ // const user = userEvent.setup();
45
+ // render(<CommentForm onSubmit={mockSubmit} />);
46
+ // await user.type(screen.getByRole('textbox'), '<script>alert(1)</script>');
47
+ // await user.click(screen.getByRole('button', { name: /submit/i }));
48
+ //
49
+ // expect(mockSubmit).toHaveBeenCalledWith(
50
+ // expect.not.objectContaining({ body: expect.stringContaining('<script>') })
51
+ // ); // currently passes raw payload — RED
52
+ // });
53
+
54
+ it('PLACEHOLDER — replace with your exploit assertion', () => {
55
+ // Remove this placeholder and uncomment one of the examples above.
56
+ expect(true).toBe(true);
57
+ });
58
+
59
+ });
@@ -0,0 +1,26 @@
1
+ name: Security Tests
2
+
3
+ on:
4
+ push:
5
+ branches: [main, master]
6
+ pull_request:
7
+ branches: [main, master]
8
+
9
+ jobs:
10
+ security-tests:
11
+ name: Exploit Test Suite
12
+ runs-on: ubuntu-latest
13
+
14
+ steps:
15
+ - uses: actions/checkout@v4
16
+
17
+ - uses: subosito/flutter-action@v2
18
+ with:
19
+ flutter-version: 'stable'
20
+ cache: true
21
+
22
+ - name: Install dependencies
23
+ run: flutter pub get
24
+
25
+ - name: Run security exploit tests
26
+ run: flutter test test/security/