@lhi/tdd-audit 1.1.2 → 1.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -120,3 +120,157 @@ 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
+ ### SSRF (Server-Side Request Forgery)
170
+ Supply a user-controlled URL pointing to an internal resource (e.g., `http://169.254.169.254/` AWS metadata).
171
+ Assert a 400 or 403 — not a 200 proxying internal content.
172
+ ```javascript
173
+ const res = await request(app)
174
+ .post('/api/fetch-preview')
175
+ .send({ url: 'http://169.254.169.254/latest/meta-data/' });
176
+ expect(res.status).toBe(400); // currently fetches and returns internal data — RED
177
+ ```
178
+
179
+ ### Open Redirect
180
+ Supply a fully external URL as the redirect destination.
181
+ Assert a 400 or that the redirect stays within the same origin.
182
+ ```javascript
183
+ const res = await request(app)
184
+ .get('/auth/callback')
185
+ .query({ redirect: 'https://evil.com/steal-token' });
186
+ expect(res.status).toBe(400); // currently 302 to attacker site — RED
187
+ // OR assert Location header is relative:
188
+ expect(res.headers.location).not.toMatch(/^https?:\/\//);
189
+ ```
190
+
191
+ ### NoSQL Injection
192
+ Supply a MongoDB operator object instead of a plain string value.
193
+ Assert the query is rejected or returns no data.
194
+ ```javascript
195
+ const res = await request(app)
196
+ .post('/api/login')
197
+ .send({ username: { $gt: '' }, password: { $gt: '' } });
198
+ expect(res.status).toBe(400); // currently returns first user record — RED
199
+ ```
200
+
201
+ ### Mass Assignment
202
+ Submit extra fields that should not be user-settable (e.g., `isAdmin`, `role`).
203
+ Assert the privileged field was ignored.
204
+ ```javascript
205
+ const res = await request(app)
206
+ .post('/api/users/register')
207
+ .send({ username: 'attacker', password: 'pass', isAdmin: true });
208
+ expect(res.status).toBe(201);
209
+ const user = await User.findOne({ username: 'attacker' });
210
+ expect(user.isAdmin).toBe(false); // currently set to true — RED
211
+ ```
212
+
213
+ ### Prototype Pollution
214
+ Submit a payload that sets `__proto__` to inject properties into Object.prototype.
215
+ Assert the injected property is not visible on a fresh `{}`.
216
+ ```javascript
217
+ const res = await request(app)
218
+ .post('/api/settings/merge')
219
+ .send({ '__proto__': { polluted: true } });
220
+ expect(res.status).toBe(200);
221
+ expect({}.polluted).toBeUndefined(); // currently true — RED
222
+ ```
223
+
224
+ ### Weak Crypto (Password Hashing)
225
+ Hash a known password and assert the resulting hash is not a raw MD5/SHA1 hex string.
226
+ ```javascript
227
+ const bcrypt = require('bcrypt');
228
+ const user = await User.create({ email: 'x@x.com', password: 'mypassword' });
229
+ // An MD5 hash of 'mypassword' is 34819d7beeabb9260a5c854bc85b3e44
230
+ expect(user.passwordHash).not.toBe('34819d7beeabb9260a5c854bc85b3e44');
231
+ // A proper bcrypt hash starts with $2b$
232
+ expect(user.passwordHash).toMatch(/^\$2[aby]\$/); // currently fails — RED
233
+ ```
234
+
235
+ ### Missing Rate Limiting
236
+ Send 10 rapid login attempts; assert the 11th is throttled (429).
237
+ ```javascript
238
+ for (let i = 0; i < 10; i++) {
239
+ await request(app).post('/api/auth/login').send({ email: 'x@x.com', password: 'wrong' });
240
+ }
241
+ const res = await request(app).post('/api/auth/login').send({ email: 'x@x.com', password: 'wrong' });
242
+ expect(res.status).toBe(429); // currently 401 — rate limit not enforced — RED
243
+ ```
244
+
245
+ ### Missing Security Headers
246
+ Assert a response includes the `X-Content-Type-Options` and `X-Frame-Options` headers set by Helmet.
247
+ ```javascript
248
+ const res = await request(app).get('/');
249
+ expect(res.headers['x-content-type-options']).toBe('nosniff'); // currently absent — RED
250
+ expect(res.headers['x-frame-options']).toBeDefined(); // currently absent — RED
251
+ ```
252
+
253
+ ### Flutter / Dart (flutter_test)
254
+ ```dart
255
+ import 'package:flutter_test/flutter_test.dart';
256
+ import 'package:shared_preferences/shared_preferences.dart';
257
+
258
+ void main() {
259
+ // Sensitive storage: token must NOT be in unencrypted SharedPreferences
260
+ test('SHOULD NOT store auth token in SharedPreferences', () async {
261
+ SharedPreferences.setMockInitialValues({});
262
+ await simulateLogin(username: 'user', password: 'password');
263
+ final prefs = await SharedPreferences.getInstance();
264
+ expect(prefs.getString('token'), isNull,
265
+ reason: 'Tokens must not be in unencrypted SharedPreferences — use flutter_secure_storage'); // currently stored — RED
266
+ });
267
+
268
+ // TLS bypass: HTTP client must not disable certificate validation
269
+ test('SHOULD enforce TLS certificate verification', () {
270
+ final client = buildHttpClient(); // the app's HTTP client factory
271
+ // Inspect that no badCertificateCallback bypasses verification
272
+ expect(client.badCertificateCallback, isNull,
273
+ reason: 'badCertificateCallback must not be set to bypass TLS'); // currently bypassed — RED
274
+ });
275
+ }
276
+ ```
@@ -22,6 +22,32 @@ 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
+ **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
+
45
+ **Flutter additions:**
46
+ - [ ] **`flutter_secure_storage` in `pubspec.yaml`** — dependency present and `flutter pub get` ran
47
+ - [ ] **No remaining `SharedPreferences` calls for sensitive keys** — grep for `prefs.getString('token')`, `prefs.setString('password', ...)`
48
+ - [ ] **TLS `badCertificateCallback` fully removed** — grep the entire `lib/` directory for `badCertificateCallback`
49
+ - [ ] **iOS entitlements updated if needed** — `flutter_secure_storage` requires Keychain Sharing capability on iOS
50
+
25
51
  ### Step 3: Clean the patch
26
52
  - Remove any debugging `console.log` or `print` statements added during patching
27
53
  - 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/
@@ -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.