@lhi/tdd-audit 1.1.2 → 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/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
@@ -23,6 +23,9 @@ const targetWorkflowDir = isClaude
23
23
  // ─── 1. Framework Detection ──────────────────────────────────────────────────
24
24
 
25
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
+
26
29
  const pkgPath = path.join(projectDir, 'package.json');
27
30
  if (fs.existsSync(pkgPath)) {
28
31
  try {
@@ -43,6 +46,25 @@ function detectFramework() {
43
46
  return 'jest';
44
47
  }
45
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
+
46
68
  const framework = detectFramework();
47
69
 
48
70
  // ─── 2. Test Directory Detection ─────────────────────────────────────────────
@@ -71,10 +93,19 @@ const VULN_PATTERNS = [
71
93
  { name: 'XSS', severity: 'HIGH', pattern: /[^/]innerHTML\s*=(?!=)|dangerouslySetInnerHTML\s*=\s*\{\{|document\.write\s*\(|res\.send\s*\(`[^`]*\$\{req\./i },
72
94
  { name: 'Path Traversal', severity: 'HIGH', pattern: /(readFile|sendFile|createReadStream|open)\s*\(.*req\.(params|body|query)|path\.join\s*\([^)]*req\.(params|body|query)/i },
73
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 },
74
105
  ];
75
106
 
76
- const SCAN_EXTENSIONS = new Set(['.js', '.ts', '.jsx', '.tsx', '.mjs', '.py', '.go']);
77
- 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']);
78
109
 
79
110
  function* walkFiles(dir) {
80
111
  let entries;
@@ -87,9 +118,43 @@ function* walkFiles(dir) {
87
118
  }
88
119
  }
89
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
+
90
154
  function quickScan() {
91
155
  const findings = [];
92
156
  for (const filePath of walkFiles(projectDir)) {
157
+ const inTest = isTestFile(filePath);
93
158
  let lines;
94
159
  try { lines = fs.readFileSync(filePath, 'utf8').split('\n'); } catch { continue; }
95
160
  for (let i = 0; i < lines.length; i++) {
@@ -101,13 +166,15 @@ function quickScan() {
101
166
  file: path.relative(projectDir, filePath),
102
167
  line: i + 1,
103
168
  snippet: lines[i].trim().slice(0, 80),
169
+ inTestFile: inTest,
170
+ likelyFalsePositive: inTest && !!vuln.skipInTests,
104
171
  });
105
172
  break; // one finding per line
106
173
  }
107
174
  }
108
175
  }
109
176
  }
110
- return findings;
177
+ return [...findings, ...scanAppConfig()];
111
178
  }
112
179
 
113
180
  function printFindings(findings) {
@@ -115,18 +182,30 @@ function printFindings(findings) {
115
182
  console.log(' ✅ No obvious vulnerability patterns detected.\n');
116
183
  return;
117
184
  }
185
+ const real = findings.filter(f => !f.likelyFalsePositive);
186
+ const noisy = findings.filter(f => f.likelyFalsePositive);
187
+
118
188
  const bySeverity = { CRITICAL: [], HIGH: [], MEDIUM: [], LOW: [] };
119
- 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);
120
190
  const icons = { CRITICAL: '🔴', HIGH: '🟠', MEDIUM: '🟡', LOW: '🔵' };
121
191
 
122
- 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`);
123
193
  for (const [sev, list] of Object.entries(bySeverity)) {
124
194
  if (!list.length) continue;
125
195
  for (const f of list) {
126
- 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}`);
127
198
  console.log(` ${f.snippet}`);
128
199
  }
129
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
+
130
209
  console.log('\n Run /tdd-audit in your agent to remediate.\n');
131
210
  }
132
211
 
@@ -142,7 +221,8 @@ if (scanOnly) {
142
221
 
143
222
  // ─── 5. Install Skill Files ───────────────────────────────────────────────────
144
223
 
145
- 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`);
146
226
 
147
227
  if (!fs.existsSync(targetSkillDir)) fs.mkdirSync(targetSkillDir, { recursive: true });
148
228
 
@@ -160,11 +240,12 @@ if (!fs.existsSync(targetTestDir)) {
160
240
  }
161
241
 
162
242
  const testTemplateMap = {
163
- jest: 'sample.exploit.test.js',
164
- vitest: 'sample.exploit.test.vitest.js',
165
- mocha: 'sample.exploit.test.js',
166
- pytest: 'sample.exploit.test.pytest.py',
167
- 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',
168
249
  };
169
250
 
170
251
  const testTemplateName = testTemplateMap[framework];
@@ -217,11 +298,12 @@ const ciWorkflowPath = path.join(ciWorkflowDir, 'security-tests.yml');
217
298
 
218
299
  if (!fs.existsSync(ciWorkflowPath)) {
219
300
  const ciTemplateMap = {
220
- jest: 'security-tests.node.yml',
221
- vitest: 'security-tests.node.yml',
222
- mocha: 'security-tests.node.yml',
223
- pytest: 'security-tests.python.yml',
224
- 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',
225
307
  };
226
308
  const ciTemplatePath = path.join(__dirname, 'templates', 'workflows', ciTemplateMap[framework]);
227
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.2",
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/