itworksbut 0.4.0 → 0.6.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +35 -31
- package/bin/itworksbut.js +9 -0
- package/package.json +1 -1
- package/src/checks/api/mass-assignment-risk.js +81 -0
- package/src/checks/api/missing-method-guard.js +68 -0
- package/src/checks/api/no-schema-validation.js +68 -0
- package/src/checks/auth/missing-csrf-protection.js +75 -0
- package/src/checks/dependencies/outdated-packages.js +297 -0
- package/src/checks/electron/remote-content-with-node.js +52 -0
- package/src/checks/files/path-traversal-risk.js +62 -0
- package/src/checks/frontend/localstorage-token.js +42 -0
- package/src/checks/index.js +23 -1
- package/src/checks/next/public-server-code-risk.js +64 -0
- package/src/checks/ssrf/user-controlled-fetch.js +60 -0
- package/src/checks/tauri/remote-url-permissions-risk.js +115 -0
- package/src/cli/output.js +9 -9
- package/src/cli/parseArgs.js +3 -1
- package/src/core/checkResults.js +53 -0
- package/src/core/scanner.js +33 -4
- package/src/reporters/consoleReporter.js +42 -1
- package/src/reporters/consoleStyle.js +30 -0
- package/src/reporters/jsonReporter.js +3 -0
- package/src/reporters/markdownReport.js +203 -0
- package/src/utils/packageManager.js +28 -0
package/README.md
CHANGED
|
@@ -6,7 +6,19 @@ It focuses on common "it works, but..." risks often found in AI-generated or rus
|
|
|
6
6
|
|
|
7
7
|
For every finding, ItWorksBut gives you a copy-ready fix prompt you can paste into your coding agent. It does not just say what is wrong; it tells your AI exactly what to inspect, what to change, and what not to leak.
|
|
8
8
|
|
|
9
|
-
It
|
|
9
|
+
It mostly reads files and reports findings. It does not send telemetry. The outdated-package check may invoke your local package manager, and the CLI only writes files when you explicitly ask for `todo.md` with `--todo` or `report.md` with `--report`.
|
|
10
|
+
|
|
11
|
+
## Table of Contents
|
|
12
|
+
|
|
13
|
+
- [Installation](#installation)
|
|
14
|
+
- [Quick Start](#quick-start)
|
|
15
|
+
- [Options](#options)
|
|
16
|
+
- [Terminal Experience](#terminal-experience)
|
|
17
|
+
- [GitHub Actions](#github-actions)
|
|
18
|
+
- [Configuration](#configuration)
|
|
19
|
+
- [Example Output](#example-output)
|
|
20
|
+
- [What It Detects](#what-it-detects)
|
|
21
|
+
- [What It Does Not Guarantee](#what-it-does-not-guarantee)
|
|
10
22
|
|
|
11
23
|
## Installation
|
|
12
24
|
|
|
@@ -41,6 +53,7 @@ itworksbut scan --fail-on high
|
|
|
41
53
|
itworksbut scan --json
|
|
42
54
|
itworksbut scan --sarif > itworksbut.sarif
|
|
43
55
|
itworksbut scan --todo
|
|
56
|
+
itworksbut scan --report
|
|
44
57
|
itworksbut scan --config itworksbut.config.json
|
|
45
58
|
itworksbut scan --verbose
|
|
46
59
|
itworksbut --version
|
|
@@ -58,6 +71,7 @@ itworksbut scan [options]
|
|
|
58
71
|
- `--json`: Print machine-readable JSON only. No banner, colors, spinner, table, or extra text.
|
|
59
72
|
- `--sarif`: Print SARIF JSON for GitHub Code Scanning. No banner, colors, spinner, table, or extra text.
|
|
60
73
|
- `--todo`: Write an AI-ready `todo.md` into the scanned project with prioritized findings, fix prompts, and acceptance criteria.
|
|
74
|
+
- `--report`: Write a Markdown `report.md` into the current working directory.
|
|
61
75
|
- `--verbose`: Include scanner warnings and extra metadata in console output.
|
|
62
76
|
- `--quiet`: Print only the summary.
|
|
63
77
|
- `--no-color`: Disable colored output.
|
|
@@ -97,6 +111,14 @@ itworksbut scan --todo
|
|
|
97
111
|
|
|
98
112
|
This writes `todo.md` to the scanned project. The file is ordered by severity and includes agent rules, exact fix prompts, locations, recommendations, and final verification checkboxes.
|
|
99
113
|
|
|
114
|
+
To create a Markdown scan report:
|
|
115
|
+
|
|
116
|
+
```sh
|
|
117
|
+
itworksbut scan --report
|
|
118
|
+
```
|
|
119
|
+
|
|
120
|
+
This writes `report.md` to the current working directory with check statuses, summaries, details, and recommendations.
|
|
121
|
+
|
|
100
122
|
## GitHub Actions
|
|
101
123
|
|
|
102
124
|
```yaml
|
|
@@ -166,38 +188,10 @@ release/**
|
|
|
166
188
|
## Example Output
|
|
167
189
|
|
|
168
190
|

|
|
169
|
-
✖ CRITICAL It works, but your .env is tracked.
|
|
170
|
-
✔ Check: env.env-file-tracked
|
|
171
|
-
📁 File: .env
|
|
172
|
-
🤔 Why: .env appears to be tracked by git. Secrets may be exposed.
|
|
173
|
-
🤖 prompt: You are a senior security engineer working in this repository. Fix the ItWorksBut finding env.env-file-tracked at .env. Treat this as a concrete finding. Problem: .env appears to be tracked by git. Secrets may be exposed. Required change: Remove tracked env files from git, add safe examples such as .env.example, and make sure any exposed credentials are treated as compromised. Do not print, log, or preserve raw secret values; use placeholders only. Keep existing behavior intact where possible, add or update focused tests when useful, and do not silence the check unless the underlying risk is actually fixed.
|
|
174
|
-
|
|
175
|
-
▲ HIGH It works, but your SQL query is one template string away from pain.
|
|
176
|
-
✔ Check: database.raw-sql-interpolation
|
|
177
|
-
📁 File: src/db.js:12
|
|
178
|
-
🤔 Why: Possible SQL injection risk: raw SQL appears to be built with template string interpolation.
|
|
179
|
-
🤖 prompt: You are a senior security engineer working in this repository. Fix the ItWorksBut finding database.raw-sql-interpolation at src/db.js:12. This finding is heuristic, so inspect the code first and only change behavior when the risk is real. Problem: Possible SQL injection risk: raw SQL appears to be built with template string interpolation. Required change: Replace SQL string interpolation or concatenation with parameterized queries, prepared statements, or a safe ORM query builder. Keep existing behavior intact where possible, add or update focused tests when useful, and do not silence the check unless the underlying risk is actually fixed.
|
|
180
|
-
|
|
181
|
-
SUMMARY
|
|
182
|
-
|
|
183
|
-
- ship status: DO NOT SHIP
|
|
184
|
-
- Fix the red stuff before production.
|
|
185
|
-
- total findings: 2
|
|
186
|
-
- critical: 1
|
|
187
|
-
- high: 1
|
|
188
|
-
- medium: 0
|
|
189
|
-
- low: 0
|
|
190
|
-
- info: 0
|
|
191
|
-
- fail-on: high
|
|
192
|
-
- exit decision: 1
|
|
193
|
-
|
|
194
|
-
```
|
|
195
|
-
|
|
196
|
-
Secret-like findings never print the full secret value. Findings report the file, line, and secret type where possible.
|
|
197
191
|
|
|
198
192
|
## What It Detects
|
|
199
193
|
|
|
200
|
-
The baseline includes
|
|
194
|
+
The baseline includes 51 modular checks:
|
|
201
195
|
|
|
202
196
|
- `git.gitignore-missing`
|
|
203
197
|
- `git.gitignore-incomplete`
|
|
@@ -211,6 +205,7 @@ The baseline includes 40 modular checks:
|
|
|
211
205
|
- `dependencies.multiple-lockfiles`
|
|
212
206
|
- `dependencies.install-scripts-risk`
|
|
213
207
|
- `dependencies.audit-script-missing`
|
|
208
|
+
- `dependencies.outdated-packages`
|
|
214
209
|
- `package.scripts-missing`
|
|
215
210
|
- `ci.no-ci-config`
|
|
216
211
|
- `ci.npm-install-instead-of-npm-ci`
|
|
@@ -228,6 +223,10 @@ The baseline includes 40 modular checks:
|
|
|
228
223
|
- `api.idor-risk`
|
|
229
224
|
- `auth.jwt-secret-weak-or-fallback`
|
|
230
225
|
- `auth.password-hashing-missing`
|
|
226
|
+
- `auth.missing-csrf-protection`
|
|
227
|
+
- `api.missing-method-guard`
|
|
228
|
+
- `api.mass-assignment-risk`
|
|
229
|
+
- `api.no-schema-validation`
|
|
231
230
|
- `database.raw-sql-interpolation`
|
|
232
231
|
- `database.no-migrations`
|
|
233
232
|
- `cookies.insecure-session-cookie`
|
|
@@ -235,10 +234,16 @@ The baseline includes 40 modular checks:
|
|
|
235
234
|
- `webhooks.missing-raw-body`
|
|
236
235
|
- `llm.prompt-injection-risk`
|
|
237
236
|
- `frontend.sourcemaps-production`
|
|
237
|
+
- `frontend.localstorage-token`
|
|
238
|
+
- `files.path-traversal-risk`
|
|
239
|
+
- `ssrf.user-controlled-fetch`
|
|
240
|
+
- `next.public-server-code-risk`
|
|
238
241
|
- `config.debug-production`
|
|
239
242
|
- `electron.node-integration-enabled`
|
|
240
243
|
- `electron.context-isolation-disabled`
|
|
244
|
+
- `electron.remote-content-with-node`
|
|
241
245
|
- `tauri.dangerous-allowlist-or-capabilities`
|
|
246
|
+
- `tauri.remote-url-permissions-risk`
|
|
242
247
|
|
|
243
248
|
Each check is a plain ESM module with an `id`, metadata, and async `run(context)` function. Add new checks under `src/checks/` and register them in `src/checks/index.js`.
|
|
244
249
|
|
|
@@ -247,4 +252,3 @@ Each check is a plain ESM module with an `id`, metadata, and async `run(context)
|
|
|
247
252
|
ItWorksBut is a static heuristic scanner, not a pentest, SAST replacement, dependency vulnerability database, or runtime security monitor. Findings intentionally use wording such as "possible", "potential", and "appears to" when a check is heuristic.
|
|
248
253
|
|
|
249
254
|
Use it as a CI guardrail for common project hygiene and security mistakes. For production systems, combine it with code review, tests, dependency scanning, secrets scanning, threat modeling, and focused security assessment.
|
|
250
|
-
```
|
package/bin/itworksbut.js
CHANGED
|
@@ -10,6 +10,7 @@ import { reportConsole } from '../src/reporters/consoleReporter.js';
|
|
|
10
10
|
import { reportJson } from '../src/reporters/jsonReporter.js';
|
|
11
11
|
import { reportSarif } from '../src/reporters/sarifReporter.js';
|
|
12
12
|
import { writeTodoReport } from '../src/reporters/todoReporter.js';
|
|
13
|
+
import { writeMarkdownReport } from '../src/reporters/markdownReport.js';
|
|
13
14
|
|
|
14
15
|
async function main() {
|
|
15
16
|
const args = parseArgs(process.argv.slice(2));
|
|
@@ -59,6 +60,14 @@ async function main() {
|
|
|
59
60
|
reportConsole(result, args);
|
|
60
61
|
}
|
|
61
62
|
|
|
63
|
+
if (args.report) {
|
|
64
|
+
const report = await writeMarkdownReport(result);
|
|
65
|
+
if (!args.json && !args.sarif) {
|
|
66
|
+
const verb = report.overwritten ? 'Overwrote' : 'Wrote';
|
|
67
|
+
process.stdout.write(`${verb} scan report: ${report.filePath}\n`);
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
62
71
|
return getExitCode(result.findings, result.config.failOn);
|
|
63
72
|
}
|
|
64
73
|
|
package/package.json
CHANGED
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
import { isCodeLikeFile, isEnvExampleFile, isLockfile, lineFromOffset, readNearby } from "../helpers.js";
|
|
2
|
+
|
|
3
|
+
const MASS_ASSIGNMENT_PATTERNS = [
|
|
4
|
+
{
|
|
5
|
+
regex: /\b(?:prisma\.\w+\.)?(?:create|update|upsert)\s*\(\s*{[\s\S]{0,600}?\bdata\s*:\s*(?:req\.body|body|input)\b/g,
|
|
6
|
+
label: "direct data object",
|
|
7
|
+
severity: "high"
|
|
8
|
+
},
|
|
9
|
+
{
|
|
10
|
+
regex: /\b(?:db|database|collection|\w+)\.(?:update|updateOne|updateMany|findOneAndUpdate)\s*\([\s\S]{0,300}?(?:req\.body|body|input)\b/g,
|
|
11
|
+
label: "direct update payload",
|
|
12
|
+
severity: "high"
|
|
13
|
+
},
|
|
14
|
+
{
|
|
15
|
+
regex: /\b(?:User|Account|Profile|Model|model|\w+)\.(?:create|update)\s*\(\s*(?:req\.body|body|input)\b/g,
|
|
16
|
+
label: "model create/update payload",
|
|
17
|
+
severity: "high"
|
|
18
|
+
},
|
|
19
|
+
{
|
|
20
|
+
regex: /\$set\s*:\s*(?:req\.body|body|input)\b/g,
|
|
21
|
+
label: "mongodb set payload",
|
|
22
|
+
severity: "high"
|
|
23
|
+
},
|
|
24
|
+
{
|
|
25
|
+
regex: /Object\.assign\s*\(\s*(?:user|entity|account|profile|record|model)[\w$]*\s*,\s*(?:req\.body|body|input)\b/g,
|
|
26
|
+
label: "object assign from request input",
|
|
27
|
+
severity: "medium"
|
|
28
|
+
},
|
|
29
|
+
{
|
|
30
|
+
regex: /\{\s*\.\.\.(?:req\.body|body)\s*}/g,
|
|
31
|
+
label: "spread request body",
|
|
32
|
+
severity: "medium",
|
|
33
|
+
requiresCreateOrUpdateContext: true
|
|
34
|
+
}
|
|
35
|
+
];
|
|
36
|
+
|
|
37
|
+
const SAFE_FIELD_RE =
|
|
38
|
+
/\b(?:pick|omit|allowedFields|allowlist|whitelist|safeData|validatedData|schema\.parse|schema\.safeParse|safeParse|zod|Joi|joi|yup|valibot)\b/i;
|
|
39
|
+
const RISKY_FIELD_RE =
|
|
40
|
+
/\b(?:role|isAdmin|admin|plan|verified|emailVerified|ownerId|userId|tenantId|accountId|permissions|credits|balance|price|status)\b/i;
|
|
41
|
+
const CREATE_OR_UPDATE_RE = /\b(?:create|update|upsert|insert|save|data\s*:|\$set)\b/i;
|
|
42
|
+
|
|
43
|
+
export default {
|
|
44
|
+
id: "api.mass-assignment-risk",
|
|
45
|
+
title: "Create and update operations should not trust raw request bodies",
|
|
46
|
+
category: "api",
|
|
47
|
+
severity: "high",
|
|
48
|
+
tags: ["api", "database", "mass-assignment", "heuristic"],
|
|
49
|
+
run: async (context) => {
|
|
50
|
+
const findings = [];
|
|
51
|
+
|
|
52
|
+
for (const file of context.textFiles) {
|
|
53
|
+
if (isLockfile(file) || isEnvExampleFile(file) || !isCodeLikeFile(file)) continue;
|
|
54
|
+
const content = await context.readFileSafe(file);
|
|
55
|
+
if (!content || !/\b(?:req\.body|body|input|Object\.assign|\$set)\b/.test(content)) continue;
|
|
56
|
+
|
|
57
|
+
for (const pattern of MASS_ASSIGNMENT_PATTERNS) {
|
|
58
|
+
pattern.regex.lastIndex = 0;
|
|
59
|
+
let match;
|
|
60
|
+
while ((match = pattern.regex.exec(content)) !== null) {
|
|
61
|
+
const line = lineFromOffset(content, match.index);
|
|
62
|
+
const nearby = await readNearby(context, file, line, 8);
|
|
63
|
+
if (pattern.requiresCreateOrUpdateContext && !CREATE_OR_UPDATE_RE.test(nearby)) continue;
|
|
64
|
+
if (SAFE_FIELD_RE.test(nearby)) continue;
|
|
65
|
+
|
|
66
|
+
findings.push({
|
|
67
|
+
severity: RISKY_FIELD_RE.test(nearby) ? "high" : pattern.severity === "high" ? "high" : "medium",
|
|
68
|
+
message: "User-controlled input appears to be passed directly into a create or update operation.",
|
|
69
|
+
file,
|
|
70
|
+
line,
|
|
71
|
+
recommendation: "Whitelist allowed fields explicitly. Never pass req.body directly into database create/update calls.",
|
|
72
|
+
heuristic: true,
|
|
73
|
+
metadata: { pattern: pattern.label }
|
|
74
|
+
});
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
return findings.slice(0, 100);
|
|
80
|
+
}
|
|
81
|
+
};
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
import { isServerOrApiFile, lineFromOffset, readNearby } from "../helpers.js";
|
|
2
|
+
|
|
3
|
+
const ALL_ROUTE_RE = /\b(?:app|router|server)\.all\s*\(/g;
|
|
4
|
+
const NEXT_PAGES_HANDLER_RE = /\bexport\s+default\s+(?:async\s+)?function\s+\w*\s*\(\s*(?:req|request)\s*,\s*(?:res|response)\s*\)/g;
|
|
5
|
+
const NAMED_HANDLER_RE = /\bexport\s+(?:async\s+)?function\s+handler\s*\(\s*(?:req|request)\s*,\s*(?:res|response)\s*\)/g;
|
|
6
|
+
const METHOD_GUARD_RE =
|
|
7
|
+
/\b(?:req|request)\.method\b|\bswitch\s*\(\s*(?:req|request)\.method\s*\)|\ballowedMethods\b|\bmethodNotAllowed\b|\breturn\s+new\s+Response\s*\([^)]*405|\bstatus\s*\(\s*405\s*\)/i;
|
|
8
|
+
const METHOD_EXPORT_RE = /\bexport\s+(?:async\s+)?function\s+(?:GET|POST|PUT|PATCH|DELETE|HEAD|OPTIONS)\s*\(/;
|
|
9
|
+
|
|
10
|
+
export default {
|
|
11
|
+
id: "api.missing-method-guard",
|
|
12
|
+
title: "API handlers should restrict HTTP methods",
|
|
13
|
+
category: "api",
|
|
14
|
+
severity: "medium",
|
|
15
|
+
tags: ["api", "http-methods", "heuristic"],
|
|
16
|
+
run: async (context) => {
|
|
17
|
+
const findings = [];
|
|
18
|
+
|
|
19
|
+
for (const file of context.textFiles) {
|
|
20
|
+
if (!isApiCandidate(file)) continue;
|
|
21
|
+
const content = await context.readFileSafe(file);
|
|
22
|
+
if (!content) continue;
|
|
23
|
+
|
|
24
|
+
ALL_ROUTE_RE.lastIndex = 0;
|
|
25
|
+
let allMatch;
|
|
26
|
+
while ((allMatch = ALL_ROUTE_RE.exec(content)) !== null) {
|
|
27
|
+
const line = lineFromOffset(content, allMatch.index);
|
|
28
|
+
const nearby = await readNearby(context, file, line, 8);
|
|
29
|
+
if (/\b(?:allowedMethods|methodNotAllowed|405)\b/i.test(nearby)) continue;
|
|
30
|
+
findings.push(methodFinding(file, line, "app-router-all"));
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
if (METHOD_EXPORT_RE.test(content) || METHOD_GUARD_RE.test(content)) continue;
|
|
34
|
+
|
|
35
|
+
for (const pattern of [NEXT_PAGES_HANDLER_RE, NAMED_HANDLER_RE]) {
|
|
36
|
+
pattern.lastIndex = 0;
|
|
37
|
+
let match;
|
|
38
|
+
while ((match = pattern.exec(content)) !== null) {
|
|
39
|
+
findings.push(methodFinding(file, lineFromOffset(content, match.index), "handler-without-method-guard"));
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
return findings.slice(0, 100);
|
|
45
|
+
}
|
|
46
|
+
};
|
|
47
|
+
|
|
48
|
+
function isApiCandidate(file) {
|
|
49
|
+
return (
|
|
50
|
+
/\.[cm]?[jt]sx?$/.test(file) &&
|
|
51
|
+
(isServerOrApiFile(file) ||
|
|
52
|
+
file.startsWith("routes/") ||
|
|
53
|
+
file.startsWith("api/") ||
|
|
54
|
+
file.includes("/controllers/") ||
|
|
55
|
+
file.includes("/handlers/"))
|
|
56
|
+
);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function methodFinding(file, line, pattern) {
|
|
60
|
+
return {
|
|
61
|
+
message: "This API handler appears to process requests without an explicit HTTP method guard.",
|
|
62
|
+
file,
|
|
63
|
+
line,
|
|
64
|
+
recommendation: "Restrict API routes to the intended HTTP methods and return 405 Method Not Allowed for unsupported methods.",
|
|
65
|
+
heuristic: true,
|
|
66
|
+
metadata: { pattern }
|
|
67
|
+
};
|
|
68
|
+
}
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
import { isServerOrApiFile, lineFromOffset, readNearby } from "../helpers.js";
|
|
2
|
+
|
|
3
|
+
const REQUEST_INPUT_RE =
|
|
4
|
+
/\breq\.(?:body|query|params)\b|\brequest\.json\s*\(|\bsearchParams\.get\s*\(|\bnew\s+URL\s*\(\s*(?:req|request)\.url\s*\)\.searchParams\b|\bformData\s*\(|\bctx\.request\.body\b/g;
|
|
5
|
+
const VALIDATION_RE =
|
|
6
|
+
/\b(?:zod|Joi|joi|yup|valibot|ajv|superstruct|TypeBox|validator|validatedData)\b|\.safeParse\s*\(|\.parse\s*\(\s*(?:req\.body|body|input|payload|data)|\.validate\s*\(\s*(?:req\.body|body|input|payload|data)|\bschema\.(?:validate|parse|safeParse)\b/i;
|
|
7
|
+
const GLOBAL_VALIDATION_RE = /\b(?:app|router|server)\.use\s*\([^)]*(?:validate|validator|schema|zod|joi|yup|ajv|valibot)/i;
|
|
8
|
+
|
|
9
|
+
export default {
|
|
10
|
+
id: "api.no-schema-validation",
|
|
11
|
+
title: "API request input should be schema validated",
|
|
12
|
+
category: "api",
|
|
13
|
+
severity: "high",
|
|
14
|
+
tags: ["api", "validation", "heuristic"],
|
|
15
|
+
run: async (context) => {
|
|
16
|
+
const findings = [];
|
|
17
|
+
const globalValidationSeen = await hasGlobalValidationMiddleware(context);
|
|
18
|
+
if (globalValidationSeen) return [];
|
|
19
|
+
|
|
20
|
+
for (const file of context.textFiles) {
|
|
21
|
+
if (!isApiFile(file)) continue;
|
|
22
|
+
const content = await context.readFileSafe(file);
|
|
23
|
+
if (!content) continue;
|
|
24
|
+
if (VALIDATION_RE.test(content)) continue;
|
|
25
|
+
|
|
26
|
+
REQUEST_INPUT_RE.lastIndex = 0;
|
|
27
|
+
const match = REQUEST_INPUT_RE.exec(content);
|
|
28
|
+
if (!match) continue;
|
|
29
|
+
|
|
30
|
+
const line = lineFromOffset(content, match.index);
|
|
31
|
+
const nearby = await readNearby(context, file, line, 10);
|
|
32
|
+
if (VALIDATION_RE.test(nearby)) continue;
|
|
33
|
+
|
|
34
|
+
findings.push({
|
|
35
|
+
message: "This API route appears to consume request input without an obvious schema validation step.",
|
|
36
|
+
file,
|
|
37
|
+
line,
|
|
38
|
+
recommendation: "Validate request body, query and params with a schema library such as Zod, Joi, Valibot, AJV or equivalent.",
|
|
39
|
+
heuristic: true,
|
|
40
|
+
metadata: { pattern: "request-input-without-schema-validation" }
|
|
41
|
+
});
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
return findings.slice(0, 100);
|
|
45
|
+
}
|
|
46
|
+
};
|
|
47
|
+
|
|
48
|
+
function isApiFile(file) {
|
|
49
|
+
return (
|
|
50
|
+
/\.[cm]?[jt]sx?$/.test(file) &&
|
|
51
|
+
(isServerOrApiFile(file) ||
|
|
52
|
+
file.startsWith("api/") ||
|
|
53
|
+
file.startsWith("routes/") ||
|
|
54
|
+
file.startsWith("handlers/") ||
|
|
55
|
+
file.startsWith("controllers/") ||
|
|
56
|
+
file.includes("/handlers/") ||
|
|
57
|
+
file.includes("/controllers/"))
|
|
58
|
+
);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
async function hasGlobalValidationMiddleware(context) {
|
|
62
|
+
for (const file of context.textFiles) {
|
|
63
|
+
if (!/\.[cm]?[jt]sx?$/.test(file) || !isServerOrApiFile(file)) continue;
|
|
64
|
+
const content = await context.readFileSafe(file);
|
|
65
|
+
if (content && GLOBAL_VALIDATION_RE.test(content)) return true;
|
|
66
|
+
}
|
|
67
|
+
return false;
|
|
68
|
+
}
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
import { isCodeLikeFile, isEnvExampleFile, isLockfile, lineFromOffset } from "../helpers.js";
|
|
2
|
+
|
|
3
|
+
const COOKIE_AUTH_RE =
|
|
4
|
+
/\b(?:res\.cookie|cookies\(\)\.set|response\.cookies\.set|setCookie|serialize)\s*\(\s*["'`](?:session|token|auth|jwt)["'`]|cookieSession\s*\(|express-session|\bsession\s*\(|credentials\s*:\s*["'`]include["'`]|withCredentials\s*:\s*true/gi;
|
|
5
|
+
const CSRF_PROTECTION_RE =
|
|
6
|
+
/\b(?:csrf|csurf|csrfToken|anti-csrf|verifyCsrf|validateCsrf|csrfProtection)\b|double\s+submit|\bsameSite\s*:\s*["'`]?(?:strict|lax)["'`]?\b/gi;
|
|
7
|
+
const STATE_CHANGING_ROUTE_RE =
|
|
8
|
+
/\b(?:app|router|server)\.(?:post|put|patch|delete)\s*\(|\bmethod\s*:\s*["'`](?:POST|PUT|PATCH|DELETE)["'`]|\breq\.method\s*={0,3}\s*["'`](?:POST|PUT|PATCH|DELETE)["'`]|\bexport\s+async\s+function\s+(?:POST|PUT|PATCH|DELETE)\s*\(/g;
|
|
9
|
+
|
|
10
|
+
export default {
|
|
11
|
+
id: "auth.missing-csrf-protection",
|
|
12
|
+
title: "Cookie-based authentication should include CSRF protection",
|
|
13
|
+
category: "auth",
|
|
14
|
+
severity: "high",
|
|
15
|
+
tags: ["auth", "csrf", "cookies", "heuristic"],
|
|
16
|
+
run: async (context) => {
|
|
17
|
+
const cookieMatches = [];
|
|
18
|
+
const stateChangingRoutes = [];
|
|
19
|
+
let csrfProtectionSeen = false;
|
|
20
|
+
|
|
21
|
+
for (const file of context.textFiles) {
|
|
22
|
+
if (isLockfile(file) || isEnvExampleFile(file) || !isCodeLikeFile(file)) continue;
|
|
23
|
+
const content = await context.readFileSafe(file);
|
|
24
|
+
if (!content) continue;
|
|
25
|
+
|
|
26
|
+
CSRF_PROTECTION_RE.lastIndex = 0;
|
|
27
|
+
if (CSRF_PROTECTION_RE.test(content)) {
|
|
28
|
+
csrfProtectionSeen = true;
|
|
29
|
+
break;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
COOKIE_AUTH_RE.lastIndex = 0;
|
|
33
|
+
let cookieMatch;
|
|
34
|
+
while ((cookieMatch = COOKIE_AUTH_RE.exec(content)) !== null) {
|
|
35
|
+
cookieMatches.push({
|
|
36
|
+
file,
|
|
37
|
+
line: lineFromOffset(content, cookieMatch.index),
|
|
38
|
+
pattern: normalizePattern(cookieMatch[0])
|
|
39
|
+
});
|
|
40
|
+
if (cookieMatches.length >= 25) break;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
STATE_CHANGING_ROUTE_RE.lastIndex = 0;
|
|
44
|
+
let routeMatch;
|
|
45
|
+
while ((routeMatch = STATE_CHANGING_ROUTE_RE.exec(content)) !== null) {
|
|
46
|
+
stateChangingRoutes.push({
|
|
47
|
+
file,
|
|
48
|
+
line: lineFromOffset(content, routeMatch.index),
|
|
49
|
+
pattern: normalizePattern(routeMatch[0])
|
|
50
|
+
});
|
|
51
|
+
if (stateChangingRoutes.length >= 25) break;
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
if (csrfProtectionSeen || cookieMatches.length === 0 || stateChangingRoutes.length === 0) return [];
|
|
56
|
+
|
|
57
|
+
const primaryCookie = cookieMatches[0];
|
|
58
|
+
return stateChangingRoutes.slice(0, 25).map((route) => ({
|
|
59
|
+
message: "Cookie-based authentication appears to be used without an obvious CSRF protection mechanism.",
|
|
60
|
+
file: route.file,
|
|
61
|
+
line: route.line,
|
|
62
|
+
recommendation: "Use SameSite cookies, CSRF tokens or another explicit CSRF mitigation for state-changing routes.",
|
|
63
|
+
heuristic: true,
|
|
64
|
+
metadata: {
|
|
65
|
+
pattern: "cookie-auth-with-state-changing-route",
|
|
66
|
+
cookieFile: primaryCookie.file,
|
|
67
|
+
cookieLine: primaryCookie.line
|
|
68
|
+
}
|
|
69
|
+
}));
|
|
70
|
+
}
|
|
71
|
+
};
|
|
72
|
+
|
|
73
|
+
function normalizePattern(value) {
|
|
74
|
+
return String(value || "").replace(/\s+/g, " ").slice(0, 80);
|
|
75
|
+
}
|