@kernlang/review 3.1.6 → 3.1.7
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/cache.d.ts +1 -1
- package/dist/cache.js +5 -3
- package/dist/cache.js.map +1 -1
- package/dist/call-graph.d.ts +63 -0
- package/dist/call-graph.js +380 -0
- package/dist/call-graph.js.map +1 -0
- package/dist/concept-rules/boundary-mutation.d.ts +1 -1
- package/dist/concept-rules/boundary-mutation.js.map +1 -1
- package/dist/concept-rules/ignored-error.d.ts +1 -1
- package/dist/concept-rules/ignored-error.js.map +1 -1
- package/dist/concept-rules/illegal-dependency.d.ts +1 -1
- package/dist/concept-rules/illegal-dependency.js.map +1 -1
- package/dist/concept-rules/index.js +1 -6
- package/dist/concept-rules/index.js.map +1 -1
- package/dist/concept-rules/unguarded-effect.d.ts +1 -1
- package/dist/concept-rules/unguarded-effect.js.map +1 -1
- package/dist/concept-rules/unrecovered-effect.d.ts +1 -1
- package/dist/concept-rules/unrecovered-effect.js +2 -1
- package/dist/concept-rules/unrecovered-effect.js.map +1 -1
- package/dist/confidence.js +12 -8
- package/dist/confidence.js.map +1 -1
- package/dist/differ.js +3 -7
- package/dist/differ.js.map +1 -1
- package/dist/external-tools.js +5 -6
- package/dist/external-tools.js.map +1 -1
- package/dist/file-context.d.ts +21 -0
- package/dist/file-context.js +234 -0
- package/dist/file-context.js.map +1 -0
- package/dist/file-role.js +14 -7
- package/dist/file-role.js.map +1 -1
- package/dist/graph.d.ts +1 -1
- package/dist/graph.js +24 -16
- package/dist/graph.js.map +1 -1
- package/dist/index.d.ts +44 -35
- package/dist/index.js +210 -121
- package/dist/index.js.map +1 -1
- package/dist/inferrer.d.ts +8 -2
- package/dist/inferrer.js +80 -47
- package/dist/inferrer.js.map +1 -1
- package/dist/kern-lint.d.ts +3 -4
- package/dist/kern-lint.js +7 -5
- package/dist/kern-lint.js.map +1 -1
- package/dist/llm-bridge.d.ts +23 -7
- package/dist/llm-bridge.js +267 -31
- package/dist/llm-bridge.js.map +1 -1
- package/dist/llm-review.d.ts +16 -2
- package/dist/llm-review.js +240 -35
- package/dist/llm-review.js.map +1 -1
- package/dist/mappers/ts-concepts.d.ts +1 -1
- package/dist/mappers/ts-concepts.js +303 -32
- package/dist/mappers/ts-concepts.js.map +1 -1
- package/dist/norm-miner.d.ts +31 -0
- package/dist/norm-miner.js +119 -0
- package/dist/norm-miner.js.map +1 -0
- package/dist/obligations.d.ts +63 -0
- package/dist/obligations.js +158 -0
- package/dist/obligations.js.map +1 -0
- package/dist/quality-rules.d.ts +3 -3
- package/dist/quality-rules.js +4 -2
- package/dist/quality-rules.js.map +1 -1
- package/dist/reporter.d.ts +7 -2
- package/dist/reporter.js +82 -51
- package/dist/reporter.js.map +1 -1
- package/dist/rule-eval.d.ts +1 -2
- package/dist/rule-eval.js +5 -9
- package/dist/rule-eval.js.map +1 -1
- package/dist/rule-loader.js +16 -14
- package/dist/rule-loader.js.map +1 -1
- package/dist/rules/base.js +153 -69
- package/dist/rules/base.js.map +1 -1
- package/dist/rules/cli.js +23 -19
- package/dist/rules/cli.js.map +1 -1
- package/dist/rules/confidence.d.ts +1 -1
- package/dist/rules/confidence.js +5 -5
- package/dist/rules/confidence.js.map +1 -1
- package/dist/rules/dead-code.d.ts +10 -0
- package/dist/rules/dead-code.js +75 -0
- package/dist/rules/dead-code.js.map +1 -0
- package/dist/rules/dead-logic.js +35 -31
- package/dist/rules/dead-logic.js.map +1 -1
- package/dist/rules/express.d.ts +2 -1
- package/dist/rules/express.js +380 -126
- package/dist/rules/express.js.map +1 -1
- package/dist/rules/fastapi.js +53 -19
- package/dist/rules/fastapi.js.map +1 -1
- package/dist/rules/ground-layer.js +3 -3
- package/dist/rules/ground-layer.js.map +1 -1
- package/dist/rules/index.js +574 -105
- package/dist/rules/index.js.map +1 -1
- package/dist/rules/ink.js +9 -8
- package/dist/rules/ink.js.map +1 -1
- package/dist/rules/kern-source.js +202 -63
- package/dist/rules/kern-source.js.map +1 -1
- package/dist/rules/nextjs.js +88 -33
- package/dist/rules/nextjs.js.map +1 -1
- package/dist/rules/null-safety.js +52 -26
- package/dist/rules/null-safety.js.map +1 -1
- package/dist/rules/nuxt.js +24 -29
- package/dist/rules/nuxt.js.map +1 -1
- package/dist/rules/react.js +355 -69
- package/dist/rules/react.js.map +1 -1
- package/dist/rules/security-v2.js +71 -57
- package/dist/rules/security-v2.js.map +1 -1
- package/dist/rules/security-v3.js.map +1 -1
- package/dist/rules/security-v4.js +54 -27
- package/dist/rules/security-v4.js.map +1 -1
- package/dist/rules/security.js +35 -5
- package/dist/rules/security.js.map +1 -1
- package/dist/rules/terminal.js +17 -5
- package/dist/rules/terminal.js.map +1 -1
- package/dist/rules/vue.js +162 -107
- package/dist/rules/vue.js.map +1 -1
- package/dist/semantic-diff.d.ts +52 -0
- package/dist/semantic-diff.js +342 -0
- package/dist/semantic-diff.js.map +1 -0
- package/dist/spec-checker.js +11 -10
- package/dist/spec-checker.js.map +1 -1
- package/dist/suppression/apply-suppression.d.ts +2 -3
- package/dist/suppression/apply-suppression.js +3 -3
- package/dist/suppression/apply-suppression.js.map +1 -1
- package/dist/suppression/index.d.ts +2 -2
- package/dist/suppression/index.js +1 -1
- package/dist/suppression/index.js.map +1 -1
- package/dist/suppression/parse-directives.d.ts +1 -1
- package/dist/suppression/parse-directives.js +9 -4
- package/dist/suppression/parse-directives.js.map +1 -1
- package/dist/taint-ast.d.ts +20 -0
- package/dist/taint-ast.js +427 -0
- package/dist/taint-ast.js.map +1 -0
- package/dist/taint-crossfile.d.ts +28 -0
- package/dist/taint-crossfile.js +174 -0
- package/dist/taint-crossfile.js.map +1 -0
- package/dist/taint-findings.d.ts +17 -0
- package/dist/taint-findings.js +131 -0
- package/dist/taint-findings.js.map +1 -0
- package/dist/taint-regex.d.ts +61 -0
- package/dist/taint-regex.js +379 -0
- package/dist/taint-regex.js.map +1 -0
- package/dist/taint-types.d.ts +128 -0
- package/dist/taint-types.js +174 -0
- package/dist/taint-types.js.map +1 -0
- package/dist/taint.d.ts +13 -107
- package/dist/taint.js +16 -1067
- package/dist/taint.js.map +1 -1
- package/dist/template-detector.d.ts +2 -2
- package/dist/template-detector.js +11 -16
- package/dist/template-detector.js.map +1 -1
- package/dist/types.d.ts +35 -0
- package/dist/types.js.map +1 -1
- package/package.json +2 -2
package/dist/taint.js
CHANGED
|
@@ -1,1081 +1,30 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Taint Tracking —
|
|
2
|
+
* Taint Tracking — barrel re-export module.
|
|
3
3
|
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
// ──
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
const HTTP_PARAM_TYPES = /Request|IncomingMessage|FastifyRequest|KoaContext|Context/;
|
|
20
|
-
/** User input access patterns — what flows from HTTP params */
|
|
21
|
-
const USER_INPUT_ACCESS = [
|
|
22
|
-
{ pattern: /\breq\.body\b/, origin: 'req.body' },
|
|
23
|
-
{ pattern: /\breq\.query\b/, origin: 'req.query' },
|
|
24
|
-
{ pattern: /\breq\.params\b/, origin: 'req.params' },
|
|
25
|
-
{ pattern: /\breq\.headers\b/, origin: 'req.headers' },
|
|
26
|
-
{ pattern: /\brequest\.body\b/, origin: 'request.body' },
|
|
27
|
-
{ pattern: /\brequest\.query\b/, origin: 'request.query' },
|
|
28
|
-
{ pattern: /\brequest\.params\b/, origin: 'request.params' },
|
|
29
|
-
{ pattern: /\bprocess\.argv\b/, origin: 'process.argv' },
|
|
30
|
-
{ pattern: /\bprocess\.env\b/, origin: 'process.env' },
|
|
31
|
-
// DB read results (indirect injection sources)
|
|
32
|
-
{ pattern: /\bdb\.query\b/, origin: 'db.query' },
|
|
33
|
-
{ pattern: /\bfindOne\b/, origin: 'findOne' },
|
|
34
|
-
{ pattern: /\bfindById\b/, origin: 'findById' },
|
|
35
|
-
{ pattern: /\bgetItem\b/, origin: 'getItem' },
|
|
36
|
-
{ pattern: /\bcollection\.find\b/, origin: 'collection.find' },
|
|
37
|
-
// RAG/retrieval results
|
|
38
|
-
{ pattern: /\bvectorStore\.search\b/, origin: 'vectorStore.search' },
|
|
39
|
-
{ pattern: /\bsimilaritySearch\b/, origin: 'similaritySearch' },
|
|
40
|
-
{ pattern: /\bindex\.query\b/, origin: 'index.query' },
|
|
41
|
-
];
|
|
42
|
-
const SINK_PATTERNS = [
|
|
43
|
-
// Command execution
|
|
44
|
-
{ pattern: /\bexec\s*\(/, name: 'exec', category: 'command' },
|
|
45
|
-
{ pattern: /\bexecSync\s*\(/, name: 'execSync', category: 'command' },
|
|
46
|
-
{ pattern: /\bspawn\s*\(/, name: 'spawn', category: 'command' },
|
|
47
|
-
{ pattern: /\bspawnSync\s*\(/, name: 'spawnSync', category: 'command' },
|
|
48
|
-
{ pattern: /\bexecFile\s*\(/, name: 'execFile', category: 'command' },
|
|
49
|
-
// Filesystem
|
|
50
|
-
{ pattern: /\bwriteFile\s*\(/, name: 'writeFile', category: 'fs' },
|
|
51
|
-
{ pattern: /\bwriteFileSync\s*\(/, name: 'writeFileSync', category: 'fs' },
|
|
52
|
-
{ pattern: /\bcreateWriteStream\s*\(/, name: 'createWriteStream', category: 'fs' },
|
|
53
|
-
{ pattern: /\bunlink\s*\(/, name: 'unlink', category: 'fs' },
|
|
54
|
-
{ pattern: /\bunlinkSync\s*\(/, name: 'unlinkSync', category: 'fs' },
|
|
55
|
-
// SQL (template literal with query-like calls)
|
|
56
|
-
{ pattern: /\bquery\s*\(/, name: 'query', category: 'sql' },
|
|
57
|
-
{ pattern: /\b\$execute\s*\(/, name: '$execute', category: 'sql' },
|
|
58
|
-
{ pattern: /\braw\s*\(/, name: 'raw', category: 'sql' },
|
|
59
|
-
// Redirect
|
|
60
|
-
{ pattern: /\bredirect\s*\(/, name: 'redirect', category: 'redirect' },
|
|
61
|
-
// Eval
|
|
62
|
-
{ pattern: /\beval\s*\(/, name: 'eval', category: 'eval' },
|
|
63
|
-
{ pattern: /\bnew\s+Function\s*\(/, name: 'new Function', category: 'eval' },
|
|
64
|
-
// LLM API calls (prompt injection sinks)
|
|
65
|
-
{ pattern: /\bgenerateContent\s*\(/, name: 'generateContent', category: 'template' },
|
|
66
|
-
{ pattern: /\bsendMessage\s*\(/, name: 'sendMessage', category: 'template' },
|
|
67
|
-
{ pattern: /\bchat\.completions\.create\s*\(/, name: 'chat.completions.create', category: 'template' },
|
|
68
|
-
// VM execution sinks (LLM output execution)
|
|
69
|
-
{ pattern: /\bvm\.runInContext\s*\(/, name: 'vm.runInContext', category: 'eval' },
|
|
70
|
-
{ pattern: /\bvm\.runInNewContext\s*\(/, name: 'vm.runInNewContext', category: 'eval' },
|
|
71
|
-
];
|
|
72
|
-
// ── Sanitizer Detection ─────────────────────────────────────────────────
|
|
73
|
-
const SANITIZER_PATTERNS = [
|
|
74
|
-
// Type coercion (sanitizes to safe type)
|
|
75
|
-
{ pattern: /\bparseInt\s*\(/, name: 'parseInt' },
|
|
76
|
-
{ pattern: /\bparseFloat\s*\(/, name: 'parseFloat' },
|
|
77
|
-
{ pattern: /\bNumber\s*\(/, name: 'Number()' },
|
|
78
|
-
{ pattern: /\bBoolean\s*\(/, name: 'Boolean()' },
|
|
79
|
-
// Schema validation
|
|
80
|
-
{ pattern: /\.parse\s*\(/, name: 'schema.parse' },
|
|
81
|
-
{ pattern: /\.safeParse\s*\(/, name: 'schema.safeParse' },
|
|
82
|
-
{ pattern: /\.validate\s*\(/, name: 'schema.validate' },
|
|
83
|
-
{ pattern: /\.validateSync\s*\(/, name: 'schema.validateSync' },
|
|
84
|
-
// String sanitization
|
|
85
|
-
{ pattern: /\bsanitize\w*\s?\(/, name: 'sanitize()' },
|
|
86
|
-
{ pattern: /\bescape\w*\s?\(/, name: 'escape()' },
|
|
87
|
-
{ pattern: /\bDOMPurify\b/, name: 'DOMPurify' },
|
|
88
|
-
{ pattern: /\bencodeURI(Component)?\s*\(/, name: 'encodeURIComponent' },
|
|
89
|
-
// Path sanitization
|
|
90
|
-
{ pattern: /path\.(resolve|normalize|basename)\s*\(/, name: 'path.normalize' },
|
|
91
|
-
{ pattern: /\.replace\s*\(\s*\/.*\.\.\//, name: 'replace(../)' },
|
|
92
|
-
// SQL parameterization
|
|
93
|
-
{ pattern: /\$\d+/, name: 'parameterized query ($N)' },
|
|
94
|
-
{ pattern: /\?\s*,/, name: 'parameterized query (?)' },
|
|
95
|
-
// Prompt sanitization
|
|
96
|
-
{ pattern: /\bsanitizeForPrompt\s*\(/, name: 'sanitizeForPrompt' },
|
|
97
|
-
{ pattern: /\bescapePrompt\s*\(/, name: 'escapePrompt' },
|
|
98
|
-
// LLM-specific sanitizers
|
|
99
|
-
{ pattern: /\bstripDelimiters\s*\(/, name: 'stripDelimiters' },
|
|
100
|
-
{ pattern: /\bcleanForPrompt\s*\(/, name: 'cleanForPrompt' },
|
|
101
|
-
];
|
|
102
|
-
const SANITIZER_SUFFICIENCY = {
|
|
103
|
-
'parseInt': new Set(['sql']),
|
|
104
|
-
'parseFloat': new Set(['sql']),
|
|
105
|
-
'Number()': new Set(['sql']),
|
|
106
|
-
'Boolean()': new Set([]), // too weak for anything
|
|
107
|
-
'schema.parse': new Set(['command', 'fs', 'sql', 'redirect', 'eval', 'template']),
|
|
108
|
-
'schema.safeParse': new Set(['command', 'fs', 'sql', 'redirect', 'eval', 'template']),
|
|
109
|
-
'schema.validate': new Set(['command', 'fs', 'sql', 'redirect', 'eval', 'template']),
|
|
110
|
-
'schema.validateSync': new Set(['command', 'fs', 'sql', 'redirect', 'eval', 'template']),
|
|
111
|
-
'sanitize()': new Set(['template']),
|
|
112
|
-
'escape()': new Set(['sql', 'template']),
|
|
113
|
-
'DOMPurify': new Set(['template']),
|
|
114
|
-
'encodeURIComponent': new Set(['redirect']),
|
|
115
|
-
'path.normalize': new Set(['fs']),
|
|
116
|
-
'replace(../)': new Set(['fs']),
|
|
117
|
-
'parameterized query ($N)': new Set(['sql']),
|
|
118
|
-
'parameterized query (?)': new Set(['sql']),
|
|
119
|
-
'sanitizeForPrompt': new Set(['template']),
|
|
120
|
-
'escapePrompt': new Set(['template']),
|
|
121
|
-
'stripDelimiters': new Set(['template']),
|
|
122
|
-
'cleanForPrompt': new Set(['template']),
|
|
123
|
-
};
|
|
124
|
-
/**
|
|
125
|
-
* Check if a sanitizer is actually sufficient for a given sink category.
|
|
126
|
-
* Returns true if the sanitizer protects against the sink, false if it's
|
|
127
|
-
* a mismatch (e.g., parseInt used to "sanitize" command injection).
|
|
128
|
-
*/
|
|
129
|
-
export function isSanitizerSufficient(sanitizerName, sinkCategory) {
|
|
130
|
-
const allowed = SANITIZER_SUFFICIENCY[sanitizerName];
|
|
131
|
-
if (!allowed)
|
|
132
|
-
return false; // Unknown sanitizer — default deny, verify manually
|
|
133
|
-
return allowed.has(sinkCategory);
|
|
134
|
-
}
|
|
135
|
-
// ── Main Analysis ────────────────────────────────────────────────────────
|
|
4
|
+
* Re-exports everything from the 5 taint sub-modules so existing
|
|
5
|
+
* imports from './taint.js' continue to work unchanged.
|
|
6
|
+
*/
|
|
7
|
+
import { analyzeTaintAST } from './taint-ast.js';
|
|
8
|
+
import { analyzeTaintRegex } from './taint-regex.js';
|
|
9
|
+
export { HTTP_PARAM_NAMES, HTTP_PARAM_TYPES, isSanitizerSufficient, SANITIZER_PATTERN_NAMES, SANITIZER_PATTERNS, SINK_NAMES, SINK_PATTERNS, USER_INPUT_ACCESS, } from './taint-types.js';
|
|
10
|
+
// ── AST Engine ──────────────────────────────────────────────────────────
|
|
11
|
+
export { analyzeTaintAST, buildInternalSinkMap } from './taint-ast.js';
|
|
12
|
+
// ── Regex Engine ────────────────────────────────────────────────────────
|
|
13
|
+
export { analyzeTaintRegex, buildPaths, classifyParams, detectSanitizers, extractAllAssignments, extractDependencies, findClosingParen, findTaintedSinks, isCircularAssignment, parseLineAssignments, propagateTaint, propagateTaintMultiHop, } from './taint-regex.js';
|
|
14
|
+
// ── Finding Generation ──────────────────────────────────────────────────
|
|
15
|
+
export { crossFileTaintToFindings, taintToFindings } from './taint-findings.js';
|
|
16
|
+
// ── Cross-File Analysis ─────────────────────────────────────────────────
|
|
17
|
+
export { analyzeTaintCrossFile, buildExportMap, buildImportMap } from './taint-crossfile.js';
|
|
18
|
+
// ── Main Entry Point ────────────────────────────────────────────────────
|
|
136
19
|
/**
|
|
137
20
|
* Run taint analysis on all fn nodes in inferred results.
|
|
138
21
|
* When sourceFile is provided, uses AST-based analysis (more accurate).
|
|
139
22
|
* Falls back to regex-based analysis when no SourceFile available.
|
|
140
23
|
*/
|
|
141
24
|
export function analyzeTaint(inferred, filePath, sourceFile) {
|
|
142
|
-
// Use AST-based analysis when SourceFile is available
|
|
143
25
|
if (sourceFile) {
|
|
144
26
|
return analyzeTaintAST(inferred, filePath, sourceFile);
|
|
145
27
|
}
|
|
146
28
|
return analyzeTaintRegex(inferred, filePath);
|
|
147
29
|
}
|
|
148
|
-
/**
|
|
149
|
-
* AST-based taint analysis — walks real ts-morph AST nodes instead of regex on strings.
|
|
150
|
-
* Handles destructuring, method chains, computed property access.
|
|
151
|
-
*/
|
|
152
|
-
function analyzeTaintAST(inferred, filePath, sourceFile) {
|
|
153
|
-
const results = [];
|
|
154
|
-
// Collect all function-like AST nodes from the SourceFile
|
|
155
|
-
const allFns = [];
|
|
156
|
-
for (const fn of sourceFile.getFunctions())
|
|
157
|
-
allFns.push({ node: fn, startLine: fn.getStartLineNumber() });
|
|
158
|
-
for (const stmt of sourceFile.getVariableStatements()) {
|
|
159
|
-
for (const decl of stmt.getDeclarations()) {
|
|
160
|
-
const init = decl.getInitializer();
|
|
161
|
-
if (init) {
|
|
162
|
-
const initKind = init.getKindName();
|
|
163
|
-
if (initKind === 'ArrowFunction' || initKind === 'FunctionExpression') {
|
|
164
|
-
allFns.push({ node: init, startLine: stmt.getStartLineNumber() });
|
|
165
|
-
}
|
|
166
|
-
}
|
|
167
|
-
}
|
|
168
|
-
}
|
|
169
|
-
for (const cls of sourceFile.getClasses()) {
|
|
170
|
-
for (const method of cls.getMethods()) {
|
|
171
|
-
allFns.push({ node: method, startLine: method.getStartLineNumber() });
|
|
172
|
-
}
|
|
173
|
-
}
|
|
174
|
-
for (const { node: fn, startLine } of allFns) {
|
|
175
|
-
const params = fn.getParameters();
|
|
176
|
-
const fnName = 'getName' in fn && typeof fn.getName === 'function' ? fn.getName() || 'anonymous' : 'anonymous';
|
|
177
|
-
// Step 1: Classify params as tainted using type info
|
|
178
|
-
const taintedParams = [];
|
|
179
|
-
for (const param of params) {
|
|
180
|
-
const name = param.getName();
|
|
181
|
-
const typeText = param.getType().getText(param);
|
|
182
|
-
if (HTTP_PARAM_NAMES.test(name) || HTTP_PARAM_TYPES.test(typeText)) {
|
|
183
|
-
taintedParams.push({ name, origin: `${name} (HTTP input)` });
|
|
184
|
-
}
|
|
185
|
-
}
|
|
186
|
-
if (taintedParams.length === 0)
|
|
187
|
-
continue;
|
|
188
|
-
// Step 2: AST-based taint propagation through the function body
|
|
189
|
-
const body = fn.getBody();
|
|
190
|
-
if (!body)
|
|
191
|
-
continue;
|
|
192
|
-
const taintedNames = new Set(taintedParams.map(p => p.name));
|
|
193
|
-
const taintedVars = new Map();
|
|
194
|
-
for (const p of taintedParams)
|
|
195
|
-
taintedVars.set(p.name, p);
|
|
196
|
-
// Walk ALL variable declarations including nested scopes (if/for/while)
|
|
197
|
-
// forEachDescendant visits in document order = parent-before-child
|
|
198
|
-
const varDecls = [];
|
|
199
|
-
body.forEachDescendant((node) => {
|
|
200
|
-
if (node.getKind() === SyntaxKind.VariableDeclaration) {
|
|
201
|
-
varDecls.push(node);
|
|
202
|
-
}
|
|
203
|
-
});
|
|
204
|
-
// Multiple passes to handle forward dependencies (max 3 hops)
|
|
205
|
-
for (let hop = 0; hop < 3; hop++) {
|
|
206
|
-
for (const decl of varDecls) {
|
|
207
|
-
const nameNode = decl.getNameNode();
|
|
208
|
-
const nameKind = nameNode.getKindName();
|
|
209
|
-
// Simple name binding: const id = parseInt(req.body.id)
|
|
210
|
-
if (nameKind === 'Identifier') {
|
|
211
|
-
const declName = nameNode.getText();
|
|
212
|
-
if (taintedNames.has(declName))
|
|
213
|
-
continue;
|
|
214
|
-
const init = decl.getInitializer();
|
|
215
|
-
if (!init)
|
|
216
|
-
continue;
|
|
217
|
-
if (astExprRefersToTainted(init, taintedNames)) {
|
|
218
|
-
taintedNames.add(declName);
|
|
219
|
-
const srcName = findTaintedIdentifier(init, taintedNames);
|
|
220
|
-
const srcVar = srcName ? taintedVars.get(srcName) : undefined;
|
|
221
|
-
const srcOrigin = srcVar?.origin;
|
|
222
|
-
taintedVars.set(declName, { name: declName, origin: srcOrigin || 'derived' });
|
|
223
|
-
}
|
|
224
|
-
}
|
|
225
|
-
// Object destructuring: const { x, y } = taintedObj
|
|
226
|
-
if (nameKind === 'ObjectBindingPattern') {
|
|
227
|
-
const init = decl.getInitializer();
|
|
228
|
-
if (!init || !astExprRefersToTainted(init, taintedNames))
|
|
229
|
-
continue;
|
|
230
|
-
const srcName = findTaintedIdentifier(init, taintedNames);
|
|
231
|
-
const srcVar2 = srcName ? taintedVars.get(srcName) : undefined;
|
|
232
|
-
const srcOrigin = srcVar2?.origin;
|
|
233
|
-
for (const element of nameNode.getElements()) {
|
|
234
|
-
const elName = element.getName();
|
|
235
|
-
if (!taintedNames.has(elName)) {
|
|
236
|
-
taintedNames.add(elName);
|
|
237
|
-
taintedVars.set(elName, { name: elName, origin: srcOrigin || 'destructured' });
|
|
238
|
-
}
|
|
239
|
-
}
|
|
240
|
-
}
|
|
241
|
-
// Array destructuring: const [a, b] = taintedArr
|
|
242
|
-
if (nameKind === 'ArrayBindingPattern') {
|
|
243
|
-
const init = decl.getInitializer();
|
|
244
|
-
if (!init || !astExprRefersToTainted(init, taintedNames))
|
|
245
|
-
continue;
|
|
246
|
-
const srcName = findTaintedIdentifier(init, taintedNames);
|
|
247
|
-
const srcVar3 = srcName ? taintedVars.get(srcName) : undefined;
|
|
248
|
-
const srcOrigin = srcVar3?.origin;
|
|
249
|
-
for (const element of nameNode.getElements()) {
|
|
250
|
-
if (element.getKindName() === 'BindingElement') {
|
|
251
|
-
const elName = element.getName();
|
|
252
|
-
if (!taintedNames.has(elName)) {
|
|
253
|
-
taintedNames.add(elName);
|
|
254
|
-
taintedVars.set(elName, { name: elName, origin: srcOrigin || 'destructured' });
|
|
255
|
-
}
|
|
256
|
-
}
|
|
257
|
-
}
|
|
258
|
-
}
|
|
259
|
-
}
|
|
260
|
-
}
|
|
261
|
-
// Step 3: Find sinks via AST CallExpression walk
|
|
262
|
-
const sinks = [];
|
|
263
|
-
const calls = [];
|
|
264
|
-
body.forEachDescendant(n => { if (n.getKindName() === 'CallExpression')
|
|
265
|
-
calls.push(n); });
|
|
266
|
-
for (const call of calls) {
|
|
267
|
-
const calleeName = getCalleeBaseName(call);
|
|
268
|
-
const sinkDef = SINK_NAMES.get(calleeName);
|
|
269
|
-
if (!sinkDef)
|
|
270
|
-
continue;
|
|
271
|
-
// Check if any argument references a tainted variable
|
|
272
|
-
for (const arg of call.getArguments()) {
|
|
273
|
-
const taintedArg = findTaintedIdentifier(arg, taintedNames);
|
|
274
|
-
if (taintedArg) {
|
|
275
|
-
sinks.push({
|
|
276
|
-
name: calleeName,
|
|
277
|
-
category: sinkDef,
|
|
278
|
-
taintedArg,
|
|
279
|
-
line: call.getStartLineNumber(),
|
|
280
|
-
});
|
|
281
|
-
break;
|
|
282
|
-
}
|
|
283
|
-
}
|
|
284
|
-
// Also check template literal arguments
|
|
285
|
-
const templateArgs = call.getArguments().filter(a => {
|
|
286
|
-
const k = a.getKindName();
|
|
287
|
-
return k === 'TemplateExpression' || k === 'NoSubstitutionTemplateLiteral';
|
|
288
|
-
});
|
|
289
|
-
for (const tpl of templateArgs) {
|
|
290
|
-
if (tpl.getKindName() === 'TemplateExpression') {
|
|
291
|
-
for (const span of tpl.getTemplateSpans()) {
|
|
292
|
-
const expr = span.getExpression();
|
|
293
|
-
const taintedArg = findTaintedIdentifier(expr, taintedNames);
|
|
294
|
-
if (taintedArg) {
|
|
295
|
-
sinks.push({
|
|
296
|
-
name: `${calleeName} (template)`,
|
|
297
|
-
category: sinkDef,
|
|
298
|
-
taintedArg,
|
|
299
|
-
line: call.getStartLineNumber(),
|
|
300
|
-
});
|
|
301
|
-
}
|
|
302
|
-
}
|
|
303
|
-
}
|
|
304
|
-
}
|
|
305
|
-
}
|
|
306
|
-
if (sinks.length === 0)
|
|
307
|
-
continue;
|
|
308
|
-
// Step 4: Check for sanitizers (AST-based)
|
|
309
|
-
const foundSanitizers = findSanitizersAST(body, taintedNames);
|
|
310
|
-
// Build paths
|
|
311
|
-
const paths = [];
|
|
312
|
-
for (const sink of sinks) {
|
|
313
|
-
const source = taintedVars.get(sink.taintedArg) || taintedParams[0];
|
|
314
|
-
// Subtree matching: sanitize(req.query) covers req.query.id but not req.body.cmd
|
|
315
|
-
// parseInt(req.query.id) does NOT cover exec(req) — only the specific property is safe
|
|
316
|
-
const sanitizer = foundSanitizers.find(s => {
|
|
317
|
-
for (const sv of s.sanitizedVars) {
|
|
318
|
-
if (sv === sink.taintedArg)
|
|
319
|
-
return true;
|
|
320
|
-
// Sanitized path is a prefix → covers all sub-properties
|
|
321
|
-
if (sink.taintedArg.startsWith(sv + '.'))
|
|
322
|
-
return true;
|
|
323
|
-
}
|
|
324
|
-
return false;
|
|
325
|
-
});
|
|
326
|
-
const hasSanitizer = sanitizer != null;
|
|
327
|
-
const sufficient = sanitizer != null ? isSanitizerSufficient(sanitizer.name, sink.category) : false;
|
|
328
|
-
paths.push({
|
|
329
|
-
source,
|
|
330
|
-
sink,
|
|
331
|
-
sanitized: hasSanitizer && sufficient,
|
|
332
|
-
sanitizer: sanitizer?.name,
|
|
333
|
-
insufficientSanitizer: hasSanitizer && !sufficient ? sanitizer.name : undefined,
|
|
334
|
-
});
|
|
335
|
-
}
|
|
336
|
-
if (paths.length > 0) {
|
|
337
|
-
results.push({ fnName, filePath, startLine, paths });
|
|
338
|
-
}
|
|
339
|
-
}
|
|
340
|
-
return results;
|
|
341
|
-
}
|
|
342
|
-
/** Check if an expression references any tainted variable name */
|
|
343
|
-
function astExprRefersToTainted(expr, taintedNames) {
|
|
344
|
-
const k = expr.getKindName();
|
|
345
|
-
if (k === 'Identifier' && taintedNames.has(expr.getText()))
|
|
346
|
-
return true;
|
|
347
|
-
if (k === 'PropertyAccessExpression') {
|
|
348
|
-
return astExprRefersToTainted(expr.getExpression(), taintedNames);
|
|
349
|
-
}
|
|
350
|
-
if (k === 'ElementAccessExpression') {
|
|
351
|
-
return astExprRefersToTainted(expr.getExpression(), taintedNames);
|
|
352
|
-
}
|
|
353
|
-
if (k === 'CallExpression') {
|
|
354
|
-
if (astExprRefersToTainted(expr.getExpression(), taintedNames))
|
|
355
|
-
return true;
|
|
356
|
-
for (const arg of expr.getArguments()) {
|
|
357
|
-
if (astExprRefersToTainted(arg, taintedNames))
|
|
358
|
-
return true;
|
|
359
|
-
}
|
|
360
|
-
return false;
|
|
361
|
-
}
|
|
362
|
-
if (k === 'AwaitExpression') {
|
|
363
|
-
return astExprRefersToTainted(expr.getExpression(), taintedNames);
|
|
364
|
-
}
|
|
365
|
-
// Check all children for complex expressions
|
|
366
|
-
for (const child of expr.getChildren()) {
|
|
367
|
-
if (astExprRefersToTainted(child, taintedNames))
|
|
368
|
-
return true;
|
|
369
|
-
}
|
|
370
|
-
return false;
|
|
371
|
-
}
|
|
372
|
-
/** Get the base name of a callee (e.g., exec from child_process.exec, or db.query) */
|
|
373
|
-
function getCalleeBaseName(call) {
|
|
374
|
-
const expr = call.getExpression();
|
|
375
|
-
const k = expr.getKindName();
|
|
376
|
-
if (k === 'Identifier')
|
|
377
|
-
return expr.getText();
|
|
378
|
-
if (k === 'PropertyAccessExpression')
|
|
379
|
-
return expr.getName();
|
|
380
|
-
return '';
|
|
381
|
-
}
|
|
382
|
-
/** Find the first tainted identifier in an expression tree */
|
|
383
|
-
/** Get the full static access path (e.g., req.query.id). Returns undefined for dynamic access. */
|
|
384
|
-
function getStaticAccessPath(expr) {
|
|
385
|
-
const k = expr.getKindName();
|
|
386
|
-
if (k === 'Identifier')
|
|
387
|
-
return expr.getText();
|
|
388
|
-
if (k === 'PropertyAccessExpression') {
|
|
389
|
-
const obj = getStaticAccessPath(expr.getExpression());
|
|
390
|
-
if (obj)
|
|
391
|
-
return `${obj}.${expr.getName()}`;
|
|
392
|
-
}
|
|
393
|
-
return undefined;
|
|
394
|
-
}
|
|
395
|
-
function findTaintedIdentifier(expr, taintedNames) {
|
|
396
|
-
const k = expr.getKindName();
|
|
397
|
-
if (k === 'Identifier' && taintedNames.has(expr.getText()))
|
|
398
|
-
return expr.getText();
|
|
399
|
-
if (k === 'PropertyAccessExpression') {
|
|
400
|
-
return findTaintedIdentifier(expr.getExpression(), taintedNames);
|
|
401
|
-
}
|
|
402
|
-
// Check binary expressions (string concatenation: 'cmd ' + userInput)
|
|
403
|
-
if (k === 'BinaryExpression') {
|
|
404
|
-
return findTaintedIdentifier(expr.getLeft(), taintedNames) || findTaintedIdentifier(expr.getRight(), taintedNames);
|
|
405
|
-
}
|
|
406
|
-
for (const child of expr.getChildren()) {
|
|
407
|
-
const found = findTaintedIdentifier(child, taintedNames);
|
|
408
|
-
if (found)
|
|
409
|
-
return found;
|
|
410
|
-
}
|
|
411
|
-
return undefined;
|
|
412
|
-
}
|
|
413
|
-
/** AST-based sanitizer detection */
|
|
414
|
-
function findSanitizersAST(body, taintedNames) {
|
|
415
|
-
const sanitizers = [];
|
|
416
|
-
const allCalls = [];
|
|
417
|
-
body.forEachDescendant(n => { if (n.getKindName() === 'CallExpression')
|
|
418
|
-
allCalls.push(n); });
|
|
419
|
-
for (const call of allCalls) {
|
|
420
|
-
const calleeName = getCalleeBaseName(call);
|
|
421
|
-
const matchedSanitizer = SANITIZER_PATTERN_NAMES.find(s => calleeName.includes(s));
|
|
422
|
-
if (!matchedSanitizer)
|
|
423
|
-
continue;
|
|
424
|
-
// Track which tainted vars are sanitized by this call
|
|
425
|
-
const sanitizedVars = new Set();
|
|
426
|
-
for (const arg of call.getArguments()) {
|
|
427
|
-
// Track the FULL access path so parseInt(req.query.id) sanitizes 'req.query.id', not 'req'
|
|
428
|
-
const fullPath = getStaticAccessPath(arg);
|
|
429
|
-
if (fullPath && findTaintedIdentifier(arg, taintedNames)) {
|
|
430
|
-
sanitizedVars.add(fullPath);
|
|
431
|
-
}
|
|
432
|
-
else {
|
|
433
|
-
const tainted = findTaintedIdentifier(arg, taintedNames);
|
|
434
|
-
if (tainted)
|
|
435
|
-
sanitizedVars.add(tainted);
|
|
436
|
-
}
|
|
437
|
-
}
|
|
438
|
-
// Also check if the result is assigned to a variable (replacing the tainted value)
|
|
439
|
-
const parent = call.getParent();
|
|
440
|
-
if (parent && parent.getKindName() === 'VariableDeclaration') {
|
|
441
|
-
const declName = parent.getName();
|
|
442
|
-
sanitizedVars.add(declName);
|
|
443
|
-
}
|
|
444
|
-
if (sanitizedVars.size > 0) {
|
|
445
|
-
sanitizers.push({ name: matchedSanitizer, sanitizedVars });
|
|
446
|
-
}
|
|
447
|
-
}
|
|
448
|
-
return sanitizers;
|
|
449
|
-
}
|
|
450
|
-
// Sink name → category lookup (flat map from SINK_PATTERNS)
|
|
451
|
-
const SINK_NAMES = new Map([
|
|
452
|
-
['exec', 'command'], ['execSync', 'command'], ['spawn', 'command'],
|
|
453
|
-
['spawnSync', 'command'], ['execFile', 'command'], ['execFileSync', 'command'],
|
|
454
|
-
['writeFile', 'fs'], ['writeFileSync', 'fs'], ['createWriteStream', 'fs'],
|
|
455
|
-
['unlink', 'fs'], ['unlinkSync', 'fs'],
|
|
456
|
-
['query', 'sql'], ['$execute', 'sql'], ['raw', 'sql'],
|
|
457
|
-
['$queryRaw', 'sql'], ['$queryRawUnsafe', 'sql'],
|
|
458
|
-
['redirect', 'redirect'],
|
|
459
|
-
['eval', 'eval'], ['Function', 'eval'],
|
|
460
|
-
]);
|
|
461
|
-
// Sanitizer names to detect (from SANITIZER_PATTERNS)
|
|
462
|
-
const SANITIZER_PATTERN_NAMES = [
|
|
463
|
-
'parseInt', 'parseFloat', 'Number', 'Boolean', 'String',
|
|
464
|
-
'encodeURI', 'encodeURIComponent', 'escape',
|
|
465
|
-
'sanitize', 'DOMPurify', 'purify', 'xss',
|
|
466
|
-
'escapeHtml', 'sqlstring', 'parameterized',
|
|
467
|
-
'parse', 'safeParse', 'validate',
|
|
468
|
-
];
|
|
469
|
-
/**
|
|
470
|
-
* Regex-based taint analysis — legacy fallback for when no SourceFile is available.
|
|
471
|
-
*/
|
|
472
|
-
function analyzeTaintRegex(inferred, filePath) {
|
|
473
|
-
const results = [];
|
|
474
|
-
for (const r of inferred) {
|
|
475
|
-
if (r.node.type !== 'fn')
|
|
476
|
-
continue;
|
|
477
|
-
const fnName = r.node.props?.name || 'anonymous';
|
|
478
|
-
const paramsStr = r.node.props?.params || '';
|
|
479
|
-
// Get handler body
|
|
480
|
-
const handler = r.node.children?.find(c => c.type === 'handler');
|
|
481
|
-
const code = handler?.props?.code || '';
|
|
482
|
-
if (!code)
|
|
483
|
-
continue;
|
|
484
|
-
// Step 1: Classify params as tainted
|
|
485
|
-
const taintedParams = classifyParams(paramsStr);
|
|
486
|
-
if (taintedParams.length === 0)
|
|
487
|
-
continue;
|
|
488
|
-
// Step 2: Propagate taint through assignments
|
|
489
|
-
const taintedVars = propagateTaint(code, taintedParams);
|
|
490
|
-
// Step 3: Find sinks that use tainted variables
|
|
491
|
-
const sinks = findTaintedSinks(code, taintedVars);
|
|
492
|
-
if (sinks.length === 0)
|
|
493
|
-
continue;
|
|
494
|
-
// Step 4: Check for sanitizers
|
|
495
|
-
const paths = buildPaths(code, taintedVars, sinks);
|
|
496
|
-
if (paths.length > 0) {
|
|
497
|
-
results.push({
|
|
498
|
-
fnName,
|
|
499
|
-
filePath,
|
|
500
|
-
startLine: r.startLine,
|
|
501
|
-
paths,
|
|
502
|
-
});
|
|
503
|
-
}
|
|
504
|
-
}
|
|
505
|
-
return results;
|
|
506
|
-
}
|
|
507
|
-
/**
|
|
508
|
-
* Classify function parameters as tainted or safe.
|
|
509
|
-
*/
|
|
510
|
-
function classifyParams(paramsStr) {
|
|
511
|
-
const sources = [];
|
|
512
|
-
if (!paramsStr)
|
|
513
|
-
return sources;
|
|
514
|
-
const params = paramsStr.split(',').map(p => {
|
|
515
|
-
const parts = p.trim().split(':');
|
|
516
|
-
return { name: parts[0]?.trim(), type: parts[1]?.trim() || '' };
|
|
517
|
-
});
|
|
518
|
-
for (const p of params) {
|
|
519
|
-
if (!p.name)
|
|
520
|
-
continue;
|
|
521
|
-
if (HTTP_PARAM_NAMES.test(p.name) || HTTP_PARAM_TYPES.test(p.type)) {
|
|
522
|
-
sources.push({ name: p.name, origin: `${p.name} (HTTP input)` });
|
|
523
|
-
}
|
|
524
|
-
}
|
|
525
|
-
return sources;
|
|
526
|
-
}
|
|
527
|
-
/**
|
|
528
|
-
* Multi-hop taint propagation using worklist algorithm.
|
|
529
|
-
* Propagates until fixed point or configurable depth limit.
|
|
530
|
-
*
|
|
531
|
-
* Handles all assignment patterns:
|
|
532
|
-
* - const b = a
|
|
533
|
-
* - const b = a.trim()
|
|
534
|
-
* - const {x} = obj
|
|
535
|
-
* - let b; b = a
|
|
536
|
-
*
|
|
537
|
-
* @param code - Handler code string
|
|
538
|
-
* @param initialTainted - Set of initially tainted variable names
|
|
539
|
-
* @param maxDepth - Maximum propagation depth (default: 3)
|
|
540
|
-
* @returns Set of all tainted variable names after fixed point or depth limit
|
|
541
|
-
*/
|
|
542
|
-
export function propagateTaintMultiHop(code, initialTainted, maxDepth = 3) {
|
|
543
|
-
const tainted = new Set(initialTainted);
|
|
544
|
-
const worklist = [];
|
|
545
|
-
const visitedAssignments = new Set();
|
|
546
|
-
const assignmentDepths = new Map();
|
|
547
|
-
for (const v of initialTainted) {
|
|
548
|
-
worklist.push({ varName: v, depth: 0 });
|
|
549
|
-
}
|
|
550
|
-
const allAssignments = extractAllAssignments(code);
|
|
551
|
-
while (worklist.length > 0) {
|
|
552
|
-
const { varName: currentVar, depth } = worklist.shift();
|
|
553
|
-
if (depth >= maxDepth)
|
|
554
|
-
continue;
|
|
555
|
-
for (const assignment of allAssignments) {
|
|
556
|
-
const { lhs, rhs, assignId } = assignment;
|
|
557
|
-
if (visitedAssignments.has(`${assignId}:${depth}`))
|
|
558
|
-
continue;
|
|
559
|
-
const rhsDeps = extractDependencies(rhs);
|
|
560
|
-
if (rhsDeps.has(currentVar)) {
|
|
561
|
-
visitedAssignments.add(`${assignId}:${depth}`);
|
|
562
|
-
const existingDepth = assignmentDepths.get(lhs);
|
|
563
|
-
if (existingDepth !== undefined && existingDepth <= depth + 1) {
|
|
564
|
-
continue;
|
|
565
|
-
}
|
|
566
|
-
assignmentDepths.set(lhs, depth + 1);
|
|
567
|
-
if (!tainted.has(lhs)) {
|
|
568
|
-
tainted.add(lhs);
|
|
569
|
-
worklist.push({ varName: lhs, depth: depth + 1 });
|
|
570
|
-
}
|
|
571
|
-
if (isCircularAssignment(lhs, rhs, allAssignments)) {
|
|
572
|
-
continue;
|
|
573
|
-
}
|
|
574
|
-
for (const dep of rhsDeps) {
|
|
575
|
-
if (dep !== currentVar && tainted.has(dep)) {
|
|
576
|
-
const depDepth = assignmentDepths.get(dep) ?? 0;
|
|
577
|
-
if (depth + 1 > depDepth) {
|
|
578
|
-
if (!tainted.has(lhs)) {
|
|
579
|
-
tainted.add(lhs);
|
|
580
|
-
worklist.push({ varName: lhs, depth: depth + 1 });
|
|
581
|
-
}
|
|
582
|
-
}
|
|
583
|
-
}
|
|
584
|
-
}
|
|
585
|
-
}
|
|
586
|
-
}
|
|
587
|
-
}
|
|
588
|
-
return tainted;
|
|
589
|
-
}
|
|
590
|
-
function extractAllAssignments(code) {
|
|
591
|
-
const assignments = [];
|
|
592
|
-
const lines = code.split('\n');
|
|
593
|
-
let assignCounter = 0;
|
|
594
|
-
for (const line of lines) {
|
|
595
|
-
const trimmed = line.trim();
|
|
596
|
-
if (!trimmed || trimmed.startsWith('//') || trimmed.startsWith('*'))
|
|
597
|
-
continue;
|
|
598
|
-
for (const assign of parseLineAssignments(trimmed, assignCounter)) {
|
|
599
|
-
assignments.push(assign);
|
|
600
|
-
assignCounter++;
|
|
601
|
-
}
|
|
602
|
-
}
|
|
603
|
-
return assignments;
|
|
604
|
-
}
|
|
605
|
-
function parseLineAssignments(line, lineNum) {
|
|
606
|
-
const assignments = [];
|
|
607
|
-
const constLetVarRegex = /^(?:const|let|var)\s+/;
|
|
608
|
-
const declMatch = line.match(constLetVarRegex);
|
|
609
|
-
if (!declMatch) {
|
|
610
|
-
const reassignRegex = /^(\w+)\s*=\s*(.+)$/;
|
|
611
|
-
const reassign = line.match(reassignRegex);
|
|
612
|
-
if (reassign) {
|
|
613
|
-
assignments.push({
|
|
614
|
-
lhs: reassign[1],
|
|
615
|
-
rhs: reassign[2],
|
|
616
|
-
assignId: `${lineNum}:reassign`,
|
|
617
|
-
});
|
|
618
|
-
}
|
|
619
|
-
return assignments;
|
|
620
|
-
}
|
|
621
|
-
const rest = line.slice(declMatch[0].length);
|
|
622
|
-
const destructRegex = /^\{\s*([^}]+)\}\s*=\s*(.+)$/;
|
|
623
|
-
const destructMatch = rest.match(destructRegex);
|
|
624
|
-
if (destructMatch) {
|
|
625
|
-
const vars = destructMatch[1].split(',').map(v => {
|
|
626
|
-
const name = v.trim().split(':')[0].split('=')[0].trim();
|
|
627
|
-
return name;
|
|
628
|
-
}).filter(v => v && !v.startsWith('...'));
|
|
629
|
-
const rhs = destructMatch[2];
|
|
630
|
-
for (let i = 0; i < vars.length; i++) {
|
|
631
|
-
assignments.push({
|
|
632
|
-
lhs: vars[i],
|
|
633
|
-
rhs: `${rhs}[${i}]`,
|
|
634
|
-
assignId: `${lineNum}:destructure:${i}`,
|
|
635
|
-
});
|
|
636
|
-
}
|
|
637
|
-
return assignments;
|
|
638
|
-
}
|
|
639
|
-
const simpleAssignRegex = /^(\w+)\s*=\s*(.+)$/;
|
|
640
|
-
const simpleMatch = rest.match(simpleAssignRegex);
|
|
641
|
-
if (simpleMatch) {
|
|
642
|
-
assignments.push({
|
|
643
|
-
lhs: simpleMatch[1],
|
|
644
|
-
rhs: simpleMatch[2],
|
|
645
|
-
assignId: `${lineNum}:simple`,
|
|
646
|
-
});
|
|
647
|
-
}
|
|
648
|
-
return assignments;
|
|
649
|
-
}
|
|
650
|
-
function extractDependencies(rhs) {
|
|
651
|
-
const deps = new Set();
|
|
652
|
-
const RESERVED = new Set(['undefined', 'null', 'true', 'false', 'const', 'let', 'var', 'new', 'typeof', 'instanceof', 'return', 'await', 'async', 'function', 'if', 'else', 'for', 'while', 'switch', 'case', 'break', 'continue', 'throw', 'try', 'catch', 'finally']);
|
|
653
|
-
// Match all identifier chains: foo, foo.bar, foo.bar.baz, foo[0].bar
|
|
654
|
-
const chainRegex = /\b([a-zA-Z_$]\w*)(?:\.\w+|\[[^\]]*\])*/g;
|
|
655
|
-
let match;
|
|
656
|
-
while ((match = chainRegex.exec(rhs)) !== null) {
|
|
657
|
-
const base = match[1];
|
|
658
|
-
if (!RESERVED.has(base) && !/^\d/.test(base)) {
|
|
659
|
-
deps.add(base);
|
|
660
|
-
}
|
|
661
|
-
}
|
|
662
|
-
return deps;
|
|
663
|
-
}
|
|
664
|
-
function isCircularAssignment(lhs, rhs, allAssignments) {
|
|
665
|
-
const rhsDeps = extractDependencies(rhs);
|
|
666
|
-
const visited = new Set();
|
|
667
|
-
const stack = [...rhsDeps];
|
|
668
|
-
while (stack.length > 0) {
|
|
669
|
-
const current = stack.pop();
|
|
670
|
-
if (current === lhs)
|
|
671
|
-
return true;
|
|
672
|
-
if (visited.has(current))
|
|
673
|
-
continue;
|
|
674
|
-
visited.add(current);
|
|
675
|
-
for (const assign of allAssignments) {
|
|
676
|
-
if (assign.lhs === current) {
|
|
677
|
-
const deps = extractDependencies(assign.rhs);
|
|
678
|
-
for (const dep of deps) {
|
|
679
|
-
if (!visited.has(dep)) {
|
|
680
|
-
stack.push(dep);
|
|
681
|
-
}
|
|
682
|
-
}
|
|
683
|
-
}
|
|
684
|
-
}
|
|
685
|
-
}
|
|
686
|
-
return false;
|
|
687
|
-
}
|
|
688
|
-
/**
|
|
689
|
-
* Propagate taint through variable assignments in handler body.
|
|
690
|
-
* Tracks: const x = req.body.foo → x is tainted.
|
|
691
|
-
* Returns all tainted variable names with their origins.
|
|
692
|
-
*/
|
|
693
|
-
function propagateTaint(code, params) {
|
|
694
|
-
const tainted = new Map();
|
|
695
|
-
for (const p of params) {
|
|
696
|
-
tainted.set(p.name, p);
|
|
697
|
-
}
|
|
698
|
-
const initialTainted = new Set(tainted.keys());
|
|
699
|
-
const propagated = propagateTaintMultiHop(code, initialTainted);
|
|
700
|
-
for (const v of propagated) {
|
|
701
|
-
if (!tainted.has(v)) {
|
|
702
|
-
tainted.set(v, { name: v, origin: `derived` });
|
|
703
|
-
}
|
|
704
|
-
}
|
|
705
|
-
return Array.from(tainted.values());
|
|
706
|
-
}
|
|
707
|
-
/**
|
|
708
|
-
* Find sink calls that use tainted variables.
|
|
709
|
-
*/
|
|
710
|
-
function findTaintedSinks(code, taintedVars) {
|
|
711
|
-
const sinks = [];
|
|
712
|
-
const taintedNames = new Set(taintedVars.map(v => v.name));
|
|
713
|
-
for (const { pattern, name, category } of SINK_PATTERNS) {
|
|
714
|
-
// Scan ALL matches using a global copy (original patterns are non-global)
|
|
715
|
-
const globalPattern = new RegExp(pattern.source, 'g');
|
|
716
|
-
let match;
|
|
717
|
-
while ((match = globalPattern.exec(code)) !== null) {
|
|
718
|
-
// Extract the argument region after the match
|
|
719
|
-
const callStart = match.index + match[0].length;
|
|
720
|
-
const parenDepth = findClosingParen(code, callStart);
|
|
721
|
-
const argText = code.slice(callStart, parenDepth);
|
|
722
|
-
// Check if any tainted variable is used in the arguments
|
|
723
|
-
for (const tName of taintedNames) {
|
|
724
|
-
if (new RegExp(`\\b${tName}\\b`).test(argText)) {
|
|
725
|
-
sinks.push({ name, category, taintedArg: tName });
|
|
726
|
-
break;
|
|
727
|
-
}
|
|
728
|
-
}
|
|
729
|
-
// Also check for template literals with tainted vars in any sink category
|
|
730
|
-
const templateMatch = argText.match(/`[^`]*\$\{(\w+)\}[^`]*`/);
|
|
731
|
-
if (templateMatch && taintedNames.has(templateMatch[1])) {
|
|
732
|
-
sinks.push({ name: `${name} (template)`, category, taintedArg: templateMatch[1] });
|
|
733
|
-
}
|
|
734
|
-
}
|
|
735
|
-
}
|
|
736
|
-
// Check template literals used in exec/spawn-like contexts
|
|
737
|
-
const templateExecRegex = /`[^`]*\$\{(\w+)\}[^`]*`/g;
|
|
738
|
-
let tm;
|
|
739
|
-
while ((tm = templateExecRegex.exec(code)) !== null) {
|
|
740
|
-
if (taintedNames.has(tm[1])) {
|
|
741
|
-
// Check if this template is used as argument to a command-like function
|
|
742
|
-
const before = code.slice(Math.max(0, tm.index - 50), tm.index);
|
|
743
|
-
if (/exec\s*\(|spawn\s*\(|execSync\s*\(/.test(before)) {
|
|
744
|
-
// Already caught by SINK_PATTERNS, skip duplicate
|
|
745
|
-
continue;
|
|
746
|
-
}
|
|
747
|
-
}
|
|
748
|
-
}
|
|
749
|
-
return sinks;
|
|
750
|
-
}
|
|
751
|
-
/**
|
|
752
|
-
* Build taint paths and check for sanitizers between source and sink.
|
|
753
|
-
*/
|
|
754
|
-
function buildPaths(code, taintedVars, sinks) {
|
|
755
|
-
const paths = [];
|
|
756
|
-
const foundSanitizers = detectSanitizers(code);
|
|
757
|
-
for (const sink of sinks) {
|
|
758
|
-
// Find the source that produced this tainted arg
|
|
759
|
-
const source = taintedVars.find(v => v.name === sink.taintedArg);
|
|
760
|
-
if (!source)
|
|
761
|
-
continue;
|
|
762
|
-
// Check if any sanitizer was applied to this specific variable
|
|
763
|
-
const sanitizer = foundSanitizers.find(s => new RegExp(`\\b${sink.taintedArg}\\b`).test(s.context) ||
|
|
764
|
-
new RegExp(`${s.name}\\s*\\([^)]*\\b${sink.taintedArg}\\b`).test(code));
|
|
765
|
-
// Check sanitizer sufficiency — is this the RIGHT sanitizer for this sink?
|
|
766
|
-
const hasSanitizer = sanitizer != null;
|
|
767
|
-
const sufficient = sanitizer != null ? isSanitizerSufficient(sanitizer.name, sink.category) : false;
|
|
768
|
-
paths.push({
|
|
769
|
-
source,
|
|
770
|
-
sink,
|
|
771
|
-
sanitized: hasSanitizer && sufficient,
|
|
772
|
-
sanitizer: sanitizer?.name,
|
|
773
|
-
insufficientSanitizer: hasSanitizer && !sufficient ? sanitizer.name : undefined,
|
|
774
|
-
});
|
|
775
|
-
}
|
|
776
|
-
return paths;
|
|
777
|
-
}
|
|
778
|
-
function detectSanitizers(code) {
|
|
779
|
-
const found = [];
|
|
780
|
-
for (const { pattern, name } of SANITIZER_PATTERNS) {
|
|
781
|
-
const globalPattern = new RegExp(pattern.source, 'g');
|
|
782
|
-
let match;
|
|
783
|
-
while ((match = globalPattern.exec(code)) !== null) {
|
|
784
|
-
const start = Math.max(0, match.index - 50);
|
|
785
|
-
const end = Math.min(code.length, match.index + match[0].length + 50);
|
|
786
|
-
found.push({ name, context: code.slice(start, end) });
|
|
787
|
-
}
|
|
788
|
-
}
|
|
789
|
-
return found;
|
|
790
|
-
}
|
|
791
|
-
function findClosingParen(code, start) {
|
|
792
|
-
let depth = 1;
|
|
793
|
-
for (let i = start; i < code.length; i++) {
|
|
794
|
-
if (code[i] === '(')
|
|
795
|
-
depth++;
|
|
796
|
-
if (code[i] === ')') {
|
|
797
|
-
depth--;
|
|
798
|
-
if (depth === 0)
|
|
799
|
-
return i;
|
|
800
|
-
}
|
|
801
|
-
}
|
|
802
|
-
return Math.min(start + 500, code.length); // fallback
|
|
803
|
-
}
|
|
804
|
-
// ── Finding Generator ────────────────────────────────────────────────────
|
|
805
|
-
/**
|
|
806
|
-
* Convert taint results into ReviewFinding[] for the unified pipeline.
|
|
807
|
-
*/
|
|
808
|
-
export function taintToFindings(results) {
|
|
809
|
-
const findings = [];
|
|
810
|
-
const categoryLabels = {
|
|
811
|
-
command: 'command injection',
|
|
812
|
-
fs: 'path traversal / file write',
|
|
813
|
-
sql: 'SQL injection',
|
|
814
|
-
redirect: 'open redirect',
|
|
815
|
-
eval: 'code injection',
|
|
816
|
-
template: 'template injection',
|
|
817
|
-
};
|
|
818
|
-
for (const r of results) {
|
|
819
|
-
// Report unsanitized paths AND insufficient sanitizer paths
|
|
820
|
-
const reportable = r.paths.filter(p => !p.sanitized);
|
|
821
|
-
if (reportable.length === 0)
|
|
822
|
-
continue;
|
|
823
|
-
for (const path of reportable) {
|
|
824
|
-
const severity = path.sink.category === 'command' || path.sink.category === 'eval'
|
|
825
|
-
? 'error'
|
|
826
|
-
: 'warning';
|
|
827
|
-
const primarySpan = {
|
|
828
|
-
file: r.filePath,
|
|
829
|
-
startLine: r.startLine,
|
|
830
|
-
startCol: 1,
|
|
831
|
-
endLine: r.startLine,
|
|
832
|
-
endCol: 1,
|
|
833
|
-
};
|
|
834
|
-
if (path.insufficientSanitizer) {
|
|
835
|
-
// Sanitizer present but wrong for this sink type
|
|
836
|
-
findings.push({
|
|
837
|
-
source: 'kern',
|
|
838
|
-
ruleId: `taint-insufficient-sanitizer`,
|
|
839
|
-
severity,
|
|
840
|
-
category: 'bug',
|
|
841
|
-
message: `Insufficient sanitizer: '${path.insufficientSanitizer}' does not protect against ${categoryLabels[path.sink.category]}. ` +
|
|
842
|
-
`${path.source.origin} → ${path.sink.name}() is still exploitable.`,
|
|
843
|
-
primarySpan,
|
|
844
|
-
suggestion: `${path.insufficientSanitizer} is not sufficient for ${path.sink.category} sinks. ${getSuggestion(path.sink.category)}`,
|
|
845
|
-
fingerprint: createFingerprint(`taint-insufficient`, r.startLine, 1),
|
|
846
|
-
});
|
|
847
|
-
}
|
|
848
|
-
else {
|
|
849
|
-
// No sanitizer at all
|
|
850
|
-
findings.push({
|
|
851
|
-
source: 'kern',
|
|
852
|
-
ruleId: `taint-${path.sink.category}`,
|
|
853
|
-
severity,
|
|
854
|
-
category: 'bug',
|
|
855
|
-
message: `Taint flow: ${path.source.origin} → ${path.sink.name}() — potential ${categoryLabels[path.sink.category]}. ` +
|
|
856
|
-
`Variable '${path.sink.taintedArg}' reaches dangerous sink without sanitization.`,
|
|
857
|
-
primarySpan,
|
|
858
|
-
suggestion: getSuggestion(path.sink.category),
|
|
859
|
-
fingerprint: createFingerprint(`taint-${path.sink.category}`, r.startLine, 1),
|
|
860
|
-
});
|
|
861
|
-
}
|
|
862
|
-
}
|
|
863
|
-
}
|
|
864
|
-
return findings;
|
|
865
|
-
}
|
|
866
|
-
function getSuggestion(category) {
|
|
867
|
-
switch (category) {
|
|
868
|
-
case 'command': return 'Use spawn() with array arguments, or validate/escape input before passing to exec()';
|
|
869
|
-
case 'fs': return 'Use path.resolve() + path.normalize() and verify the result stays within allowed directory';
|
|
870
|
-
case 'sql': return 'Use parameterized queries ($1, ?) instead of string interpolation';
|
|
871
|
-
case 'redirect': return 'Validate redirect URL against an allowlist of safe destinations';
|
|
872
|
-
case 'eval': return 'Never pass user input to eval() or new Function() — use safe alternatives';
|
|
873
|
-
case 'template': return 'Sanitize user input before embedding in templates';
|
|
874
|
-
}
|
|
875
|
-
}
|
|
876
|
-
// ── Cross-File Taint Analysis ────────────────────────────────────────────
|
|
877
|
-
/**
|
|
878
|
-
* Build a map of exported functions across all files.
|
|
879
|
-
* Maps "filePath::fnName" → ExportedFunction with sink info.
|
|
880
|
-
*/
|
|
881
|
-
export function buildExportMap(inferredPerFile) {
|
|
882
|
-
const exportMap = new Map();
|
|
883
|
-
for (const [filePath, inferred] of inferredPerFile) {
|
|
884
|
-
for (const r of inferred) {
|
|
885
|
-
if (r.node.type !== 'fn')
|
|
886
|
-
continue;
|
|
887
|
-
const fnName = r.node.props?.name || '';
|
|
888
|
-
if (!fnName)
|
|
889
|
-
continue;
|
|
890
|
-
// Check if function is exported (absence of export='false' means exported)
|
|
891
|
-
const isExported = r.node.props?.export !== 'false';
|
|
892
|
-
if (!isExported)
|
|
893
|
-
continue;
|
|
894
|
-
const params = r.node.props?.params || '';
|
|
895
|
-
const handler = r.node.children?.find(c => c.type === 'handler');
|
|
896
|
-
const code = handler?.props?.code || '';
|
|
897
|
-
// Check if the function body contains dangerous sinks
|
|
898
|
-
const sinks = [];
|
|
899
|
-
if (code) {
|
|
900
|
-
const dummyTaint = [];
|
|
901
|
-
// Parse params to get variable names for sink detection
|
|
902
|
-
const paramNames = params.split(',').map(p => p.trim().split(':')[0]?.trim()).filter(Boolean);
|
|
903
|
-
for (const name of paramNames) {
|
|
904
|
-
dummyTaint.push({ name, origin: `param:${name}` });
|
|
905
|
-
}
|
|
906
|
-
if (dummyTaint.length > 0) {
|
|
907
|
-
sinks.push(...findTaintedSinks(code, dummyTaint));
|
|
908
|
-
}
|
|
909
|
-
}
|
|
910
|
-
exportMap.set(`${filePath}::${fnName}`, {
|
|
911
|
-
filePath,
|
|
912
|
-
fnName,
|
|
913
|
-
params,
|
|
914
|
-
hasSink: sinks.length > 0,
|
|
915
|
-
sinks,
|
|
916
|
-
});
|
|
917
|
-
}
|
|
918
|
-
}
|
|
919
|
-
return exportMap;
|
|
920
|
-
}
|
|
921
|
-
/**
|
|
922
|
-
* Build import→function resolution map.
|
|
923
|
-
* Maps "importingFile::importedName" → absolute file path of the definition.
|
|
924
|
-
*/
|
|
925
|
-
export function buildImportMap(inferredPerFile, graphImports) {
|
|
926
|
-
const importMap = new Map();
|
|
927
|
-
for (const [filePath, inferred] of inferredPerFile) {
|
|
928
|
-
const resolvedImports = graphImports.get(filePath) || [];
|
|
929
|
-
for (const r of inferred) {
|
|
930
|
-
if (r.node.type !== 'import')
|
|
931
|
-
continue;
|
|
932
|
-
const from = r.node.props?.from || '';
|
|
933
|
-
const names = r.node.props?.names || '';
|
|
934
|
-
const defaultImport = r.node.props?.default || '';
|
|
935
|
-
if (!from)
|
|
936
|
-
continue;
|
|
937
|
-
// Find the resolved path for this import specifier
|
|
938
|
-
const resolvedPath = resolvedImports.find(p => p.includes(from.replace(/^\.\//, '').replace(/\.(js|ts|tsx)$/, '')));
|
|
939
|
-
if (!resolvedPath)
|
|
940
|
-
continue;
|
|
941
|
-
// Map each imported name to its resolved file
|
|
942
|
-
if (names) {
|
|
943
|
-
for (const name of names.split(',').map(n => n.trim())) {
|
|
944
|
-
if (name)
|
|
945
|
-
importMap.set(`${filePath}::${name}`, resolvedPath);
|
|
946
|
-
}
|
|
947
|
-
}
|
|
948
|
-
if (defaultImport) {
|
|
949
|
-
importMap.set(`${filePath}::${defaultImport}`, resolvedPath);
|
|
950
|
-
}
|
|
951
|
-
}
|
|
952
|
-
}
|
|
953
|
-
return importMap;
|
|
954
|
-
}
|
|
955
|
-
/**
|
|
956
|
-
* Cross-file taint analysis.
|
|
957
|
-
*
|
|
958
|
-
* For each handler function with tainted params:
|
|
959
|
-
* 1. Find calls to imported functions in the handler body
|
|
960
|
-
* 2. Check if tainted data is passed as an argument
|
|
961
|
-
* 3. Look up the target function — does it have a dangerous sink?
|
|
962
|
-
* 4. If yes and no sanitizer in between → cross-file taint path
|
|
963
|
-
*/
|
|
964
|
-
export function analyzeTaintCrossFile(inferredPerFile, graphImports) {
|
|
965
|
-
const exportMap = buildExportMap(inferredPerFile);
|
|
966
|
-
const importMap = buildImportMap(inferredPerFile, graphImports);
|
|
967
|
-
const results = [];
|
|
968
|
-
for (const [filePath, inferred] of inferredPerFile) {
|
|
969
|
-
for (const r of inferred) {
|
|
970
|
-
if (r.node.type !== 'fn')
|
|
971
|
-
continue;
|
|
972
|
-
const fnName = r.node.props?.name || 'anonymous';
|
|
973
|
-
const paramsStr = r.node.props?.params || '';
|
|
974
|
-
const handler = r.node.children?.find(c => c.type === 'handler');
|
|
975
|
-
const code = handler?.props?.code || '';
|
|
976
|
-
if (!code)
|
|
977
|
-
continue;
|
|
978
|
-
// Only analyze functions with tainted params
|
|
979
|
-
const taintedParams = classifyParams(paramsStr);
|
|
980
|
-
if (taintedParams.length === 0)
|
|
981
|
-
continue;
|
|
982
|
-
const taintedVars = propagateTaint(code, taintedParams);
|
|
983
|
-
const taintedNames = new Set(taintedVars.map(v => v.name));
|
|
984
|
-
// Find calls to imported functions: importedFn(taintedVar)
|
|
985
|
-
const callRegex = /\b(\w+)\s*\(/g;
|
|
986
|
-
let callMatch;
|
|
987
|
-
while ((callMatch = callRegex.exec(code)) !== null) {
|
|
988
|
-
const calledFn = callMatch[0].replace(/\s*\($/, '');
|
|
989
|
-
// Is this an imported function?
|
|
990
|
-
const resolvedFile = importMap.get(`${filePath}::${calledFn}`);
|
|
991
|
-
if (!resolvedFile)
|
|
992
|
-
continue;
|
|
993
|
-
// Does the target have dangerous sinks?
|
|
994
|
-
const targetFn = exportMap.get(`${resolvedFile}::${calledFn}`);
|
|
995
|
-
if (!targetFn || !targetFn.hasSink)
|
|
996
|
-
continue;
|
|
997
|
-
// Extract arguments passed to this call
|
|
998
|
-
const callStart = callMatch.index + callMatch[0].length;
|
|
999
|
-
const parenEnd = findClosingParen(code, callStart);
|
|
1000
|
-
const argText = code.slice(callStart, parenEnd);
|
|
1001
|
-
// Check if any tainted variable is passed as argument
|
|
1002
|
-
const taintedArgs = [];
|
|
1003
|
-
for (const tName of taintedNames) {
|
|
1004
|
-
if (new RegExp(`\\b${tName}\\b`).test(argText)) {
|
|
1005
|
-
taintedArgs.push(tName);
|
|
1006
|
-
}
|
|
1007
|
-
}
|
|
1008
|
-
if (taintedArgs.length === 0)
|
|
1009
|
-
continue;
|
|
1010
|
-
// Check for sanitizers between the taint and the call
|
|
1011
|
-
const beforeCall = code.slice(0, callMatch.index);
|
|
1012
|
-
const foundSanitizers = detectSanitizers(beforeCall);
|
|
1013
|
-
const hasSanitizer = taintedArgs.some(arg => foundSanitizers.some(s => new RegExp(`\\b${arg}\\b`).test(s.context)));
|
|
1014
|
-
if (hasSanitizer)
|
|
1015
|
-
continue; // Sanitized before passing to callee
|
|
1016
|
-
// Found cross-file taint path
|
|
1017
|
-
for (const sink of targetFn.sinks) {
|
|
1018
|
-
const source = taintedVars.find(v => taintedArgs.includes(v.name));
|
|
1019
|
-
if (!source)
|
|
1020
|
-
continue;
|
|
1021
|
-
results.push({
|
|
1022
|
-
callerFile: filePath,
|
|
1023
|
-
callerFn: fnName,
|
|
1024
|
-
callerLine: r.startLine,
|
|
1025
|
-
calleeFile: resolvedFile,
|
|
1026
|
-
calleeFn: calledFn,
|
|
1027
|
-
taintedArgs,
|
|
1028
|
-
sinkInCallee: sink,
|
|
1029
|
-
source,
|
|
1030
|
-
});
|
|
1031
|
-
}
|
|
1032
|
-
}
|
|
1033
|
-
}
|
|
1034
|
-
}
|
|
1035
|
-
return results;
|
|
1036
|
-
}
|
|
1037
|
-
/**
|
|
1038
|
-
* Convert cross-file taint results into ReviewFinding[].
|
|
1039
|
-
*/
|
|
1040
|
-
export function crossFileTaintToFindings(results) {
|
|
1041
|
-
const findings = [];
|
|
1042
|
-
const categoryLabels = {
|
|
1043
|
-
command: 'command injection',
|
|
1044
|
-
fs: 'path traversal / file write',
|
|
1045
|
-
sql: 'SQL injection',
|
|
1046
|
-
redirect: 'open redirect',
|
|
1047
|
-
eval: 'code injection',
|
|
1048
|
-
template: 'template injection',
|
|
1049
|
-
};
|
|
1050
|
-
for (const r of results) {
|
|
1051
|
-
const severity = r.sinkInCallee.category === 'command' || r.sinkInCallee.category === 'eval'
|
|
1052
|
-
? 'error'
|
|
1053
|
-
: 'warning';
|
|
1054
|
-
findings.push({
|
|
1055
|
-
source: 'kern',
|
|
1056
|
-
ruleId: `taint-crossfile-${r.sinkInCallee.category}`,
|
|
1057
|
-
severity,
|
|
1058
|
-
category: 'bug',
|
|
1059
|
-
message: `Cross-file taint: ${r.source.origin} in ${r.callerFn}() → ${r.calleeFn}() → ${r.sinkInCallee.name}(). ` +
|
|
1060
|
-
`Tainted data crosses file boundary to reach ${categoryLabels[r.sinkInCallee.category]} sink.`,
|
|
1061
|
-
primarySpan: {
|
|
1062
|
-
file: r.callerFile,
|
|
1063
|
-
startLine: r.callerLine,
|
|
1064
|
-
startCol: 1,
|
|
1065
|
-
endLine: r.callerLine,
|
|
1066
|
-
endCol: 1,
|
|
1067
|
-
},
|
|
1068
|
-
relatedSpans: [{
|
|
1069
|
-
file: r.calleeFile,
|
|
1070
|
-
startLine: 1,
|
|
1071
|
-
startCol: 1,
|
|
1072
|
-
endLine: 1,
|
|
1073
|
-
endCol: 1,
|
|
1074
|
-
}],
|
|
1075
|
-
suggestion: `Validate '${r.taintedArgs.join(', ')}' before passing to ${r.calleeFn}(). ${getSuggestion(r.sinkInCallee.category)}`,
|
|
1076
|
-
fingerprint: createFingerprint(`taint-xfile-${r.sinkInCallee.category}`, r.callerLine, 1),
|
|
1077
|
-
});
|
|
1078
|
-
}
|
|
1079
|
-
return findings;
|
|
1080
|
-
}
|
|
1081
30
|
//# sourceMappingURL=taint.js.map
|