@oculum/scanner 1.0.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/dist/formatters/cli-terminal.d.ts +27 -0
- package/dist/formatters/cli-terminal.d.ts.map +1 -0
- package/dist/formatters/cli-terminal.js +412 -0
- package/dist/formatters/cli-terminal.js.map +1 -0
- package/dist/formatters/github-comment.d.ts +41 -0
- package/dist/formatters/github-comment.d.ts.map +1 -0
- package/dist/formatters/github-comment.js +306 -0
- package/dist/formatters/github-comment.js.map +1 -0
- package/dist/formatters/grouping.d.ts +52 -0
- package/dist/formatters/grouping.d.ts.map +1 -0
- package/dist/formatters/grouping.js +152 -0
- package/dist/formatters/grouping.js.map +1 -0
- package/dist/formatters/index.d.ts +9 -0
- package/dist/formatters/index.d.ts.map +1 -0
- package/dist/formatters/index.js +35 -0
- package/dist/formatters/index.js.map +1 -0
- package/dist/formatters/vscode-diagnostic.d.ts +103 -0
- package/dist/formatters/vscode-diagnostic.d.ts.map +1 -0
- package/dist/formatters/vscode-diagnostic.js +151 -0
- package/dist/formatters/vscode-diagnostic.js.map +1 -0
- package/dist/index.d.ts +52 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +648 -0
- package/dist/index.js.map +1 -0
- package/dist/layer1/comments.d.ts +8 -0
- package/dist/layer1/comments.d.ts.map +1 -0
- package/dist/layer1/comments.js +203 -0
- package/dist/layer1/comments.js.map +1 -0
- package/dist/layer1/config-audit.d.ts +8 -0
- package/dist/layer1/config-audit.d.ts.map +1 -0
- package/dist/layer1/config-audit.js +252 -0
- package/dist/layer1/config-audit.js.map +1 -0
- package/dist/layer1/entropy.d.ts +8 -0
- package/dist/layer1/entropy.d.ts.map +1 -0
- package/dist/layer1/entropy.js +500 -0
- package/dist/layer1/entropy.js.map +1 -0
- package/dist/layer1/file-flags.d.ts +7 -0
- package/dist/layer1/file-flags.d.ts.map +1 -0
- package/dist/layer1/file-flags.js +112 -0
- package/dist/layer1/file-flags.js.map +1 -0
- package/dist/layer1/index.d.ts +36 -0
- package/dist/layer1/index.d.ts.map +1 -0
- package/dist/layer1/index.js +132 -0
- package/dist/layer1/index.js.map +1 -0
- package/dist/layer1/patterns.d.ts +8 -0
- package/dist/layer1/patterns.d.ts.map +1 -0
- package/dist/layer1/patterns.js +482 -0
- package/dist/layer1/patterns.js.map +1 -0
- package/dist/layer1/urls.d.ts +8 -0
- package/dist/layer1/urls.d.ts.map +1 -0
- package/dist/layer1/urls.js +296 -0
- package/dist/layer1/urls.js.map +1 -0
- package/dist/layer1/weak-crypto.d.ts +7 -0
- package/dist/layer1/weak-crypto.d.ts.map +1 -0
- package/dist/layer1/weak-crypto.js +291 -0
- package/dist/layer1/weak-crypto.js.map +1 -0
- package/dist/layer2/ai-agent-tools.d.ts +19 -0
- package/dist/layer2/ai-agent-tools.d.ts.map +1 -0
- package/dist/layer2/ai-agent-tools.js +528 -0
- package/dist/layer2/ai-agent-tools.js.map +1 -0
- package/dist/layer2/ai-endpoint-protection.d.ts +36 -0
- package/dist/layer2/ai-endpoint-protection.d.ts.map +1 -0
- package/dist/layer2/ai-endpoint-protection.js +332 -0
- package/dist/layer2/ai-endpoint-protection.js.map +1 -0
- package/dist/layer2/ai-execution-sinks.d.ts +18 -0
- package/dist/layer2/ai-execution-sinks.d.ts.map +1 -0
- package/dist/layer2/ai-execution-sinks.js +496 -0
- package/dist/layer2/ai-execution-sinks.js.map +1 -0
- package/dist/layer2/ai-fingerprinting.d.ts +7 -0
- package/dist/layer2/ai-fingerprinting.d.ts.map +1 -0
- package/dist/layer2/ai-fingerprinting.js +654 -0
- package/dist/layer2/ai-fingerprinting.js.map +1 -0
- package/dist/layer2/ai-prompt-hygiene.d.ts +19 -0
- package/dist/layer2/ai-prompt-hygiene.d.ts.map +1 -0
- package/dist/layer2/ai-prompt-hygiene.js +356 -0
- package/dist/layer2/ai-prompt-hygiene.js.map +1 -0
- package/dist/layer2/ai-rag-safety.d.ts +21 -0
- package/dist/layer2/ai-rag-safety.d.ts.map +1 -0
- package/dist/layer2/ai-rag-safety.js +459 -0
- package/dist/layer2/ai-rag-safety.js.map +1 -0
- package/dist/layer2/ai-schema-validation.d.ts +25 -0
- package/dist/layer2/ai-schema-validation.d.ts.map +1 -0
- package/dist/layer2/ai-schema-validation.js +375 -0
- package/dist/layer2/ai-schema-validation.js.map +1 -0
- package/dist/layer2/auth-antipatterns.d.ts +20 -0
- package/dist/layer2/auth-antipatterns.d.ts.map +1 -0
- package/dist/layer2/auth-antipatterns.js +333 -0
- package/dist/layer2/auth-antipatterns.js.map +1 -0
- package/dist/layer2/byok-patterns.d.ts +12 -0
- package/dist/layer2/byok-patterns.d.ts.map +1 -0
- package/dist/layer2/byok-patterns.js +299 -0
- package/dist/layer2/byok-patterns.js.map +1 -0
- package/dist/layer2/dangerous-functions.d.ts +7 -0
- package/dist/layer2/dangerous-functions.d.ts.map +1 -0
- package/dist/layer2/dangerous-functions.js +1375 -0
- package/dist/layer2/dangerous-functions.js.map +1 -0
- package/dist/layer2/data-exposure.d.ts +16 -0
- package/dist/layer2/data-exposure.d.ts.map +1 -0
- package/dist/layer2/data-exposure.js +279 -0
- package/dist/layer2/data-exposure.js.map +1 -0
- package/dist/layer2/framework-checks.d.ts +7 -0
- package/dist/layer2/framework-checks.d.ts.map +1 -0
- package/dist/layer2/framework-checks.js +388 -0
- package/dist/layer2/framework-checks.js.map +1 -0
- package/dist/layer2/index.d.ts +58 -0
- package/dist/layer2/index.d.ts.map +1 -0
- package/dist/layer2/index.js +380 -0
- package/dist/layer2/index.js.map +1 -0
- package/dist/layer2/logic-gates.d.ts +7 -0
- package/dist/layer2/logic-gates.d.ts.map +1 -0
- package/dist/layer2/logic-gates.js +182 -0
- package/dist/layer2/logic-gates.js.map +1 -0
- package/dist/layer2/risky-imports.d.ts +7 -0
- package/dist/layer2/risky-imports.d.ts.map +1 -0
- package/dist/layer2/risky-imports.js +161 -0
- package/dist/layer2/risky-imports.js.map +1 -0
- package/dist/layer2/variables.d.ts +8 -0
- package/dist/layer2/variables.d.ts.map +1 -0
- package/dist/layer2/variables.js +152 -0
- package/dist/layer2/variables.js.map +1 -0
- package/dist/layer3/anthropic.d.ts +83 -0
- package/dist/layer3/anthropic.d.ts.map +1 -0
- package/dist/layer3/anthropic.js +1745 -0
- package/dist/layer3/anthropic.js.map +1 -0
- package/dist/layer3/index.d.ts +24 -0
- package/dist/layer3/index.d.ts.map +1 -0
- package/dist/layer3/index.js +119 -0
- package/dist/layer3/index.js.map +1 -0
- package/dist/layer3/openai.d.ts +25 -0
- package/dist/layer3/openai.d.ts.map +1 -0
- package/dist/layer3/openai.js +238 -0
- package/dist/layer3/openai.js.map +1 -0
- package/dist/layer3/package-check.d.ts +63 -0
- package/dist/layer3/package-check.d.ts.map +1 -0
- package/dist/layer3/package-check.js +508 -0
- package/dist/layer3/package-check.js.map +1 -0
- package/dist/modes/incremental.d.ts +66 -0
- package/dist/modes/incremental.d.ts.map +1 -0
- package/dist/modes/incremental.js +200 -0
- package/dist/modes/incremental.js.map +1 -0
- package/dist/tiers.d.ts +125 -0
- package/dist/tiers.d.ts.map +1 -0
- package/dist/tiers.js +234 -0
- package/dist/tiers.js.map +1 -0
- package/dist/types.d.ts +175 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +50 -0
- package/dist/types.js.map +1 -0
- package/dist/utils/auth-helper-detector.d.ts +56 -0
- package/dist/utils/auth-helper-detector.d.ts.map +1 -0
- package/dist/utils/auth-helper-detector.js +360 -0
- package/dist/utils/auth-helper-detector.js.map +1 -0
- package/dist/utils/context-helpers.d.ts +96 -0
- package/dist/utils/context-helpers.d.ts.map +1 -0
- package/dist/utils/context-helpers.js +493 -0
- package/dist/utils/context-helpers.js.map +1 -0
- package/dist/utils/diff-detector.d.ts +53 -0
- package/dist/utils/diff-detector.d.ts.map +1 -0
- package/dist/utils/diff-detector.js +104 -0
- package/dist/utils/diff-detector.js.map +1 -0
- package/dist/utils/diff-parser.d.ts +80 -0
- package/dist/utils/diff-parser.d.ts.map +1 -0
- package/dist/utils/diff-parser.js +202 -0
- package/dist/utils/diff-parser.js.map +1 -0
- package/dist/utils/imported-auth-detector.d.ts +37 -0
- package/dist/utils/imported-auth-detector.d.ts.map +1 -0
- package/dist/utils/imported-auth-detector.js +251 -0
- package/dist/utils/imported-auth-detector.js.map +1 -0
- package/dist/utils/middleware-detector.d.ts +55 -0
- package/dist/utils/middleware-detector.d.ts.map +1 -0
- package/dist/utils/middleware-detector.js +260 -0
- package/dist/utils/middleware-detector.js.map +1 -0
- package/dist/utils/oauth-flow-detector.d.ts +41 -0
- package/dist/utils/oauth-flow-detector.d.ts.map +1 -0
- package/dist/utils/oauth-flow-detector.js +202 -0
- package/dist/utils/oauth-flow-detector.js.map +1 -0
- package/dist/utils/path-exclusions.d.ts +55 -0
- package/dist/utils/path-exclusions.d.ts.map +1 -0
- package/dist/utils/path-exclusions.js +222 -0
- package/dist/utils/path-exclusions.js.map +1 -0
- package/dist/utils/project-context-builder.d.ts +119 -0
- package/dist/utils/project-context-builder.d.ts.map +1 -0
- package/dist/utils/project-context-builder.js +534 -0
- package/dist/utils/project-context-builder.js.map +1 -0
- package/dist/utils/registry-clients.d.ts +93 -0
- package/dist/utils/registry-clients.d.ts.map +1 -0
- package/dist/utils/registry-clients.js +273 -0
- package/dist/utils/registry-clients.js.map +1 -0
- package/dist/utils/trpc-analyzer.d.ts +78 -0
- package/dist/utils/trpc-analyzer.d.ts.map +1 -0
- package/dist/utils/trpc-analyzer.js +297 -0
- package/dist/utils/trpc-analyzer.js.map +1 -0
- package/package.json +45 -0
- package/src/__tests__/benchmark/fixtures/false-positives.ts +227 -0
- package/src/__tests__/benchmark/fixtures/index.ts +68 -0
- package/src/__tests__/benchmark/fixtures/layer1/config-audit.ts +364 -0
- package/src/__tests__/benchmark/fixtures/layer1/hardcoded-secrets.ts +173 -0
- package/src/__tests__/benchmark/fixtures/layer1/high-entropy.ts +234 -0
- package/src/__tests__/benchmark/fixtures/layer1/index.ts +31 -0
- package/src/__tests__/benchmark/fixtures/layer1/sensitive-urls.ts +90 -0
- package/src/__tests__/benchmark/fixtures/layer1/weak-crypto.ts +197 -0
- package/src/__tests__/benchmark/fixtures/layer2/ai-agent-tools.ts +170 -0
- package/src/__tests__/benchmark/fixtures/layer2/ai-endpoint-protection.ts +418 -0
- package/src/__tests__/benchmark/fixtures/layer2/ai-execution-sinks.ts +189 -0
- package/src/__tests__/benchmark/fixtures/layer2/ai-fingerprinting.ts +316 -0
- package/src/__tests__/benchmark/fixtures/layer2/ai-prompt-hygiene.ts +178 -0
- package/src/__tests__/benchmark/fixtures/layer2/ai-rag-safety.ts +184 -0
- package/src/__tests__/benchmark/fixtures/layer2/ai-schema-validation.ts +434 -0
- package/src/__tests__/benchmark/fixtures/layer2/auth-antipatterns.ts +159 -0
- package/src/__tests__/benchmark/fixtures/layer2/byok-patterns.ts +112 -0
- package/src/__tests__/benchmark/fixtures/layer2/dangerous-functions.ts +246 -0
- package/src/__tests__/benchmark/fixtures/layer2/data-exposure.ts +168 -0
- package/src/__tests__/benchmark/fixtures/layer2/framework-checks.ts +346 -0
- package/src/__tests__/benchmark/fixtures/layer2/index.ts +67 -0
- package/src/__tests__/benchmark/fixtures/layer2/injection-vulnerabilities.ts +239 -0
- package/src/__tests__/benchmark/fixtures/layer2/logic-gates.ts +246 -0
- package/src/__tests__/benchmark/fixtures/layer2/risky-imports.ts +231 -0
- package/src/__tests__/benchmark/fixtures/layer2/variables.ts +167 -0
- package/src/__tests__/benchmark/index.ts +29 -0
- package/src/__tests__/benchmark/run-benchmark.ts +144 -0
- package/src/__tests__/benchmark/run-depth-validation.ts +206 -0
- package/src/__tests__/benchmark/run-real-world-test.ts +243 -0
- package/src/__tests__/benchmark/security-benchmark-script.ts +1737 -0
- package/src/__tests__/benchmark/tier-integration-script.ts +177 -0
- package/src/__tests__/benchmark/types.ts +144 -0
- package/src/__tests__/benchmark/utils/test-runner.ts +475 -0
- package/src/__tests__/regression/known-false-positives.test.ts +467 -0
- package/src/__tests__/snapshots/__snapshots__/scan-depth.test.ts.snap +178 -0
- package/src/__tests__/snapshots/scan-depth.test.ts +258 -0
- package/src/__tests__/validation/analyze-results.ts +542 -0
- package/src/__tests__/validation/extract-for-triage.ts +146 -0
- package/src/__tests__/validation/fp-deep-analysis.ts +327 -0
- package/src/__tests__/validation/run-validation.ts +364 -0
- package/src/__tests__/validation/triage-template.md +132 -0
- package/src/formatters/cli-terminal.ts +446 -0
- package/src/formatters/github-comment.ts +382 -0
- package/src/formatters/grouping.ts +190 -0
- package/src/formatters/index.ts +47 -0
- package/src/formatters/vscode-diagnostic.ts +243 -0
- package/src/index.ts +823 -0
- package/src/layer1/comments.ts +218 -0
- package/src/layer1/config-audit.ts +289 -0
- package/src/layer1/entropy.ts +583 -0
- package/src/layer1/file-flags.ts +127 -0
- package/src/layer1/index.ts +181 -0
- package/src/layer1/patterns.ts +516 -0
- package/src/layer1/urls.ts +334 -0
- package/src/layer1/weak-crypto.ts +328 -0
- package/src/layer2/ai-agent-tools.ts +601 -0
- package/src/layer2/ai-endpoint-protection.ts +387 -0
- package/src/layer2/ai-execution-sinks.ts +580 -0
- package/src/layer2/ai-fingerprinting.ts +758 -0
- package/src/layer2/ai-prompt-hygiene.ts +411 -0
- package/src/layer2/ai-rag-safety.ts +511 -0
- package/src/layer2/ai-schema-validation.ts +421 -0
- package/src/layer2/auth-antipatterns.ts +394 -0
- package/src/layer2/byok-patterns.ts +336 -0
- package/src/layer2/dangerous-functions.ts +1563 -0
- package/src/layer2/data-exposure.ts +315 -0
- package/src/layer2/framework-checks.ts +433 -0
- package/src/layer2/index.ts +473 -0
- package/src/layer2/logic-gates.ts +206 -0
- package/src/layer2/risky-imports.ts +186 -0
- package/src/layer2/variables.ts +166 -0
- package/src/layer3/anthropic.ts +2030 -0
- package/src/layer3/index.ts +130 -0
- package/src/layer3/package-check.ts +604 -0
- package/src/modes/incremental.ts +293 -0
- package/src/tiers.ts +318 -0
- package/src/types.ts +284 -0
- package/src/utils/auth-helper-detector.ts +443 -0
- package/src/utils/context-helpers.ts +535 -0
- package/src/utils/diff-detector.ts +135 -0
- package/src/utils/diff-parser.ts +272 -0
- package/src/utils/imported-auth-detector.ts +320 -0
- package/src/utils/middleware-detector.ts +333 -0
- package/src/utils/oauth-flow-detector.ts +246 -0
- package/src/utils/path-exclusions.ts +266 -0
- package/src/utils/project-context-builder.ts +707 -0
- package/src/utils/registry-clients.ts +351 -0
- package/src/utils/trpc-analyzer.ts +382 -0
|
@@ -0,0 +1,1375 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Layer 2: Dangerous Function Call Analysis
|
|
4
|
+
* Detects usage of dangerous functions that can lead to security vulnerabilities
|
|
5
|
+
*/
|
|
6
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
7
|
+
exports.detectDangerousFunctions = detectDangerousFunctions;
|
|
8
|
+
const context_helpers_1 = require("../utils/context-helpers");
|
|
9
|
+
/**
|
|
10
|
+
* Check if exec() call is from child_process (dangerous) vs RegExp.exec (safe)
|
|
11
|
+
* Returns true if this is a child_process exec call that should be flagged
|
|
12
|
+
*/
|
|
13
|
+
function isChildProcessExec(content, lineContent) {
|
|
14
|
+
// Check for child_process import
|
|
15
|
+
const hasChildProcessImport = /require\s*\(\s*['"]child_process['"]\s*\)/.test(content) ||
|
|
16
|
+
/from\s+['"]child_process['"]/.test(content) ||
|
|
17
|
+
/import\s+.*child_process/.test(content) ||
|
|
18
|
+
/require\s*\(\s*['"]node:child_process['"]\s*\)/.test(content) ||
|
|
19
|
+
/from\s+['"]node:child_process['"]/.test(content);
|
|
20
|
+
// If no child_process import, this is likely RegExp.exec or similar
|
|
21
|
+
if (!hasChildProcessImport) {
|
|
22
|
+
return false;
|
|
23
|
+
}
|
|
24
|
+
// Check if this specific line is RegExp.exec pattern
|
|
25
|
+
// RegExp.exec is called as: regex.exec(string) or /pattern/.exec(string)
|
|
26
|
+
const isRegExpExec = /\.\s*exec\s*\(/.test(lineContent) && // Method call on an object
|
|
27
|
+
!/\bexec\s*\(/.test(lineContent.replace(/\.\s*exec\s*\(/, '')); // Not a standalone exec()
|
|
28
|
+
// Also check for common RegExp patterns
|
|
29
|
+
const isRegExpPattern = /\/[^/]+\/[gimsuy]*\.exec\s*\(/.test(lineContent) || // /pattern/.exec()
|
|
30
|
+
/new\s+RegExp\s*\([^)]+\)\.exec\s*\(/.test(lineContent) || // new RegExp().exec()
|
|
31
|
+
/regex\.exec\s*\(/i.test(lineContent) || // regex.exec()
|
|
32
|
+
/pattern\.exec\s*\(/i.test(lineContent) || // pattern.exec()
|
|
33
|
+
/match\.exec\s*\(/i.test(lineContent) || // match.exec()
|
|
34
|
+
/re\.exec\s*\(/i.test(lineContent); // re.exec()
|
|
35
|
+
if (isRegExpExec || isRegExpPattern) {
|
|
36
|
+
return false;
|
|
37
|
+
}
|
|
38
|
+
// Check if exec is imported/destructured from child_process
|
|
39
|
+
const execImported = /\{\s*[^}]*\bexec\b[^}]*\}\s*=\s*require\s*\(\s*['"]child_process['"]/.test(content) ||
|
|
40
|
+
/\{\s*[^}]*\bexec\b[^}]*\}\s*=\s*require\s*\(\s*['"]node:child_process['"]/.test(content) ||
|
|
41
|
+
/import\s+\{\s*[^}]*\bexec\b[^}]*\}\s+from\s+['"]child_process['"]/.test(content) ||
|
|
42
|
+
/import\s+\{\s*[^}]*\bexec\b[^}]*\}\s+from\s+['"]node:child_process['"]/.test(content);
|
|
43
|
+
// If exec is directly imported from child_process, standalone exec() is dangerous
|
|
44
|
+
if (execImported && /\bexec\s*\(/.test(lineContent)) {
|
|
45
|
+
return true;
|
|
46
|
+
}
|
|
47
|
+
// Check for child_process.exec() pattern
|
|
48
|
+
if (/child_process\.exec\s*\(/.test(lineContent) ||
|
|
49
|
+
/cp\.exec\s*\(/.test(lineContent) ||
|
|
50
|
+
/childProcess\.exec\s*\(/.test(lineContent)) {
|
|
51
|
+
return true;
|
|
52
|
+
}
|
|
53
|
+
// If we have child_process import but can't determine usage, be conservative
|
|
54
|
+
// Only flag if it looks like a standalone exec() call
|
|
55
|
+
return /\bexec\s*\(/.test(lineContent) && !/\.\s*exec\s*\(/.test(lineContent);
|
|
56
|
+
}
|
|
57
|
+
/**
|
|
58
|
+
* Check if schema validation is applied near a JSON.parse call
|
|
59
|
+
* Looks for zod, yup, joi, or similar validation patterns
|
|
60
|
+
*/
|
|
61
|
+
function hasSchemaValidationNearby(content, lineNumber) {
|
|
62
|
+
const lines = content.split('\n');
|
|
63
|
+
const start = Math.max(0, lineNumber - 5);
|
|
64
|
+
const end = Math.min(lines.length, lineNumber + 10);
|
|
65
|
+
const context = lines.slice(start, end).join('\n');
|
|
66
|
+
const schemaValidationPatterns = [
|
|
67
|
+
// Zod patterns
|
|
68
|
+
/z\.(object|string|number|array|boolean)\s*\(/i,
|
|
69
|
+
/\.parse\s*\(/i,
|
|
70
|
+
/\.safeParse\s*\(/i,
|
|
71
|
+
/schema\.parse/i,
|
|
72
|
+
/Schema\.parse/i,
|
|
73
|
+
// Yup patterns
|
|
74
|
+
/yup\.(object|string|number|array|boolean)\s*\(/i,
|
|
75
|
+
/\.validate\s*\(/i,
|
|
76
|
+
/\.validateSync\s*\(/i,
|
|
77
|
+
// Joi patterns
|
|
78
|
+
/Joi\.(object|string|number|array|boolean)\s*\(/i,
|
|
79
|
+
/\.validateAsync\s*\(/i,
|
|
80
|
+
// Valibot patterns
|
|
81
|
+
/v\.(object|string|number|array|boolean)\s*\(/i,
|
|
82
|
+
// AJV patterns
|
|
83
|
+
/ajv\.compile/i,
|
|
84
|
+
/validate\s*\(\s*schema/i,
|
|
85
|
+
// TypeBox patterns
|
|
86
|
+
/Type\.(Object|String|Number|Array|Boolean)\s*\(/i,
|
|
87
|
+
// Generic validation patterns
|
|
88
|
+
/validateSchema/i,
|
|
89
|
+
/schemaValidator/i,
|
|
90
|
+
/parseAndValidate/i,
|
|
91
|
+
];
|
|
92
|
+
return schemaValidationPatterns.some(p => p.test(context));
|
|
93
|
+
}
|
|
94
|
+
/**
|
|
95
|
+
* Check if path traversal protection is in place
|
|
96
|
+
* Looks for common sanitization patterns that prevent directory traversal attacks
|
|
97
|
+
*/
|
|
98
|
+
function hasPathTraversalProtection(context, lineContent) {
|
|
99
|
+
const protectionPatterns = [
|
|
100
|
+
// Path normalization with base directory check
|
|
101
|
+
/path\.resolve\s*\([^)]+\).*\.startsWith\s*\(/i,
|
|
102
|
+
/\.startsWith\s*\([^)]*(?:baseDir|basePath|rootDir|uploadDir|allowedDir)/i,
|
|
103
|
+
// Explicit ".." rejection
|
|
104
|
+
/\.includes\s*\(\s*['"`]\.\.['"`]\s*\)/i,
|
|
105
|
+
/\.indexOf\s*\(\s*['"`]\.\.['"`]\s*\)/i,
|
|
106
|
+
/['"`]\.\.['"`].*(?:throw|reject|return|error)/i,
|
|
107
|
+
// Path sanitization libraries
|
|
108
|
+
/sanitizePath|sanitizeFilename|sanitize-filename/i,
|
|
109
|
+
/path-sanitizer|secure-path/i,
|
|
110
|
+
// Explicit path validation
|
|
111
|
+
/validatePath|isValidPath|checkPath|verifyPath/i,
|
|
112
|
+
/isPathAllowed|isAllowedPath|pathIsAllowed/i,
|
|
113
|
+
// Normalize and check pattern
|
|
114
|
+
/path\.normalize\s*\([^)]+\).*(?:startsWith|includes|indexOf)/i,
|
|
115
|
+
// Regex validation for safe characters only
|
|
116
|
+
/\/\^?\[a-zA-Z0-9_\-\.\\\/\]\+\$?\//, // Only alphanumeric, dash, underscore, dot
|
|
117
|
+
// Allowlist/whitelist patterns
|
|
118
|
+
/allowedExtensions|allowedTypes|whitelist/i,
|
|
119
|
+
/\.endsWith\s*\(\s*['"`]\.\w+['"`]\s*\)/i, // Extension check
|
|
120
|
+
// Path.basename to strip directory
|
|
121
|
+
/path\.basename\s*\(/i,
|
|
122
|
+
// Zod/validation for filename patterns
|
|
123
|
+
/z\.string\s*\(\s*\)\.regex\s*\(/i,
|
|
124
|
+
];
|
|
125
|
+
return protectionPatterns.some(p => p.test(context) || p.test(lineContent));
|
|
126
|
+
}
|
|
127
|
+
/**
|
|
128
|
+
* Check if spawn/execFile/execSync is from child_process
|
|
129
|
+
*/
|
|
130
|
+
function isChildProcessSpawn(content, lineContent) {
|
|
131
|
+
// Check for child_process import
|
|
132
|
+
const hasChildProcessImport = /require\s*\(\s*['"]child_process['"]\s*\)/.test(content) ||
|
|
133
|
+
/from\s+['"]child_process['"]/.test(content) ||
|
|
134
|
+
/require\s*\(\s*['"]node:child_process['"]\s*\)/.test(content) ||
|
|
135
|
+
/from\s+['"]node:child_process['"]/.test(content);
|
|
136
|
+
if (!hasChildProcessImport) {
|
|
137
|
+
return false;
|
|
138
|
+
}
|
|
139
|
+
// These functions are always from child_process when that module is imported
|
|
140
|
+
return /\b(spawn|spawnSync|execSync|execFile|execFileSync)\s*\(/.test(lineContent);
|
|
141
|
+
}
|
|
142
|
+
/**
|
|
143
|
+
* Check if a line is inside a try-catch block
|
|
144
|
+
* Looks for enclosing try { ... } catch pattern
|
|
145
|
+
*/
|
|
146
|
+
function isInsideTryCatch(content, lineNumber) {
|
|
147
|
+
const lines = content.split('\n');
|
|
148
|
+
// Track brace depth and whether we're in a try block
|
|
149
|
+
let tryDepth = 0;
|
|
150
|
+
let inTryBlock = false;
|
|
151
|
+
let braceStack = [];
|
|
152
|
+
// Scan from start to the target line
|
|
153
|
+
for (let i = 0; i < lineNumber && i < lines.length; i++) {
|
|
154
|
+
const line = lines[i];
|
|
155
|
+
// Check for try keyword (not in a comment)
|
|
156
|
+
if (/\btry\s*\{/.test(line) && !(0, context_helpers_1.isComment)(line)) {
|
|
157
|
+
inTryBlock = true;
|
|
158
|
+
tryDepth++;
|
|
159
|
+
// Count opening braces on this line
|
|
160
|
+
const openBraces = (line.match(/\{/g) || []).length;
|
|
161
|
+
const closeBraces = (line.match(/\}/g) || []).length;
|
|
162
|
+
for (let j = 0; j < openBraces - closeBraces; j++) {
|
|
163
|
+
braceStack.push('try');
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
else if (/\bcatch\s*\(/.test(line) && !(0, context_helpers_1.isComment)(line)) {
|
|
167
|
+
// Entering catch block - still protected
|
|
168
|
+
// Don't decrement tryDepth yet
|
|
169
|
+
}
|
|
170
|
+
else if (/\bfinally\s*\{/.test(line) && !(0, context_helpers_1.isComment)(line)) {
|
|
171
|
+
// Entering finally block - still protected
|
|
172
|
+
}
|
|
173
|
+
else {
|
|
174
|
+
// Track regular braces
|
|
175
|
+
const openBraces = (line.match(/\{/g) || []).length;
|
|
176
|
+
const closeBraces = (line.match(/\}/g) || []).length;
|
|
177
|
+
for (let j = 0; j < openBraces; j++) {
|
|
178
|
+
braceStack.push(inTryBlock && tryDepth > 0 ? 'try' : 'other');
|
|
179
|
+
}
|
|
180
|
+
for (let j = 0; j < closeBraces; j++) {
|
|
181
|
+
const popped = braceStack.pop();
|
|
182
|
+
if (popped === 'try') {
|
|
183
|
+
tryDepth--;
|
|
184
|
+
if (tryDepth === 0) {
|
|
185
|
+
inTryBlock = false;
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
return tryDepth > 0;
|
|
192
|
+
}
|
|
193
|
+
/**
|
|
194
|
+
* Simpler heuristic: check if there's a try-catch in the same function scope
|
|
195
|
+
* Looks for try { before the line and } catch after, within reasonable bounds
|
|
196
|
+
*/
|
|
197
|
+
function hasTryCatchNearby(content, lineNumber, windowSize = 20) {
|
|
198
|
+
const lines = content.split('\n');
|
|
199
|
+
const startLine = Math.max(0, lineNumber - windowSize);
|
|
200
|
+
const endLine = Math.min(lines.length, lineNumber + windowSize);
|
|
201
|
+
// Look backward for 'try {'
|
|
202
|
+
let foundTry = false;
|
|
203
|
+
for (let i = lineNumber - 1; i >= startLine; i--) {
|
|
204
|
+
const line = lines[i];
|
|
205
|
+
if (/\btry\s*\{/.test(line) && !(0, context_helpers_1.isComment)(line)) {
|
|
206
|
+
foundTry = true;
|
|
207
|
+
break;
|
|
208
|
+
}
|
|
209
|
+
// Stop if we hit a function boundary
|
|
210
|
+
if (/\b(function|async function|=>|class)\b/.test(line) && /\{/.test(line)) {
|
|
211
|
+
break;
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
if (!foundTry)
|
|
215
|
+
return false;
|
|
216
|
+
// Look forward for '} catch'
|
|
217
|
+
for (let i = lineNumber; i < endLine; i++) {
|
|
218
|
+
const line = lines[i];
|
|
219
|
+
if (/\}\s*catch\s*\(/.test(line) && !(0, context_helpers_1.isComment)(line)) {
|
|
220
|
+
return true;
|
|
221
|
+
}
|
|
222
|
+
// Stop if we hit another function boundary
|
|
223
|
+
if (i > lineNumber && /\b(function|async function|class)\b/.test(line) && /\{/.test(line)) {
|
|
224
|
+
break;
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
return false;
|
|
228
|
+
}
|
|
229
|
+
/**
|
|
230
|
+
* Check if file path indicates a low-risk context for JSON.parse
|
|
231
|
+
*/
|
|
232
|
+
function isLowRiskJSONParseFile(filePath) {
|
|
233
|
+
// Test/mock files - skip or info only
|
|
234
|
+
if ((0, context_helpers_1.isTestOrMockFile)(filePath)) {
|
|
235
|
+
return 'test_fixture';
|
|
236
|
+
}
|
|
237
|
+
// Settings/preferences components - internal UI state
|
|
238
|
+
if (/\/(components|pages)\/(settings|preferences|config)/i.test(filePath)) {
|
|
239
|
+
return 'ui_state';
|
|
240
|
+
}
|
|
241
|
+
// Provider/context files - typically storing state in localStorage
|
|
242
|
+
if (/Provider\.(ts|tsx|js|jsx)$/i.test(filePath)) {
|
|
243
|
+
return 'ui_state';
|
|
244
|
+
}
|
|
245
|
+
// Modal/Dialog components - typically internal state
|
|
246
|
+
if (/(Modal|Dialog|Settings|Preferences)\.(ts|tsx|js|jsx)$/i.test(filePath)) {
|
|
247
|
+
return 'ui_state';
|
|
248
|
+
}
|
|
249
|
+
// __mocks__ directory
|
|
250
|
+
if (/__mocks__/i.test(filePath)) {
|
|
251
|
+
return 'test_fixture';
|
|
252
|
+
}
|
|
253
|
+
// fixtures directory
|
|
254
|
+
if (/\/(fixtures?|stubs?|mocks?)\//i.test(filePath)) {
|
|
255
|
+
return 'test_fixture';
|
|
256
|
+
}
|
|
257
|
+
// scripts/tools directories (internal tooling)
|
|
258
|
+
if (/\/(scripts?|tools?|cli)\//i.test(filePath)) {
|
|
259
|
+
return 'internal';
|
|
260
|
+
}
|
|
261
|
+
// Migration files
|
|
262
|
+
if (/migration/i.test(filePath)) {
|
|
263
|
+
return 'migration';
|
|
264
|
+
}
|
|
265
|
+
// Config files
|
|
266
|
+
if (/\/(config|settings|constants)\.(ts|js)/i.test(filePath)) {
|
|
267
|
+
return 'config';
|
|
268
|
+
}
|
|
269
|
+
return null;
|
|
270
|
+
}
|
|
271
|
+
/**
|
|
272
|
+
* Check if JSON.parse is parsing a trusted SDK response
|
|
273
|
+
* These are well-defined responses from known APIs and are safe to parse
|
|
274
|
+
*/
|
|
275
|
+
function isTrustedSDKResponse(lineContent, content) {
|
|
276
|
+
const trustedPatterns = [
|
|
277
|
+
// OpenAI SDK responses
|
|
278
|
+
/JSON\.parse\s*\(\s*(?:response|completion|result|message)\.(?:content|text|data)/i,
|
|
279
|
+
/JSON\.parse\s*\(\s*(?:openai|anthropic|client)\./i,
|
|
280
|
+
// Fetch response.json() result (already parsed by fetch)
|
|
281
|
+
/JSON\.parse\s*\(\s*await\s+.*\.json\s*\(\s*\)\s*\)/i,
|
|
282
|
+
// SDK method results
|
|
283
|
+
/JSON\.parse\s*\(\s*(?:result|response)\.(?:choices|content|data|body)\[/i,
|
|
284
|
+
// AI SDK streaming results
|
|
285
|
+
/JSON\.parse\s*\(\s*(?:chunk|delta|part)\.(?:content|text)/i,
|
|
286
|
+
];
|
|
287
|
+
if (trustedPatterns.some(p => p.test(lineContent))) {
|
|
288
|
+
return true;
|
|
289
|
+
}
|
|
290
|
+
// Check surrounding context for SDK usage
|
|
291
|
+
const sdkContextPatterns = [
|
|
292
|
+
/openai\..*\.create/i,
|
|
293
|
+
/anthropic\..*\.create/i,
|
|
294
|
+
/\.chat\.completions/i,
|
|
295
|
+
/\.messages\.create/i,
|
|
296
|
+
];
|
|
297
|
+
return sdkContextPatterns.some(p => p.test(content));
|
|
298
|
+
}
|
|
299
|
+
function classifyJSONParseSource(lineContent, filePath) {
|
|
300
|
+
// First check file path for low-risk contexts
|
|
301
|
+
const fileBasedSource = isLowRiskJSONParseFile(filePath);
|
|
302
|
+
if (fileBasedSource) {
|
|
303
|
+
return fileBasedSource;
|
|
304
|
+
}
|
|
305
|
+
// User input - potentially dangerous
|
|
306
|
+
const userInputPatterns = [
|
|
307
|
+
/JSON\.parse\s*\(\s*(req|request)\.(body|query|params)/i,
|
|
308
|
+
/JSON\.parse\s*\(\s*event\.(body|queryStringParameters)/i, // AWS Lambda
|
|
309
|
+
/JSON\.parse\s*\(\s*ctx\.(request|body|query)/i, // Koa
|
|
310
|
+
/JSON\.parse\s*\(\s*(input|userInput|rawInput|payload)/i,
|
|
311
|
+
/JSON\.parse\s*\(\s*body\b/i, // Generic 'body' often means request body
|
|
312
|
+
];
|
|
313
|
+
if (userInputPatterns.some(p => p.test(lineContent))) {
|
|
314
|
+
return 'user_input';
|
|
315
|
+
}
|
|
316
|
+
// localStorage/sessionStorage - client-side storage
|
|
317
|
+
const storagePatterns = [
|
|
318
|
+
/JSON\.parse\s*\(\s*localStorage\.getItem/i,
|
|
319
|
+
/JSON\.parse\s*\(\s*sessionStorage\.getItem/i,
|
|
320
|
+
/JSON\.parse\s*\(\s*window\.localStorage/i,
|
|
321
|
+
/JSON\.parse\s*\(\s*storage\.get/i,
|
|
322
|
+
/JSON\.parse\s*\(\s*saved\b/i, // Common pattern: const saved = localStorage.getItem(...); JSON.parse(saved)
|
|
323
|
+
/JSON\.parse\s*\(\s*stored\b/i,
|
|
324
|
+
];
|
|
325
|
+
if (storagePatterns.some(p => p.test(lineContent))) {
|
|
326
|
+
return 'local_storage';
|
|
327
|
+
}
|
|
328
|
+
// Database results - internal data
|
|
329
|
+
const databasePatterns = [
|
|
330
|
+
/JSON\.parse\s*\(\s*(row|result|record|doc|document)\./i,
|
|
331
|
+
/JSON\.parse\s*\(\s*\w+\.(data|json|metadata|embedding)\)/i,
|
|
332
|
+
/JSON\.parse\s*\(\s*\w+\[['"]?\w+['"]?\]\.(data|json|embedding)/i,
|
|
333
|
+
/JSON\.parse\s*\(\s*item\.\w+\)/i, // ORM iteration: items.map(item => JSON.parse(item.field))
|
|
334
|
+
/JSON\.parse\s*\(\s*\w+\.content\)/i, // Parsing content field from DB
|
|
335
|
+
];
|
|
336
|
+
if (databasePatterns.some(p => p.test(lineContent))) {
|
|
337
|
+
return 'database';
|
|
338
|
+
}
|
|
339
|
+
// Editor state, internal caches, UI state
|
|
340
|
+
const internalPatterns = [
|
|
341
|
+
/JSON\.parse\s*\(\s*(state|cache|stored|saved|cached)/i,
|
|
342
|
+
/JSON\.parse\s*\(\s*this\.(state|cache|data)/i,
|
|
343
|
+
/JSON\.parse\s*\(\s*\w+State\)/i,
|
|
344
|
+
/JSON\.parse\s*\(\s*editorState/i,
|
|
345
|
+
/JSON\.parse\s*\(\s*parsed\b/i, // JSON.parse(parsed) - likely already validated
|
|
346
|
+
/JSON\.parse\s*\(\s*settings\b/i, // Settings data
|
|
347
|
+
/JSON\.parse\s*\(\s*preferences\b/i,
|
|
348
|
+
];
|
|
349
|
+
if (internalPatterns.some(p => p.test(lineContent))) {
|
|
350
|
+
return 'internal';
|
|
351
|
+
}
|
|
352
|
+
// Node content in editor apps (e.g., noda-os nodes have JSON content)
|
|
353
|
+
if (/JSON\.parse\s*\(\s*(node|note|document|entry)\.(content|body|data)\)/i.test(lineContent)) {
|
|
354
|
+
return 'database';
|
|
355
|
+
}
|
|
356
|
+
return 'unknown';
|
|
357
|
+
}
|
|
358
|
+
const DANGEROUS_FUNCTIONS = [
|
|
359
|
+
// Code execution
|
|
360
|
+
{
|
|
361
|
+
name: 'eval() usage',
|
|
362
|
+
pattern: /\beval\s*\(/gi,
|
|
363
|
+
severity: 'critical',
|
|
364
|
+
description: 'eval() executes arbitrary code and is a major security risk',
|
|
365
|
+
suggestedFix: 'Use JSON.parse() for JSON data, or refactor to avoid dynamic code execution',
|
|
366
|
+
},
|
|
367
|
+
{
|
|
368
|
+
name: 'Function constructor',
|
|
369
|
+
pattern: /new\s+Function\s*\(/gi,
|
|
370
|
+
severity: 'critical',
|
|
371
|
+
description: 'Function constructor can execute arbitrary code like eval()',
|
|
372
|
+
suggestedFix: 'Refactor to use static functions or safe alternatives',
|
|
373
|
+
},
|
|
374
|
+
{
|
|
375
|
+
name: 'setTimeout/setInterval with string',
|
|
376
|
+
pattern: /set(Timeout|Interval)\s*\(\s*['"`]/gi,
|
|
377
|
+
severity: 'high',
|
|
378
|
+
description: 'setTimeout/setInterval with string argument acts like eval()',
|
|
379
|
+
suggestedFix: 'Pass a function reference instead of a string',
|
|
380
|
+
},
|
|
381
|
+
// Command injection
|
|
382
|
+
{
|
|
383
|
+
name: 'child_process exec',
|
|
384
|
+
pattern: /\b(exec|execSync|spawn|spawnSync|execFile)\s*\(/gi,
|
|
385
|
+
severity: 'high',
|
|
386
|
+
description: 'Shell command execution can lead to command injection',
|
|
387
|
+
suggestedFix: 'Validate and sanitize all inputs, prefer execFile over exec',
|
|
388
|
+
},
|
|
389
|
+
{
|
|
390
|
+
name: 'os.system/subprocess (Python)',
|
|
391
|
+
pattern: /\b(os\.system|subprocess\.(call|run|Popen|check_output))\s*\(/gi,
|
|
392
|
+
severity: 'high',
|
|
393
|
+
description: 'Shell command execution can lead to command injection',
|
|
394
|
+
suggestedFix: 'Use subprocess with shell=False and pass arguments as a list',
|
|
395
|
+
languages: ['py'],
|
|
396
|
+
},
|
|
397
|
+
// SQL injection risks
|
|
398
|
+
{
|
|
399
|
+
name: 'Raw SQL query construction',
|
|
400
|
+
pattern: /\.(query|execute|raw)\s*\(\s*[`'"].*\$\{|\.query\s*\(\s*['"].*\+/gi,
|
|
401
|
+
severity: 'critical',
|
|
402
|
+
description: 'String concatenation in SQL queries can lead to SQL injection',
|
|
403
|
+
suggestedFix: 'Use parameterized queries or prepared statements',
|
|
404
|
+
},
|
|
405
|
+
{
|
|
406
|
+
name: 'SQL template literal',
|
|
407
|
+
pattern: /`SELECT.*FROM.*WHERE.*\$\{|`INSERT.*INTO.*VALUES.*\$\{|`UPDATE.*SET.*\$\{|`DELETE.*FROM.*WHERE.*\$\{/gi,
|
|
408
|
+
severity: 'critical',
|
|
409
|
+
description: 'Template literals in SQL queries can lead to SQL injection',
|
|
410
|
+
suggestedFix: 'Use parameterized queries with placeholders (?, $1, etc.)',
|
|
411
|
+
},
|
|
412
|
+
// XSS risks
|
|
413
|
+
{
|
|
414
|
+
name: 'innerHTML assignment',
|
|
415
|
+
pattern: /\.innerHTML\s*=|\.outerHTML\s*=/gi,
|
|
416
|
+
severity: 'high',
|
|
417
|
+
description: 'Direct innerHTML assignment can lead to XSS vulnerabilities',
|
|
418
|
+
suggestedFix: 'Use textContent for text, or sanitize HTML with DOMPurify',
|
|
419
|
+
},
|
|
420
|
+
{
|
|
421
|
+
name: 'document.write',
|
|
422
|
+
pattern: /document\.write\s*\(/gi,
|
|
423
|
+
severity: 'high',
|
|
424
|
+
description: 'document.write can introduce XSS vulnerabilities',
|
|
425
|
+
suggestedFix: 'Use DOM manipulation methods instead',
|
|
426
|
+
},
|
|
427
|
+
{
|
|
428
|
+
name: 'dangerouslySetInnerHTML',
|
|
429
|
+
pattern: /dangerouslySetInnerHTML\s*=\s*\{\s*\{\s*__html\s*:/gi,
|
|
430
|
+
severity: 'high',
|
|
431
|
+
description: 'dangerouslySetInnerHTML can lead to XSS if content is not sanitized',
|
|
432
|
+
suggestedFix: 'Sanitize HTML content with DOMPurify before rendering',
|
|
433
|
+
},
|
|
434
|
+
// Deserialization
|
|
435
|
+
{
|
|
436
|
+
name: 'Unsafe deserialization',
|
|
437
|
+
pattern: /\b(pickle\.loads?|yaml\.load\s*\((?!.*Loader)|unserialize|Marshal\.load)\s*\(/gi,
|
|
438
|
+
severity: 'critical',
|
|
439
|
+
description: 'Unsafe deserialization can lead to remote code execution',
|
|
440
|
+
suggestedFix: 'Use safe loaders (yaml.safe_load) or validate input before deserializing',
|
|
441
|
+
},
|
|
442
|
+
// Note: JSON.parse is handled specially with source-aware severity - see below
|
|
443
|
+
// Note: request.json() is NOT a dangerous function - see schema validation rules
|
|
444
|
+
// File system risks
|
|
445
|
+
{
|
|
446
|
+
name: 'Dynamic file path',
|
|
447
|
+
pattern: /\b(readFile|writeFile|readFileSync|writeFileSync|createReadStream|createWriteStream)\s*\(\s*[^'"]/gi,
|
|
448
|
+
severity: 'medium',
|
|
449
|
+
description: 'Dynamic file paths can lead to path traversal attacks',
|
|
450
|
+
suggestedFix: 'Validate and sanitize file paths, use path.resolve with a base directory',
|
|
451
|
+
},
|
|
452
|
+
{
|
|
453
|
+
name: 'Path traversal risk',
|
|
454
|
+
pattern: /path\.(join|resolve)\s*\([^)]*req\.(params|query|body)/gi,
|
|
455
|
+
severity: 'high',
|
|
456
|
+
description: 'User input in file paths can lead to path traversal attacks',
|
|
457
|
+
suggestedFix: 'Validate paths and ensure they stay within allowed directories',
|
|
458
|
+
},
|
|
459
|
+
// Crypto weaknesses
|
|
460
|
+
{
|
|
461
|
+
name: 'Math.random for security',
|
|
462
|
+
pattern: /Math\.random\s*\(\s*\)/gi,
|
|
463
|
+
severity: 'medium',
|
|
464
|
+
description: 'Math.random() is not cryptographically secure',
|
|
465
|
+
suggestedFix: 'Use crypto.randomBytes() or crypto.getRandomValues() for security-sensitive operations',
|
|
466
|
+
},
|
|
467
|
+
// Regex DoS
|
|
468
|
+
{
|
|
469
|
+
name: 'Potentially unsafe regex',
|
|
470
|
+
pattern: /new\s+RegExp\s*\(\s*[^'"]/gi,
|
|
471
|
+
severity: 'medium',
|
|
472
|
+
description: 'Dynamic regex construction can lead to ReDoS attacks',
|
|
473
|
+
suggestedFix: 'Validate regex patterns and consider using safe-regex library',
|
|
474
|
+
},
|
|
475
|
+
// Prototype pollution
|
|
476
|
+
{
|
|
477
|
+
name: 'Object.assign with user input',
|
|
478
|
+
pattern: /Object\.assign\s*\(\s*\{\s*\}\s*,\s*(req\.|request\.|body|params|query)/gi,
|
|
479
|
+
severity: 'high',
|
|
480
|
+
description: 'Object.assign with user input can lead to prototype pollution',
|
|
481
|
+
suggestedFix: 'Validate and sanitize input, or use a safe merge function',
|
|
482
|
+
},
|
|
483
|
+
{
|
|
484
|
+
name: 'Spread operator with user input',
|
|
485
|
+
pattern: /\{\s*\.\.\.req\.(body|params|query)|\.\.\.request\.(body|params|query)/gi,
|
|
486
|
+
severity: 'medium',
|
|
487
|
+
description: 'Spreading user input can lead to mass assignment vulnerabilities',
|
|
488
|
+
suggestedFix: 'Explicitly pick allowed properties instead of spreading all input',
|
|
489
|
+
},
|
|
490
|
+
];
|
|
491
|
+
// Check if file matches language filter
|
|
492
|
+
function matchesLanguage(filePath, languages) {
|
|
493
|
+
if (!languages || languages.length === 0)
|
|
494
|
+
return true;
|
|
495
|
+
const ext = filePath.split('.').pop()?.toLowerCase() || '';
|
|
496
|
+
return languages.some(lang => {
|
|
497
|
+
if (lang === 'py')
|
|
498
|
+
return ext === 'py';
|
|
499
|
+
if (lang === 'js')
|
|
500
|
+
return ['js', 'jsx', 'mjs', 'cjs'].includes(ext);
|
|
501
|
+
if (lang === 'ts')
|
|
502
|
+
return ['ts', 'tsx'].includes(ext);
|
|
503
|
+
return ext === lang;
|
|
504
|
+
});
|
|
505
|
+
}
|
|
506
|
+
// Check if innerHTML/dangerouslySetInnerHTML uses static content only
|
|
507
|
+
function isStaticHTMLContent(lineContent, content, lineNumber) {
|
|
508
|
+
const lines = content.split('\n');
|
|
509
|
+
// Get surrounding context (5 lines before and after)
|
|
510
|
+
const contextStart = Math.max(0, lineNumber - 6);
|
|
511
|
+
const contextEnd = Math.min(lines.length, lineNumber + 5);
|
|
512
|
+
const context = lines.slice(contextStart, contextEnd).join('\n');
|
|
513
|
+
// Static HTML indicators - string literals only
|
|
514
|
+
const staticIndicators = [
|
|
515
|
+
/innerHTML\s*=\s*['"`][^'"`]*['"`]/, // innerHTML = "static string"
|
|
516
|
+
/innerHTML\s*=\s*`[^$]*`/, // innerHTML = `static template without ${}`
|
|
517
|
+
/dangerouslySetInnerHTML\s*=\s*\{\s*\{\s*__html:\s*['"`]/, // React static string
|
|
518
|
+
];
|
|
519
|
+
// Dynamic content indicators (red flags)
|
|
520
|
+
const dynamicIndicators = [
|
|
521
|
+
/\$\{[^}]+\}/, // Template interpolation ${...}
|
|
522
|
+
/innerHTML\s*=.*\+/, // String concatenation with +
|
|
523
|
+
/innerHTML\s*\+=\s*/, // Append operation
|
|
524
|
+
/\breq\.|\.params|\.query|\.body/, // User input (req.params, req.query, req.body)
|
|
525
|
+
/\bprops\./, // Component props
|
|
526
|
+
/\bstate\./, // Component state
|
|
527
|
+
/\.value\b/, // Input value
|
|
528
|
+
/dangerouslySetInnerHTML\s*=\s*\{\s*\{\s*__html:\s*[^'"`]/, // React dynamic
|
|
529
|
+
];
|
|
530
|
+
const isStatic = staticIndicators.some(p => p.test(lineContent));
|
|
531
|
+
const isDynamic = dynamicIndicators.some(p => p.test(context));
|
|
532
|
+
return isStatic && !isDynamic;
|
|
533
|
+
}
|
|
534
|
+
/**
|
|
535
|
+
* Check if eval/exec/Function has only static literal inputs (no user data)
|
|
536
|
+
* Static inputs like eval('({ mode: "production" })') are low risk
|
|
537
|
+
*/
|
|
538
|
+
function hasOnlyStaticInputs(lineContent, content, lineNumber) {
|
|
539
|
+
const lines = content.split('\n');
|
|
540
|
+
// Check if the argument to eval/exec/Function is a string literal only
|
|
541
|
+
const staticPatterns = [
|
|
542
|
+
/eval\s*\(\s*['"`][^'"`$]*['"`]\s*\)/, // eval('static string')
|
|
543
|
+
/eval\s*\(\s*`[^$`]*`\s*\)/, // eval(`static template without ${}`)
|
|
544
|
+
/new\s+Function\s*\(\s*['"`][^'"`$]*['"`]\s*\)/, // new Function('static')
|
|
545
|
+
/execSync\s*\(\s*['"`][^'"`$]*['"`]\s*\)/, // execSync('static command')
|
|
546
|
+
/exec\s*\(\s*['"`][^'"`$]*['"`]/, // exec('static command'
|
|
547
|
+
];
|
|
548
|
+
if (staticPatterns.some(p => p.test(lineContent))) {
|
|
549
|
+
return true;
|
|
550
|
+
}
|
|
551
|
+
// Check surrounding context for user input flowing in
|
|
552
|
+
const userInputIndicators = [
|
|
553
|
+
/\$\{/, // Template interpolation
|
|
554
|
+
/\+\s*\w+/, // String concatenation with variable
|
|
555
|
+
/req\.|request\.|body\.|params\.|query\./i, // Request data
|
|
556
|
+
/user[Ii]nput|userCode|userCommand/, // User input variables
|
|
557
|
+
/args\[|argv\[/, // Command line args
|
|
558
|
+
];
|
|
559
|
+
const contextStart = Math.max(0, lineNumber - 3);
|
|
560
|
+
const contextEnd = Math.min(lines.length, lineNumber + 1);
|
|
561
|
+
const context = lines.slice(contextStart, contextEnd).join('\n');
|
|
562
|
+
// If no user input indicators found, likely static
|
|
563
|
+
return !userInputIndicators.some(p => p.test(context));
|
|
564
|
+
}
|
|
565
|
+
/**
|
|
566
|
+
* Check if SQL query uses whitelist validation pattern
|
|
567
|
+
* e.g., columns validated against allowedColumns array before use
|
|
568
|
+
*/
|
|
569
|
+
function hasSQLWhitelistValidation(content, lineNumber) {
|
|
570
|
+
const lines = content.split('\n');
|
|
571
|
+
const contextStart = Math.max(0, lineNumber - 15);
|
|
572
|
+
const contextEnd = Math.min(lines.length, lineNumber + 5);
|
|
573
|
+
const context = lines.slice(contextStart, contextEnd).join('\n');
|
|
574
|
+
// Whitelist/allowlist validation patterns
|
|
575
|
+
const whitelistPatterns = [
|
|
576
|
+
/allowed\w*\s*=\s*\[/i, // allowedColumns = [...]
|
|
577
|
+
/whitelist\w*\s*=\s*\[/i, // whitelistFields = [...]
|
|
578
|
+
/valid\w*\s*=\s*\[/i, // validColumns = [...]
|
|
579
|
+
/\.filter\s*\([^)]*\.includes\s*\(/i, // .filter(c => allowed.includes(c))
|
|
580
|
+
/\.includes\s*\([^)]*\)/i, // allowedColumns.includes(col)
|
|
581
|
+
/\.every\s*\([^)]*\.includes/i, // columns.every(c => allowed.includes(c))
|
|
582
|
+
/if\s*\(\s*!.*\.includes/i, // if (!allowed.includes(...))
|
|
583
|
+
];
|
|
584
|
+
return whitelistPatterns.some(p => p.test(context));
|
|
585
|
+
}
|
|
586
|
+
/**
|
|
587
|
+
* Check if dangerouslySetInnerHTML is used with DOMPurify sanitization
|
|
588
|
+
*/
|
|
589
|
+
function hasDOMPurifySanitization(lineContent, content, lineNumber) {
|
|
590
|
+
const lines = content.split('\n');
|
|
591
|
+
const contextStart = Math.max(0, lineNumber - 10);
|
|
592
|
+
const contextEnd = Math.min(lines.length, lineNumber + 5);
|
|
593
|
+
const context = lines.slice(contextStart, contextEnd).join('\n');
|
|
594
|
+
// DOMPurify sanitization patterns
|
|
595
|
+
const sanitizationPatterns = [
|
|
596
|
+
/DOMPurify\.sanitize/i,
|
|
597
|
+
/sanitize\s*\(/i,
|
|
598
|
+
/purify\s*\(/i,
|
|
599
|
+
/xss\s*\(/i,
|
|
600
|
+
/clean\s*\(/i,
|
|
601
|
+
/sanitizeHtml/i,
|
|
602
|
+
/escapeHtml/i,
|
|
603
|
+
/sanitized/i,
|
|
604
|
+
/purified/i,
|
|
605
|
+
];
|
|
606
|
+
return sanitizationPatterns.some(p => p.test(context));
|
|
607
|
+
}
|
|
608
|
+
/**
|
|
609
|
+
* Check if data flows to an LLM prompt rather than a DOM sink
|
|
610
|
+
* LLM prompts are NOT XSS - they're prompt injection (different risk profile)
|
|
611
|
+
*/
|
|
612
|
+
function isLLMPromptContext(lineContent, content, filePath) {
|
|
613
|
+
// File path indicators of AI/LLM code
|
|
614
|
+
const aiFilePatterns = [
|
|
615
|
+
/\/(ai|llm|chat|openai|anthropic|gpt|claude)\//i,
|
|
616
|
+
/\/(assistants?|agents?|prompts?)\//i,
|
|
617
|
+
/(chat|ai|llm|prompt|assistant).*\.(ts|js|tsx|jsx)$/i,
|
|
618
|
+
];
|
|
619
|
+
if (aiFilePatterns.some(p => p.test(filePath))) {
|
|
620
|
+
return true;
|
|
621
|
+
}
|
|
622
|
+
// Content patterns suggesting LLM API usage
|
|
623
|
+
const llmApiPatterns = [
|
|
624
|
+
/\.create\s*\(\s*\{[^}]*messages\s*:/i, // OpenAI/Anthropic SDK
|
|
625
|
+
/openai|anthropic|claude|gpt-4|gpt-3/i, // AI service mentions
|
|
626
|
+
/\bprompt\s*[=:+]/i, // prompt assignment
|
|
627
|
+
/\bsystemPrompt|userPrompt|assistantPrompt/i, // Prompt variables
|
|
628
|
+
/completion|chat\.create|messages\.create/i, // API calls
|
|
629
|
+
/\bmessages\s*:\s*\[/i, // Messages array
|
|
630
|
+
/role:\s*['"`](user|assistant|system)['"`]/i, // Message roles
|
|
631
|
+
];
|
|
632
|
+
// Check the line and surrounding context
|
|
633
|
+
const lines = content.split('\n');
|
|
634
|
+
const lineIndex = lines.findIndex(l => l === lineContent || l.includes(lineContent.trim()));
|
|
635
|
+
const startLine = Math.max(0, lineIndex - 10);
|
|
636
|
+
const endLine = Math.min(lines.length, lineIndex + 10);
|
|
637
|
+
const context = lines.slice(startLine, endLine).join('\n');
|
|
638
|
+
return llmApiPatterns.some(p => p.test(lineContent) || p.test(context));
|
|
639
|
+
}
|
|
640
|
+
/**
|
|
641
|
+
* Check if this is a static bootstrap script (e.g., localStorage theme reader)
|
|
642
|
+
* These are very low risk even with dangerouslySetInnerHTML
|
|
643
|
+
*/
|
|
644
|
+
function isStaticBootstrapScript(_lineContent, content, lineNumber) {
|
|
645
|
+
const lines = content.split('\n');
|
|
646
|
+
const contextStart = Math.max(0, lineNumber - 10);
|
|
647
|
+
const contextEnd = Math.min(lines.length, lineNumber + 5);
|
|
648
|
+
const context = lines.slice(contextStart, contextEnd).join('\n');
|
|
649
|
+
// Bootstrap script indicators (reading from localStorage, setting attributes)
|
|
650
|
+
const bootstrapPatterns = [
|
|
651
|
+
/localStorage\.getItem/i,
|
|
652
|
+
/document\.documentElement\.setAttribute/i,
|
|
653
|
+
/data-(theme|font|mode)/i,
|
|
654
|
+
/classList\.(add|remove|toggle)/i,
|
|
655
|
+
/\.dataset\./i,
|
|
656
|
+
];
|
|
657
|
+
// Dangerous patterns that disqualify as safe bootstrap
|
|
658
|
+
const dangerousPatterns = [
|
|
659
|
+
/\$\{.*\}/, // Template interpolation
|
|
660
|
+
/\+\s*[a-zA-Z]/, // String concatenation with variable
|
|
661
|
+
/innerHTML\s*=\s*[a-zA-Z]/, // innerHTML set to variable directly
|
|
662
|
+
/fetch\s*\(/, // Network requests
|
|
663
|
+
/\.(query|params|body)/, // User input
|
|
664
|
+
/location\.(search|hash)/, // URL parameters
|
|
665
|
+
/document\.cookie/, // Cookie access
|
|
666
|
+
];
|
|
667
|
+
const hasBootstrapPatterns = bootstrapPatterns.some(p => p.test(context));
|
|
668
|
+
const hasDangerousPatterns = dangerousPatterns.some(p => p.test(context));
|
|
669
|
+
return hasBootstrapPatterns && !hasDangerousPatterns;
|
|
670
|
+
}
|
|
671
|
+
/**
|
|
672
|
+
* Check if Math.random() is used for cosmetic/UI purposes (not security)
|
|
673
|
+
* Cosmetic uses: CSS values, animations, UI variations, demo data
|
|
674
|
+
* Security uses: tokens, IDs, cryptographic operations, session management
|
|
675
|
+
*/
|
|
676
|
+
function isCosmeticMathRandom(lineContent, content, lineNumber) {
|
|
677
|
+
const lines = content.split('\n');
|
|
678
|
+
// Check the line itself for cosmetic indicators
|
|
679
|
+
const cosmeticLinePatterns = [
|
|
680
|
+
// CSS/style values
|
|
681
|
+
/['"`]\s*\$\{.*Math\.random.*\}\s*%['"`]/, // `${Math.random() * 40 + 50}%`
|
|
682
|
+
/Math\.random.*\s*\+\s*['"`]%['"`]/, // Math.random() * 40 + '%'
|
|
683
|
+
/Math\.random.*\)\s*\*\s*\d+\s*\+\s*\d+\s*\}\s*%/, // }) * 40 + 50}%
|
|
684
|
+
/return\s+`.*Math\.random.*%`/, // return `${...}%`
|
|
685
|
+
/width:\s*['"`].*Math\.random/i, // width: `${Math.random()...}%`
|
|
686
|
+
/height:\s*['"`].*Math\.random/i, // height: `${Math.random()...}%`
|
|
687
|
+
/opacity:\s*['"`]?.*Math\.random/i, // opacity: Math.random()
|
|
688
|
+
/transform:\s*['"`]?.*Math\.random/i, // transform: translate(...)
|
|
689
|
+
/rotate\(.*Math\.random/i, // rotate(Math.random() * 360)
|
|
690
|
+
/translate\(.*Math\.random/i, // translate(Math.random() * 100)
|
|
691
|
+
/scale\(.*Math\.random/i, // scale(Math.random() * 2)
|
|
692
|
+
// Color/animation values
|
|
693
|
+
/rgba?\(.*Math\.random/i, // rgb(Math.random() * 255, ...)
|
|
694
|
+
/hsl\(.*Math\.random/i, // hsl(Math.random() * 360, ...)
|
|
695
|
+
/Math\.random.*\*\s*360/, // Math.random() * 360 (degrees/hue)
|
|
696
|
+
/Math\.random.*\*\s*255/, // Math.random() * 255 (RGB values)
|
|
697
|
+
// Array/list randomization for UI
|
|
698
|
+
/Math\.floor\(Math\.random.*\.length\)/, // Math.floor(Math.random() * array.length)
|
|
699
|
+
/\[Math\.floor\(Math\.random/, // array[Math.floor(Math.random()...)]
|
|
700
|
+
// Demo/placeholder data
|
|
701
|
+
/Math\.random.*\*\s*\d+\s*\+\s*\d+.*\bpx\b/i, // Math.random() * 100 + 50 + 'px'
|
|
702
|
+
/Math\.random.*\*\s*\d+\s*\+\s*\d+.*\bms\b/i, // Math.random() * 1000 + 500 + 'ms'
|
|
703
|
+
/Math\.random.*\*\s*\d+\s*\+\s*\d+.*\bs\b/i, // Math.random() * 5 + 2 + 's'
|
|
704
|
+
// UI identifier generation (short strings for element IDs, keys, etc.)
|
|
705
|
+
/Math\.random\(\)\.toString\(36\)\.substring\(/, // .toString(36).substring(2, 9) - short UI IDs
|
|
706
|
+
/Math\.random\(\)\.toString\(36\)\.substr\(/, // .substr() variant
|
|
707
|
+
/Math\.random\(\)\.toString\(36\)\.slice\(/, // .slice() variant
|
|
708
|
+
/Math\.random\(\)\.toString\(16\)\.substring\(/, // .toString(16).substring() - hex UI IDs
|
|
709
|
+
/Math\.random\(\)\.toString\(16\)\.slice\(/, // hex slice variant
|
|
710
|
+
];
|
|
711
|
+
if (cosmeticLinePatterns.some(p => p.test(lineContent))) {
|
|
712
|
+
return true;
|
|
713
|
+
}
|
|
714
|
+
// Check surrounding context (5 lines before and after)
|
|
715
|
+
const contextStart = Math.max(0, lineNumber - 5);
|
|
716
|
+
const contextEnd = Math.min(lines.length, lineNumber + 5);
|
|
717
|
+
const context = lines.slice(contextStart, contextEnd).join('\n');
|
|
718
|
+
// Context indicators of cosmetic use
|
|
719
|
+
const cosmeticContextPatterns = [
|
|
720
|
+
// UI component files
|
|
721
|
+
/\/(components?|ui|widgets?|animations?|contexts?)\//i,
|
|
722
|
+
// Style-related variables/functions
|
|
723
|
+
/\b(style|styles|css|className|animation|transition)/i,
|
|
724
|
+
/\b(width|height|opacity|color|transform|rotate|scale|translate)/i,
|
|
725
|
+
// Demo/example data
|
|
726
|
+
/\b(demo|example|placeholder|mock|fake|sample|test)Data/i,
|
|
727
|
+
/\b(random|shuffle|pick|choose).*\b(color|item|element|option)/i,
|
|
728
|
+
// Animation/timing
|
|
729
|
+
/setTimeout.*Math\.random/i,
|
|
730
|
+
/setInterval.*Math\.random/i,
|
|
731
|
+
/delay.*Math\.random/i,
|
|
732
|
+
/duration.*Math\.random/i,
|
|
733
|
+
// UI state variations
|
|
734
|
+
/\b(variant|theme|layout|position).*Math\.random/i,
|
|
735
|
+
// UI identifier variable names (toast, notification, element, component IDs)
|
|
736
|
+
/\b(toast|notification|element|component|widget|modal|dialog|popup).*id\b/i,
|
|
737
|
+
/\bid\s*=.*Math\.random/i,
|
|
738
|
+
/\bkey\s*=.*Math\.random/i, // React keys
|
|
739
|
+
/\btempId|temporaryId|uniqueId\b/i,
|
|
740
|
+
];
|
|
741
|
+
if (cosmeticContextPatterns.some(p => p.test(context))) {
|
|
742
|
+
return true;
|
|
743
|
+
}
|
|
744
|
+
// Security-sensitive patterns that override cosmetic detection
|
|
745
|
+
const securityPatterns = [
|
|
746
|
+
/\b(token|secret|key|password|credential|signature)/i,
|
|
747
|
+
/\b(auth|crypto|encrypt|decrypt|hash)/i,
|
|
748
|
+
/\b(session|nonce|salt)\b/i,
|
|
749
|
+
/Math\.random.*\*\s*1e\d+/, // Math.random() * 1e16 (large numbers for IDs)
|
|
750
|
+
];
|
|
751
|
+
if (securityPatterns.some(p => p.test(lineContent) || p.test(context))) {
|
|
752
|
+
return false; // Not cosmetic - this is security-sensitive
|
|
753
|
+
}
|
|
754
|
+
// Check for .toString(36) WITHOUT substring/slice/substr (security token pattern)
|
|
755
|
+
// If it has substring/slice/substr, it's already caught by cosmeticLinePatterns above
|
|
756
|
+
const hasToString36WithoutTruncation = /Math\.random\(\)\.toString\(36\)/.test(lineContent) &&
|
|
757
|
+
!/\.(substring|substr|slice)\(/.test(lineContent);
|
|
758
|
+
const hasToString16WithoutTruncation = /Math\.random\(\)\.toString\(16\)/.test(lineContent) &&
|
|
759
|
+
!/\.(substring|substr|slice)\(/.test(lineContent);
|
|
760
|
+
if (hasToString36WithoutTruncation || hasToString16WithoutTruncation) {
|
|
761
|
+
return false; // Full-length toString() without truncation - likely security token
|
|
762
|
+
}
|
|
763
|
+
return false; // Default to flagging if unclear
|
|
764
|
+
}
|
|
765
|
+
function detectDangerousFunctions(content, filePath) {
|
|
766
|
+
const vulnerabilities = [];
|
|
767
|
+
// Skip scanner/fixture files to avoid self-detection
|
|
768
|
+
if ((0, context_helpers_1.isScannerOrFixtureFile)(filePath)) {
|
|
769
|
+
return vulnerabilities;
|
|
770
|
+
}
|
|
771
|
+
const lines = content.split('\n');
|
|
772
|
+
const isTestFile = (0, context_helpers_1.isTestOrMockFile)(filePath);
|
|
773
|
+
lines.forEach((line, index) => {
|
|
774
|
+
// Skip comment lines
|
|
775
|
+
if ((0, context_helpers_1.isComment)(line))
|
|
776
|
+
return;
|
|
777
|
+
for (const funcPattern of DANGEROUS_FUNCTIONS) {
|
|
778
|
+
// Check language filter
|
|
779
|
+
if (!matchesLanguage(filePath, funcPattern.languages))
|
|
780
|
+
continue;
|
|
781
|
+
const regex = new RegExp(funcPattern.pattern.source, funcPattern.pattern.flags);
|
|
782
|
+
if (regex.test(line)) {
|
|
783
|
+
// Special handling for innerHTML patterns
|
|
784
|
+
if (funcPattern.name === 'innerHTML assignment' ||
|
|
785
|
+
funcPattern.name === 'dangerouslySetInnerHTML') {
|
|
786
|
+
// Check if this uses static content only
|
|
787
|
+
if (isStaticHTMLContent(line, content, index)) {
|
|
788
|
+
vulnerabilities.push({
|
|
789
|
+
id: `dangerous-func-${filePath}-${index + 1}-${funcPattern.name}`,
|
|
790
|
+
filePath,
|
|
791
|
+
lineNumber: index + 1,
|
|
792
|
+
lineContent: line.trim(),
|
|
793
|
+
severity: 'info',
|
|
794
|
+
category: 'dangerous_function',
|
|
795
|
+
title: funcPattern.name + ' (static content)',
|
|
796
|
+
description: 'Static HTML assignment detected. Generally safe for hardcoded content, but consider using textContent for plain text or proper DOM methods for dynamic content.',
|
|
797
|
+
suggestedFix: 'If this is plain text, use textContent instead. If HTML must be used, ensure it is static and does not come from user input.',
|
|
798
|
+
confidence: 'low',
|
|
799
|
+
layer: 2,
|
|
800
|
+
});
|
|
801
|
+
break; // Only report once per line
|
|
802
|
+
}
|
|
803
|
+
// Check if DOMPurify or similar sanitization is used
|
|
804
|
+
if (hasDOMPurifySanitization(line, content, index)) {
|
|
805
|
+
vulnerabilities.push({
|
|
806
|
+
id: `dangerous-func-${filePath}-${index + 1}-${funcPattern.name}`,
|
|
807
|
+
filePath,
|
|
808
|
+
lineNumber: index + 1,
|
|
809
|
+
lineContent: line.trim(),
|
|
810
|
+
severity: 'info',
|
|
811
|
+
category: 'dangerous_function',
|
|
812
|
+
title: funcPattern.name + ' (sanitized)',
|
|
813
|
+
description: 'HTML is sanitized before rendering (DOMPurify or similar detected). This is the recommended pattern for rendering user-generated HTML.',
|
|
814
|
+
suggestedFix: 'Ensure DOMPurify is configured correctly and kept up to date.',
|
|
815
|
+
confidence: 'low',
|
|
816
|
+
layer: 2,
|
|
817
|
+
});
|
|
818
|
+
break; // Only report once per line
|
|
819
|
+
}
|
|
820
|
+
// Check if this is a static bootstrap script (e.g., theme/font loader)
|
|
821
|
+
if (isStaticBootstrapScript(line, content, index)) {
|
|
822
|
+
vulnerabilities.push({
|
|
823
|
+
id: `dangerous-func-${filePath}-${index + 1}-${funcPattern.name}`,
|
|
824
|
+
filePath,
|
|
825
|
+
lineNumber: index + 1,
|
|
826
|
+
lineContent: line.trim(),
|
|
827
|
+
severity: 'info',
|
|
828
|
+
category: 'dangerous_function',
|
|
829
|
+
title: funcPattern.name + ' (static bootstrap script)',
|
|
830
|
+
description: 'This appears to be a static bootstrap script (e.g., reading localStorage for theme/font preferences). Low risk as no untrusted data is interpolated into the HTML/JS.',
|
|
831
|
+
suggestedFix: 'Verify no user-controlled data is interpolated into the script content.',
|
|
832
|
+
confidence: 'low',
|
|
833
|
+
layer: 2,
|
|
834
|
+
});
|
|
835
|
+
break; // Only report once per line
|
|
836
|
+
}
|
|
837
|
+
// Check if this is in LLM prompt context (not XSS - it's prompt injection)
|
|
838
|
+
if (isLLMPromptContext(line, content, filePath)) {
|
|
839
|
+
vulnerabilities.push({
|
|
840
|
+
id: `dangerous-func-${filePath}-${index + 1}-prompt-injection`,
|
|
841
|
+
filePath,
|
|
842
|
+
lineNumber: index + 1,
|
|
843
|
+
lineContent: line.trim(),
|
|
844
|
+
severity: 'info',
|
|
845
|
+
category: 'ai_pattern',
|
|
846
|
+
title: 'Potential prompt injection risk',
|
|
847
|
+
description: 'User content is being used in an LLM prompt context. This is NOT XSS (the content goes to an AI, not a DOM). However, untrusted content in prompts may lead to prompt injection attacks.',
|
|
848
|
+
suggestedFix: 'Consider input validation, content filtering, or structured prompts to limit prompt injection risk.',
|
|
849
|
+
confidence: 'low',
|
|
850
|
+
layer: 2,
|
|
851
|
+
});
|
|
852
|
+
break; // Only report once per line
|
|
853
|
+
}
|
|
854
|
+
// Dynamic content - full severity, needs AI validation
|
|
855
|
+
let severity = funcPattern.severity;
|
|
856
|
+
if (isTestFile) {
|
|
857
|
+
severity = 'low';
|
|
858
|
+
}
|
|
859
|
+
vulnerabilities.push({
|
|
860
|
+
id: `dangerous-func-${filePath}-${index + 1}-${funcPattern.name}`,
|
|
861
|
+
filePath,
|
|
862
|
+
lineNumber: index + 1,
|
|
863
|
+
lineContent: line.trim(),
|
|
864
|
+
severity,
|
|
865
|
+
category: 'dangerous_function',
|
|
866
|
+
title: funcPattern.name,
|
|
867
|
+
description: funcPattern.description + ' This appears to use dynamic content which increases XSS risk.' + (isTestFile ? ' (in test file)' : ''),
|
|
868
|
+
suggestedFix: funcPattern.suggestedFix,
|
|
869
|
+
confidence: isTestFile ? 'low' : 'high',
|
|
870
|
+
layer: 2,
|
|
871
|
+
requiresAIValidation: true, // Dynamic HTML needs validation
|
|
872
|
+
});
|
|
873
|
+
break; // Only report once per line
|
|
874
|
+
}
|
|
875
|
+
// Note: JSON.parse is now handled by standalone detectJSONParseSafe() function
|
|
876
|
+
// which provides better source-aware severity classification
|
|
877
|
+
// Special handling for eval and Function constructor
|
|
878
|
+
if (funcPattern.name === 'eval() usage' || funcPattern.name === 'Function constructor') {
|
|
879
|
+
// Suppress entirely in test files - test files legitimately test eval behavior
|
|
880
|
+
if (isTestFile) {
|
|
881
|
+
break; // Skip reporting entirely
|
|
882
|
+
}
|
|
883
|
+
// Check if eval is inside a test assertion (expect(), test(), it(), describe())
|
|
884
|
+
const testAssertionPattern = /\b(expect|test|it|describe)\s*\(/;
|
|
885
|
+
if (testAssertionPattern.test(line)) {
|
|
886
|
+
break; // Skip reporting - this is testing eval behavior
|
|
887
|
+
}
|
|
888
|
+
// Check if inputs are static literals (low risk)
|
|
889
|
+
if (hasOnlyStaticInputs(line, content, index)) {
|
|
890
|
+
vulnerabilities.push({
|
|
891
|
+
id: `dangerous-func-${filePath}-${index + 1}-${funcPattern.name}`,
|
|
892
|
+
filePath,
|
|
893
|
+
lineNumber: index + 1,
|
|
894
|
+
lineContent: line.trim(),
|
|
895
|
+
severity: 'info',
|
|
896
|
+
category: 'dangerous_function',
|
|
897
|
+
title: funcPattern.name + ' (static input)',
|
|
898
|
+
description: 'eval/Function with static string literal input. Lower risk than dynamic input, but consider refactoring to avoid eval entirely.',
|
|
899
|
+
suggestedFix: 'Consider using JSON.parse() for JSON data or refactoring to avoid eval.',
|
|
900
|
+
confidence: 'low',
|
|
901
|
+
layer: 2,
|
|
902
|
+
});
|
|
903
|
+
break;
|
|
904
|
+
}
|
|
905
|
+
vulnerabilities.push({
|
|
906
|
+
id: `dangerous-func-${filePath}-${index + 1}-${funcPattern.name}`,
|
|
907
|
+
filePath,
|
|
908
|
+
lineNumber: index + 1,
|
|
909
|
+
lineContent: line.trim(),
|
|
910
|
+
severity: funcPattern.severity,
|
|
911
|
+
category: 'dangerous_function',
|
|
912
|
+
title: funcPattern.name,
|
|
913
|
+
description: funcPattern.description,
|
|
914
|
+
suggestedFix: funcPattern.suggestedFix,
|
|
915
|
+
confidence: 'high',
|
|
916
|
+
layer: 2,
|
|
917
|
+
requiresAIValidation: true, // Code execution patterns need validation
|
|
918
|
+
});
|
|
919
|
+
break;
|
|
920
|
+
}
|
|
921
|
+
// Special handling for child_process exec - verify it's not RegExp.exec
|
|
922
|
+
if (funcPattern.name === 'child_process exec') {
|
|
923
|
+
// First check if this is actually from child_process (not RegExp.exec)
|
|
924
|
+
const isExecMatch = /\bexec\s*\(/.test(line);
|
|
925
|
+
const isOtherMatch = /\b(execSync|spawn|spawnSync|execFile)\s*\(/.test(line);
|
|
926
|
+
if (isExecMatch && !isOtherMatch) {
|
|
927
|
+
// This matched 'exec(' - verify it's from child_process
|
|
928
|
+
if (!isChildProcessExec(content, line)) {
|
|
929
|
+
// This is RegExp.exec or similar - skip
|
|
930
|
+
break;
|
|
931
|
+
}
|
|
932
|
+
}
|
|
933
|
+
else if (isOtherMatch) {
|
|
934
|
+
// This matched spawn/execSync/etc - verify child_process import
|
|
935
|
+
if (!isChildProcessSpawn(content, line)) {
|
|
936
|
+
// No child_process import - skip
|
|
937
|
+
break;
|
|
938
|
+
}
|
|
939
|
+
}
|
|
940
|
+
if (hasOnlyStaticInputs(line, content, index)) {
|
|
941
|
+
vulnerabilities.push({
|
|
942
|
+
id: `dangerous-func-${filePath}-${index + 1}-${funcPattern.name}`,
|
|
943
|
+
filePath,
|
|
944
|
+
lineNumber: index + 1,
|
|
945
|
+
lineContent: line.trim(),
|
|
946
|
+
severity: 'info',
|
|
947
|
+
category: 'dangerous_function',
|
|
948
|
+
title: funcPattern.name + ' (static command)',
|
|
949
|
+
description: 'exec/execSync with hardcoded command. Lower risk than dynamic commands, but ensure command does not change based on user input.',
|
|
950
|
+
suggestedFix: 'If command is truly static, this is generally acceptable. For dynamic commands, validate and sanitize inputs.',
|
|
951
|
+
confidence: 'low',
|
|
952
|
+
layer: 2,
|
|
953
|
+
});
|
|
954
|
+
break;
|
|
955
|
+
}
|
|
956
|
+
}
|
|
957
|
+
// Special handling for SQL patterns - check for whitelist validation
|
|
958
|
+
if (funcPattern.name === 'Raw SQL query construction' || funcPattern.name === 'SQL template literal') {
|
|
959
|
+
if (hasSQLWhitelistValidation(content, index)) {
|
|
960
|
+
vulnerabilities.push({
|
|
961
|
+
id: `dangerous-func-${filePath}-${index + 1}-${funcPattern.name}`,
|
|
962
|
+
filePath,
|
|
963
|
+
lineNumber: index + 1,
|
|
964
|
+
lineContent: line.trim(),
|
|
965
|
+
severity: 'info',
|
|
966
|
+
category: 'dangerous_function',
|
|
967
|
+
title: funcPattern.name + ' (whitelist validated)',
|
|
968
|
+
description: 'SQL query with dynamic content, but whitelist/allowlist validation detected. This is a safer pattern that limits injection risk.',
|
|
969
|
+
suggestedFix: 'Ensure the whitelist is comprehensive and cannot be bypassed. Consider using parameterized queries for additional safety.',
|
|
970
|
+
confidence: 'low',
|
|
971
|
+
layer: 2,
|
|
972
|
+
});
|
|
973
|
+
break;
|
|
974
|
+
}
|
|
975
|
+
}
|
|
976
|
+
// Special handling for Dynamic file path - skip for utility files
|
|
977
|
+
// Utility functions designed to work with file paths are expected to take path parameters
|
|
978
|
+
if (funcPattern.name === 'Dynamic file path') {
|
|
979
|
+
// Skip for utility/lib/helper files - these are internal functions, not API handlers
|
|
980
|
+
const isUtilityFile = /\/(utils?|lib|helpers?|services?|modules?)\//i.test(filePath);
|
|
981
|
+
// Skip if function name suggests it's designed for file operations
|
|
982
|
+
const isFileOperationFunction = /\b(checksum|hash|digest|fingerprint|read|write|load|save|get|set|copy|move|delete)File/i.test(content.slice(Math.max(0, index - 200), index + 100));
|
|
983
|
+
// Skip CLI command files - these take paths from command-line args (controlled inputs)
|
|
984
|
+
const isCLIFile = /\/(cli|commands?|bin)\//i.test(filePath) ||
|
|
985
|
+
/\/src\/(index|main|cli)\.(ts|js)$/i.test(filePath);
|
|
986
|
+
// Skip GitHub Action files - these process repo files (controlled environment)
|
|
987
|
+
const isGitHubAction = /github-action/i.test(filePath) ||
|
|
988
|
+
/action\.(ts|js|yml|yaml)$/i.test(filePath);
|
|
989
|
+
// Check for schema validation patterns in the surrounding context
|
|
990
|
+
// Zod, Yup, Joi, or regex validation on the input
|
|
991
|
+
const contextWindow = content.slice(Math.max(0, content.indexOf(line) - 500), content.indexOf(line) + line.length);
|
|
992
|
+
const hasSchemaValidation = /z\.(string|object)\s*\(\s*\)\.regex\s*\(/i.test(contextWindow) ||
|
|
993
|
+
/z\.enum\s*\(/i.test(contextWindow) ||
|
|
994
|
+
/\.regex\s*\(\s*\/.*\/\s*\)/i.test(contextWindow) || // .regex(/.../)
|
|
995
|
+
/\.match\s*\(\s*\/.*\/\s*\)/i.test(contextWindow) || // .match(/.../)
|
|
996
|
+
/\.(schema|validate)\s*\(/i.test(contextWindow) ||
|
|
997
|
+
/joi\./i.test(contextWindow) ||
|
|
998
|
+
/yup\./i.test(contextWindow);
|
|
999
|
+
// Check for path sanitization patterns
|
|
1000
|
+
const hasPathSanitization = hasPathTraversalProtection(contextWindow, line);
|
|
1001
|
+
if (isUtilityFile || isFileOperationFunction || isTestFile || isCLIFile || isGitHubAction || hasSchemaValidation || hasPathSanitization) {
|
|
1002
|
+
// Skip entirely for utility functions or when schema validation is present
|
|
1003
|
+
break;
|
|
1004
|
+
}
|
|
1005
|
+
}
|
|
1006
|
+
// Special handling for Path traversal risk - check for sanitization
|
|
1007
|
+
if (funcPattern.name === 'Path traversal risk') {
|
|
1008
|
+
const contextWindow = content.slice(Math.max(0, content.indexOf(line) - 500), content.indexOf(line) + line.length + 200);
|
|
1009
|
+
// Check for path sanitization patterns
|
|
1010
|
+
if (hasPathTraversalProtection(contextWindow, line)) {
|
|
1011
|
+
vulnerabilities.push({
|
|
1012
|
+
id: `dangerous-func-${filePath}-${index + 1}-${funcPattern.name}`,
|
|
1013
|
+
filePath,
|
|
1014
|
+
lineNumber: index + 1,
|
|
1015
|
+
lineContent: line.trim(),
|
|
1016
|
+
severity: 'info',
|
|
1017
|
+
category: 'dangerous_function',
|
|
1018
|
+
title: funcPattern.name + ' (sanitized)',
|
|
1019
|
+
description: 'User input in file path, but path traversal protection detected. Verify sanitization is comprehensive.',
|
|
1020
|
+
suggestedFix: 'Ensure path.resolve() result is checked against base directory and ".." sequences are rejected.',
|
|
1021
|
+
confidence: 'low',
|
|
1022
|
+
layer: 2,
|
|
1023
|
+
});
|
|
1024
|
+
break;
|
|
1025
|
+
}
|
|
1026
|
+
}
|
|
1027
|
+
// Special handling for Math.random() - skip cosmetic/UI uses
|
|
1028
|
+
if (funcPattern.name === 'Math.random for security') {
|
|
1029
|
+
// Check if this is cosmetic use (CSS, animations, UI variations)
|
|
1030
|
+
if (isCosmeticMathRandom(line, content, index)) {
|
|
1031
|
+
// Skip entirely - this is not a security concern
|
|
1032
|
+
break;
|
|
1033
|
+
}
|
|
1034
|
+
}
|
|
1035
|
+
// Standard handling for all other patterns
|
|
1036
|
+
let severity = funcPattern.severity;
|
|
1037
|
+
let confidence = 'high';
|
|
1038
|
+
if (isTestFile) {
|
|
1039
|
+
if (severity === 'critical') {
|
|
1040
|
+
severity = 'medium';
|
|
1041
|
+
}
|
|
1042
|
+
else if (severity === 'high') {
|
|
1043
|
+
severity = 'low';
|
|
1044
|
+
}
|
|
1045
|
+
else {
|
|
1046
|
+
severity = 'info';
|
|
1047
|
+
}
|
|
1048
|
+
confidence = 'low';
|
|
1049
|
+
}
|
|
1050
|
+
vulnerabilities.push({
|
|
1051
|
+
id: `dangerous-func-${filePath}-${index + 1}-${funcPattern.name}`,
|
|
1052
|
+
filePath,
|
|
1053
|
+
lineNumber: index + 1,
|
|
1054
|
+
lineContent: line.trim(),
|
|
1055
|
+
severity,
|
|
1056
|
+
category: 'dangerous_function',
|
|
1057
|
+
title: funcPattern.name,
|
|
1058
|
+
description: funcPattern.description + (isTestFile ? ' (in test file)' : ''),
|
|
1059
|
+
suggestedFix: funcPattern.suggestedFix,
|
|
1060
|
+
confidence,
|
|
1061
|
+
layer: 2,
|
|
1062
|
+
});
|
|
1063
|
+
break; // Only report once per line
|
|
1064
|
+
}
|
|
1065
|
+
}
|
|
1066
|
+
});
|
|
1067
|
+
// Additional standalone checks (not in DANGEROUS_FUNCTIONS array)
|
|
1068
|
+
// JSON.parse source-aware detection
|
|
1069
|
+
detectJSONParseSafe(content, filePath, isTestFile, vulnerabilities);
|
|
1070
|
+
// request.json() / req.json() schema validation suggestion
|
|
1071
|
+
detectRequestJsonValidation(content, filePath, isTestFile, vulnerabilities);
|
|
1072
|
+
return vulnerabilities;
|
|
1073
|
+
}
|
|
1074
|
+
/**
|
|
1075
|
+
* Detect JSON.parse usage with source-aware severity
|
|
1076
|
+
* Much smarter than simple pattern matching - considers try/catch and data source
|
|
1077
|
+
*/
|
|
1078
|
+
function detectJSONParseSafe(content, filePath, isTestFile, vulnerabilities) {
|
|
1079
|
+
const lines = content.split('\n');
|
|
1080
|
+
const jsonParsePattern = /JSON\.parse\s*\(/gi;
|
|
1081
|
+
// Track instances per file to aggregate noisy patterns
|
|
1082
|
+
const instances = [];
|
|
1083
|
+
lines.forEach((line, index) => {
|
|
1084
|
+
if ((0, context_helpers_1.isComment)(line))
|
|
1085
|
+
return;
|
|
1086
|
+
jsonParsePattern.lastIndex = 0;
|
|
1087
|
+
if (!jsonParsePattern.test(line))
|
|
1088
|
+
return;
|
|
1089
|
+
const jsonSource = classifyJSONParseSource(line, filePath);
|
|
1090
|
+
// Skip migration files entirely - they're internal tooling
|
|
1091
|
+
if (jsonSource === 'migration')
|
|
1092
|
+
return;
|
|
1093
|
+
// Skip test fixtures entirely - they're intentionally parsing test data
|
|
1094
|
+
if (jsonSource === 'test_fixture')
|
|
1095
|
+
return;
|
|
1096
|
+
// Skip trusted SDK responses - these are well-defined and safe to parse
|
|
1097
|
+
if (isTrustedSDKResponse(line, content))
|
|
1098
|
+
return;
|
|
1099
|
+
// Check if JSON.parse is inside a try-catch block
|
|
1100
|
+
const insideTryCatch = isInsideTryCatch(content, index) || hasTryCatchNearby(content, index);
|
|
1101
|
+
// Check if schema validation is applied after JSON.parse
|
|
1102
|
+
const hasSchemaValidation = hasSchemaValidationNearby(content, index);
|
|
1103
|
+
// If inside try-catch with safe source, suppress entirely - this is perfectly fine
|
|
1104
|
+
if (insideTryCatch && ['local_storage', 'database', 'config', 'internal', 'ui_state'].includes(jsonSource)) {
|
|
1105
|
+
return;
|
|
1106
|
+
}
|
|
1107
|
+
// If schema validation is present, this is properly handled
|
|
1108
|
+
if (hasSchemaValidation) {
|
|
1109
|
+
return;
|
|
1110
|
+
}
|
|
1111
|
+
// UI state (settings, providers, modals) - very low risk, aggregate or skip
|
|
1112
|
+
if (jsonSource === 'ui_state') {
|
|
1113
|
+
// Only track for aggregation, don't report individually
|
|
1114
|
+
instances.push({ lineNumber: index + 1, lineContent: line.trim(), source: jsonSource });
|
|
1115
|
+
return;
|
|
1116
|
+
}
|
|
1117
|
+
// Determine severity based on source and error handling
|
|
1118
|
+
let severity;
|
|
1119
|
+
let description;
|
|
1120
|
+
let suggestedFix;
|
|
1121
|
+
let confidence = 'medium';
|
|
1122
|
+
if (insideTryCatch) {
|
|
1123
|
+
// Already has error handling
|
|
1124
|
+
switch (jsonSource) {
|
|
1125
|
+
case 'user_input':
|
|
1126
|
+
severity = 'low';
|
|
1127
|
+
description = 'JSON.parse on user input is wrapped in try-catch. Consider adding schema validation (zod/yup) to validate the parsed structure.';
|
|
1128
|
+
suggestedFix = 'Add schema validation after parsing: const validated = schema.parse(JSON.parse(input))';
|
|
1129
|
+
confidence = 'low';
|
|
1130
|
+
break;
|
|
1131
|
+
default:
|
|
1132
|
+
// With try-catch and non-user source, this is fine - don't report
|
|
1133
|
+
return;
|
|
1134
|
+
}
|
|
1135
|
+
}
|
|
1136
|
+
else {
|
|
1137
|
+
// No try-catch
|
|
1138
|
+
switch (jsonSource) {
|
|
1139
|
+
case 'user_input':
|
|
1140
|
+
severity = 'medium';
|
|
1141
|
+
description = 'JSON.parse on user input without schema validation. Malformed input will crash; malicious input may have unexpected shape.';
|
|
1142
|
+
suggestedFix = 'Use a schema validation library (zod, yup, joi): try { const data = schema.parse(JSON.parse(body)) } catch (e) { return 400 }';
|
|
1143
|
+
confidence = 'high';
|
|
1144
|
+
break;
|
|
1145
|
+
case 'local_storage':
|
|
1146
|
+
severity = 'info';
|
|
1147
|
+
description = 'JSON.parse on localStorage data. Consider adding try-catch for robustness against corrupted data.';
|
|
1148
|
+
suggestedFix = 'Wrap in try-catch to handle corrupted localStorage gracefully.';
|
|
1149
|
+
confidence = 'low';
|
|
1150
|
+
break;
|
|
1151
|
+
case 'database':
|
|
1152
|
+
// Database content parsing is very common and low-risk
|
|
1153
|
+
instances.push({ lineNumber: index + 1, lineContent: line.trim(), source: jsonSource });
|
|
1154
|
+
return; // Will be aggregated below
|
|
1155
|
+
case 'config':
|
|
1156
|
+
case 'internal':
|
|
1157
|
+
severity = 'info';
|
|
1158
|
+
description = `JSON.parse on ${jsonSource.replace('_', ' ')} data without error handling. Low risk but consider defensive coding.`;
|
|
1159
|
+
suggestedFix = 'Consider adding try-catch for robustness.';
|
|
1160
|
+
confidence = 'low';
|
|
1161
|
+
break;
|
|
1162
|
+
default:
|
|
1163
|
+
// Unknown source - track for potential aggregation
|
|
1164
|
+
instances.push({ lineNumber: index + 1, lineContent: line.trim(), source: jsonSource });
|
|
1165
|
+
return; // Will be evaluated below based on aggregation
|
|
1166
|
+
}
|
|
1167
|
+
}
|
|
1168
|
+
// Downgrade test files
|
|
1169
|
+
if (isTestFile) {
|
|
1170
|
+
severity = 'info';
|
|
1171
|
+
confidence = 'low';
|
|
1172
|
+
description += ' (in test file)';
|
|
1173
|
+
}
|
|
1174
|
+
vulnerabilities.push({
|
|
1175
|
+
id: `json-parse-${filePath}-${index + 1}`,
|
|
1176
|
+
filePath,
|
|
1177
|
+
lineNumber: index + 1,
|
|
1178
|
+
lineContent: line.trim(),
|
|
1179
|
+
severity,
|
|
1180
|
+
category: 'dangerous_function',
|
|
1181
|
+
title: 'JSON.parse usage',
|
|
1182
|
+
description,
|
|
1183
|
+
suggestedFix,
|
|
1184
|
+
confidence,
|
|
1185
|
+
layer: 2,
|
|
1186
|
+
});
|
|
1187
|
+
});
|
|
1188
|
+
// Aggregate low-risk JSON.parse instances if there are many
|
|
1189
|
+
if (instances.length >= 3) {
|
|
1190
|
+
// Create single aggregated finding instead of N individual findings
|
|
1191
|
+
const lineNumbers = instances.map(i => i.lineNumber).slice(0, 5);
|
|
1192
|
+
const moreText = instances.length > 5 ? `... (${instances.length} total)` : '';
|
|
1193
|
+
vulnerabilities.push({
|
|
1194
|
+
id: `json-parse-aggregated-${filePath}`,
|
|
1195
|
+
filePath,
|
|
1196
|
+
lineNumber: instances[0].lineNumber,
|
|
1197
|
+
lineContent: `${instances.length} instances across this file`,
|
|
1198
|
+
severity: 'info',
|
|
1199
|
+
category: 'dangerous_function',
|
|
1200
|
+
title: `JSON.parse usage (${instances.length} instances)`,
|
|
1201
|
+
description: `JSON.parse detected. Consider adding error handling and schema validation if parsing user input.${isTestFile ? ' (in test file)' : ''}\n\nFound ${instances.length} occurrences at lines: ${lineNumbers.join(', ')}${moreText}`,
|
|
1202
|
+
suggestedFix: 'Add try-catch for error handling. If parsing user input, add schema validation.',
|
|
1203
|
+
confidence: 'low',
|
|
1204
|
+
layer: 2,
|
|
1205
|
+
});
|
|
1206
|
+
}
|
|
1207
|
+
else if (instances.length > 0 && instances.length < 3) {
|
|
1208
|
+
// Report individually for small counts
|
|
1209
|
+
for (const instance of instances) {
|
|
1210
|
+
vulnerabilities.push({
|
|
1211
|
+
id: `json-parse-${filePath}-${instance.lineNumber}`,
|
|
1212
|
+
filePath,
|
|
1213
|
+
lineNumber: instance.lineNumber,
|
|
1214
|
+
lineContent: instance.lineContent,
|
|
1215
|
+
severity: 'info',
|
|
1216
|
+
category: 'dangerous_function',
|
|
1217
|
+
title: 'JSON.parse usage',
|
|
1218
|
+
description: `JSON.parse on ${instance.source.replace('_', ' ')} data without error handling. Low risk but consider defensive coding.${isTestFile ? ' (in test file)' : ''}`,
|
|
1219
|
+
suggestedFix: 'Consider adding try-catch for robustness.',
|
|
1220
|
+
confidence: 'low',
|
|
1221
|
+
layer: 2,
|
|
1222
|
+
});
|
|
1223
|
+
}
|
|
1224
|
+
}
|
|
1225
|
+
}
|
|
1226
|
+
/**
|
|
1227
|
+
* Check if this file appears to have form/input validation elsewhere
|
|
1228
|
+
* (manual checks on body fields, type guards, etc.)
|
|
1229
|
+
*/
|
|
1230
|
+
function hasManualValidation(content) {
|
|
1231
|
+
const manualValidationPatterns = [
|
|
1232
|
+
// Type checking / type guards
|
|
1233
|
+
/typeof\s+\w+\s*[!=]==?\s*['"](?:string|number|boolean|object)['"]|Array\.isArray\s*\(/i,
|
|
1234
|
+
// Field existence checks followed by throws/returns
|
|
1235
|
+
/if\s*\(\s*!(?:body|data|input)\.\w+\s*\)\s*\{?\s*(throw|return)/i,
|
|
1236
|
+
// Property access with type assertion comments or inline validation
|
|
1237
|
+
/\b(body|data|input)\s*as\s+\w+/i, // Type assertion
|
|
1238
|
+
// Manual validation with error handling
|
|
1239
|
+
/if\s*\(\s*![\w.]+\s*\|\|\s*typeof\s+[\w.]+/i,
|
|
1240
|
+
// Using type predicates
|
|
1241
|
+
/is\w+\s*\([\w.]+\)/i, // isFoo(bar) pattern
|
|
1242
|
+
];
|
|
1243
|
+
return manualValidationPatterns.some(p => p.test(content));
|
|
1244
|
+
}
|
|
1245
|
+
/**
|
|
1246
|
+
* Check if route has throwing auth helper (getCurrentUserId, requireAuth, etc.)
|
|
1247
|
+
* Routes with throwing auth helpers are already protected
|
|
1248
|
+
*/
|
|
1249
|
+
function hasThrowingAuthHelper(content) {
|
|
1250
|
+
const throwingAuthPatterns = [
|
|
1251
|
+
/\bgetCurrentUserId\s*\(/i,
|
|
1252
|
+
/\brequireAuth\s*\(/i,
|
|
1253
|
+
/\bensureAuth\s*\(/i,
|
|
1254
|
+
/\bauth\s*\(\s*\)\s*\.protect\s*\(/i, // Clerk: auth().protect()
|
|
1255
|
+
/\bcurrentUser\s*\(\s*\)/i, // Clerk: currentUser()
|
|
1256
|
+
/\bgetServerSession\s*\([^)]*\)/i, // NextAuth
|
|
1257
|
+
/\bauth\s*\(\s*\)/i, // Generic auth() call
|
|
1258
|
+
/\bcheckAuth\s*\(/i,
|
|
1259
|
+
/\bverifyAuth\s*\(/i,
|
|
1260
|
+
/\bvalidateAuth\s*\(/i,
|
|
1261
|
+
/\bassertAuth\s*\(/i,
|
|
1262
|
+
/\bgetAuth\s*\(/i,
|
|
1263
|
+
/\brequireUser\s*\(/i,
|
|
1264
|
+
/\bgetUser\s*\(\s*\)/i, // supabase.auth.getUser()
|
|
1265
|
+
/const\s+\{\s*user\s*\}\s*=\s*await/i, // Destructuring pattern
|
|
1266
|
+
];
|
|
1267
|
+
return throwingAuthPatterns.some(p => p.test(content));
|
|
1268
|
+
}
|
|
1269
|
+
/**
|
|
1270
|
+
* Detect request.json() / req.json() and suggest schema validation
|
|
1271
|
+
* This is NOT a dangerous function - it's a prompt for best practices
|
|
1272
|
+
*/
|
|
1273
|
+
function detectRequestJsonValidation(content, filePath, isTestFile, vulnerabilities) {
|
|
1274
|
+
// Only check API route files
|
|
1275
|
+
if (!/\/(api|routes?|handlers?|controllers?)\//i.test(filePath) &&
|
|
1276
|
+
!/route\.(ts|js)$/i.test(filePath)) {
|
|
1277
|
+
return;
|
|
1278
|
+
}
|
|
1279
|
+
// Skip if route has throwing auth helper - these are already protected routes
|
|
1280
|
+
// and the schema validation suggestion is lower priority
|
|
1281
|
+
if (hasThrowingAuthHelper(content)) {
|
|
1282
|
+
return;
|
|
1283
|
+
}
|
|
1284
|
+
const lines = content.split('\n');
|
|
1285
|
+
// Matches: request.json(), req.json(), await request.json(), etc.
|
|
1286
|
+
const requestJsonPattern = /\b(request|req)\.json\s*\(\s*\)/gi;
|
|
1287
|
+
// Check if file has schema validation (library-based)
|
|
1288
|
+
const hasSchemaLibrary = /\b(zod|yup|joi|ajv|superstruct|valibot|typebox)\b/i.test(content) ||
|
|
1289
|
+
/\.parse\s*\(|\.validate\s*\(|\.safeParse\s*\(/i.test(content);
|
|
1290
|
+
// If file has schema library validation, don't report
|
|
1291
|
+
if (hasSchemaLibrary)
|
|
1292
|
+
return;
|
|
1293
|
+
// Check for manual validation patterns (less robust but still indicates intent)
|
|
1294
|
+
const hasManualCheck = hasManualValidation(content);
|
|
1295
|
+
// Track instances for potential aggregation
|
|
1296
|
+
const instances = [];
|
|
1297
|
+
lines.forEach((line, index) => {
|
|
1298
|
+
if ((0, context_helpers_1.isComment)(line))
|
|
1299
|
+
return;
|
|
1300
|
+
requestJsonPattern.lastIndex = 0;
|
|
1301
|
+
if (!requestJsonPattern.test(line))
|
|
1302
|
+
return;
|
|
1303
|
+
// Check if there's validation nearby (within 10 lines after)
|
|
1304
|
+
const startCheck = index;
|
|
1305
|
+
const endCheck = Math.min(lines.length, index + 10);
|
|
1306
|
+
const nearbyContent = lines.slice(startCheck, endCheck).join('\n');
|
|
1307
|
+
// If there's validation in the nearby lines, skip
|
|
1308
|
+
if (/\.parse\s*\(|\.validate\s*\(|\.safeParse\s*\(|schema\./i.test(nearbyContent)) {
|
|
1309
|
+
return;
|
|
1310
|
+
}
|
|
1311
|
+
// If manual validation is present, skip individual reporting but track for aggregate
|
|
1312
|
+
if (hasManualCheck) {
|
|
1313
|
+
instances.push({ lineNumber: index + 1, lineContent: line.trim() });
|
|
1314
|
+
return;
|
|
1315
|
+
}
|
|
1316
|
+
if (isTestFile) {
|
|
1317
|
+
return; // Don't report in test files
|
|
1318
|
+
}
|
|
1319
|
+
instances.push({ lineNumber: index + 1, lineContent: line.trim() });
|
|
1320
|
+
});
|
|
1321
|
+
// Don't report if no instances found
|
|
1322
|
+
if (instances.length === 0)
|
|
1323
|
+
return;
|
|
1324
|
+
// If manual validation exists, create a single info-level note
|
|
1325
|
+
if (hasManualCheck && instances.length > 0) {
|
|
1326
|
+
vulnerabilities.push({
|
|
1327
|
+
id: `request-json-manual-${filePath}`,
|
|
1328
|
+
filePath,
|
|
1329
|
+
lineNumber: instances[0].lineNumber,
|
|
1330
|
+
lineContent: instances[0].lineContent,
|
|
1331
|
+
severity: 'info',
|
|
1332
|
+
category: 'dangerous_function',
|
|
1333
|
+
title: 'Request body with manual validation',
|
|
1334
|
+
description: `API endpoint parses request body with manual validation patterns detected. Consider using a schema library (zod, yup) for more robust type-safe validation.`,
|
|
1335
|
+
suggestedFix: 'While manual validation works, schema libraries provide better TypeScript integration and error messages.',
|
|
1336
|
+
confidence: 'low',
|
|
1337
|
+
layer: 2,
|
|
1338
|
+
});
|
|
1339
|
+
return;
|
|
1340
|
+
}
|
|
1341
|
+
// Aggregate if multiple instances without validation
|
|
1342
|
+
if (instances.length >= 2) {
|
|
1343
|
+
const lineNumbers = instances.map(i => i.lineNumber).slice(0, 5);
|
|
1344
|
+
vulnerabilities.push({
|
|
1345
|
+
id: `request-json-aggregated-${filePath}`,
|
|
1346
|
+
filePath,
|
|
1347
|
+
lineNumber: instances[0].lineNumber,
|
|
1348
|
+
lineContent: `${instances.length} instances`,
|
|
1349
|
+
severity: 'info',
|
|
1350
|
+
category: 'dangerous_function',
|
|
1351
|
+
title: `Request body without schema validation (${instances.length} instances)`,
|
|
1352
|
+
description: `API endpoint parses request body without visible schema validation at lines: ${lineNumbers.join(', ')}. Consider validating the shape of incoming data.`,
|
|
1353
|
+
suggestedFix: 'Add schema validation (e.g., zod): const body = await request.json(); const data = schema.parse(body);',
|
|
1354
|
+
confidence: 'low',
|
|
1355
|
+
layer: 2,
|
|
1356
|
+
});
|
|
1357
|
+
}
|
|
1358
|
+
else {
|
|
1359
|
+
// Single instance
|
|
1360
|
+
vulnerabilities.push({
|
|
1361
|
+
id: `request-json-${filePath}-${instances[0].lineNumber}`,
|
|
1362
|
+
filePath,
|
|
1363
|
+
lineNumber: instances[0].lineNumber,
|
|
1364
|
+
lineContent: instances[0].lineContent,
|
|
1365
|
+
severity: 'info',
|
|
1366
|
+
category: 'dangerous_function',
|
|
1367
|
+
title: 'Request body without schema validation',
|
|
1368
|
+
description: 'API endpoint parses request body without visible schema validation. Consider validating the shape of incoming data.',
|
|
1369
|
+
suggestedFix: 'Add schema validation (e.g., zod): const body = await request.json(); const data = schema.parse(body);',
|
|
1370
|
+
confidence: 'low',
|
|
1371
|
+
layer: 2,
|
|
1372
|
+
});
|
|
1373
|
+
}
|
|
1374
|
+
}
|
|
1375
|
+
//# sourceMappingURL=dangerous-functions.js.map
|