@clear-capabilities/agentic-security-scanner 0.77.0 → 0.78.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/bin/.agentic-security/findings.json +1907 -0
- package/bin/.agentic-security/last-scan.json +1907 -0
- package/bin/.agentic-security/last-scan.json.sig +1 -0
- package/bin/.agentic-security/scan-history.json +115 -0
- package/bin/.agentic-security/streak.json +20 -0
- package/bin/agentic-security.js +33 -2
- package/dist/178.index.js +1 -1
- package/dist/384.index.js +1 -1
- package/dist/637.index.js +1 -1
- package/dist/718.index.js +106 -0
- package/dist/824.index.js +126 -0
- package/dist/838.index.js +1 -1
- package/dist/agentic-security.mjs +32 -32
- package/dist/agentic-security.mjs.sha256 +1 -1
- package/package.json +3 -3
- package/src/.agentic-security/findings.json +82642 -0
- package/src/.agentic-security/last-scan.json +82642 -0
- package/src/.agentic-security/last-scan.json.sig +1 -0
- package/src/.agentic-security/scan-history.json +10054 -0
- package/src/.agentic-security/streak.json +21 -0
- package/src/dataflow/.agentic-security/findings.json +3515 -0
- package/src/dataflow/.agentic-security/last-scan.json +3515 -0
- package/src/dataflow/.agentic-security/last-scan.json.sig +1 -0
- package/src/dataflow/.agentic-security/scan-history.json +702 -0
- package/src/dataflow/.agentic-security/streak.json +22 -0
- package/src/dataflow/async-sequencing.js +16 -7
- package/src/dataflow/builtin-summaries.js +131 -0
- package/src/dataflow/catalog.js +107 -0
- package/src/dataflow/cross-repo.js +75 -1
- package/src/dataflow/engine.js +129 -0
- package/src/dataflow/implicit-flow.js +24 -6
- package/src/dataflow/stub-aware-filter.js +69 -11
- package/src/dataflow/summaries.js +28 -3
- package/src/engine-parallel.js +70 -0
- package/src/engine.js +165 -15
- package/src/ir/.agentic-security/findings.json +3777 -0
- package/src/ir/.agentic-security/last-scan.json +3777 -0
- package/src/ir/.agentic-security/last-scan.json.sig +1 -0
- package/src/ir/.agentic-security/scan-history.json +771 -0
- package/src/ir/.agentic-security/streak.json +21 -0
- package/src/ir/index.js +22 -1
- package/src/ir/parser-go.js +403 -0
- package/src/ir/parser-js.js +2 -0
- package/src/ir/parser-php.js +330 -0
- package/src/ir/parser-py.helper.py +137 -11
- package/src/ir/parser-rb.js +309 -0
- package/src/posture/.agentic-security/findings.json +51562 -0
- package/src/posture/.agentic-security/last-scan.json +51562 -0
- package/src/posture/.agentic-security/last-scan.json.sig +1 -0
- package/src/posture/.agentic-security/scan-history.json +650 -0
- package/src/posture/.agentic-security/streak.json +20 -0
- package/src/posture/calibration.js +14 -0
- package/src/posture/triage.js +13 -0
- package/src/report/.agentic-security/findings.json +80 -0
- package/src/report/.agentic-security/last-scan.json +80 -0
- package/src/report/.agentic-security/last-scan.json.sig +1 -0
- package/src/report/.agentic-security/scan-history.json +35 -0
- package/src/report/.agentic-security/streak.json +22 -0
- package/src/report/index.js +23 -2
- package/src/sast/.agentic-security/findings.json +5190 -0
- package/src/sast/.agentic-security/last-scan.json +5190 -0
- package/src/sast/.agentic-security/last-scan.json.sig +1 -0
- package/src/sast/.agentic-security/scan-history.json +408 -0
- package/src/sast/.agentic-security/streak.json +20 -0
- package/src/sast/cache-poisoning.js +77 -0
- package/src/sast/comparison-safety.js +73 -0
- package/src/sast/db-taint.js +54 -0
- package/src/sast/graphql.js +127 -0
- package/src/sast/llm-stored-prompt.js +57 -0
- package/src/sast/mutation-xss.js +43 -0
- package/src/sast/nosql-injection.js +5 -0
- package/src/sast/null-byte-injection.js +76 -0
- package/src/sast/redos-nfa.js +338 -0
- package/src/sast/sensitive-data-logging.js +73 -0
- package/src/sast/weak-password-hash.js +77 -0
- package/src/sast/weak-randomness.js +100 -0
- package/src/sca/.agentic-security/findings.json +1587 -0
- package/src/sca/.agentic-security/last-scan.json +1587 -0
- package/src/sca/.agentic-security/last-scan.json.sig +1 -0
- package/src/sca/.agentic-security/scan-history.json +36 -0
- package/src/sca/.agentic-security/streak.json +21 -0
- package/src/sca/llm-function-extract.js +107 -0
- package/src/sca/vendor-detect.js +91 -0
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
{
|
|
2
|
+
"firstScanDate": "2026-05-26T15:54:30.269Z",
|
|
3
|
+
"lastScanDate": "2026-05-27T09:30:02.400Z",
|
|
4
|
+
"totalScans": 28,
|
|
5
|
+
"daysCleanCritical": 2,
|
|
6
|
+
"lastCleanDate": "2026-05-27",
|
|
7
|
+
"lastCriticalDate": null,
|
|
8
|
+
"hasEverHadCritical": false,
|
|
9
|
+
"bestDaysCleanCritical": 2,
|
|
10
|
+
"totalFindingsAtFirstScan": 17,
|
|
11
|
+
"totalFindingsAtLastScan": 17,
|
|
12
|
+
"totalFixesInferred": 0,
|
|
13
|
+
"lastGrade": "A",
|
|
14
|
+
"bestGrade": "A",
|
|
15
|
+
"launchCheckPassedAt": null,
|
|
16
|
+
"achievements": [
|
|
17
|
+
"first-scan",
|
|
18
|
+
"grade-a",
|
|
19
|
+
"scan-veteran-25"
|
|
20
|
+
],
|
|
21
|
+
"previousGrade": "A"
|
|
22
|
+
}
|
|
@@ -55,22 +55,24 @@ const ASYNC_ITER_BODY_SOURCES = new Set([
|
|
|
55
55
|
* AST shape expected (parser-js.js neutral):
|
|
56
56
|
* { kind: 'call', callee: { kind: 'member', object: <expr>, prop: <string> }, args: [...] }
|
|
57
57
|
*/
|
|
58
|
-
export function describeChain(callExpr) {
|
|
58
|
+
export function describeChain(callExpr, opts = {}) {
|
|
59
59
|
if (!callExpr || callExpr.kind !== 'call') return null;
|
|
60
60
|
const ops = [];
|
|
61
61
|
let cur = callExpr;
|
|
62
|
-
// Walk leftward through .then/.catch/.finally chain.
|
|
63
62
|
while (cur && cur.kind === 'call' && cur.callee && cur.callee.kind === 'member' && PROMISE_CHAIN_METHODS.has(cur.callee.prop)) {
|
|
64
63
|
const arg = (cur.args || [])[0];
|
|
65
64
|
ops.unshift({
|
|
66
|
-
kind: cur.callee.prop,
|
|
65
|
+
kind: cur.callee.prop,
|
|
67
66
|
callback: arg && (arg.kind === 'ident' || arg.kind === 'arrow' || arg.kind === 'function') ? arg : null,
|
|
68
67
|
argIndex: 0,
|
|
69
68
|
});
|
|
70
69
|
cur = cur.callee.object;
|
|
71
70
|
}
|
|
72
|
-
|
|
73
|
-
|
|
71
|
+
let isPromise = isPromiseRoot(cur);
|
|
72
|
+
if (!isPromise && opts.summaryCache && opts.callGraph && cur && cur.kind === 'call') {
|
|
73
|
+
const name = typeof cur.callee === 'string' ? cur.callee : (cur.callee?.name || null);
|
|
74
|
+
if (name) isPromise = isAsyncSourceFromSummary(name, opts.summaryCache, opts.callGraph);
|
|
75
|
+
}
|
|
74
76
|
return { ops, rootCallee: cur, isPromise };
|
|
75
77
|
}
|
|
76
78
|
|
|
@@ -84,13 +86,20 @@ function isPromiseRoot(expr) {
|
|
|
84
86
|
}
|
|
85
87
|
if (c.kind === 'member') {
|
|
86
88
|
if (c.object && c.object.kind === 'ident' && c.object.name === 'Promise' && PROMISE_STATIC_METHODS.has(c.prop)) return true;
|
|
87
|
-
// any .xxxAsync() pattern, or .then-chainable: too noisy to assume; require
|
|
88
|
-
// an explicit await elsewhere or a known callee.
|
|
89
89
|
return /Async$/.test(c.prop) || /^(fetch|json|text|blob|formData)$/.test(c.prop);
|
|
90
90
|
}
|
|
91
91
|
return false;
|
|
92
92
|
}
|
|
93
93
|
|
|
94
|
+
export function isAsyncSourceFromSummary(calleeName, summaryCache, callGraph) {
|
|
95
|
+
if (!calleeName || !summaryCache || !callGraph) return false;
|
|
96
|
+
const resolved = callGraph.resolve ? callGraph.resolve(calleeName) : null;
|
|
97
|
+
const qid = resolved && (resolved.qid || resolved);
|
|
98
|
+
if (typeof qid !== 'string') return false;
|
|
99
|
+
const sum = summaryCache.get(qid, new Set());
|
|
100
|
+
return !!(sum && sum.returnTainted);
|
|
101
|
+
}
|
|
102
|
+
|
|
94
103
|
/**
|
|
95
104
|
* Given a chain descriptor + a `sourceTainted` boolean indicating whether
|
|
96
105
|
* the root callee's resolved value is tainted, return whether each callback
|
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
// Pre-computed taint summaries for popular npm/pip packages.
|
|
2
|
+
//
|
|
3
|
+
// When the taint engine encounters a call to an external function that
|
|
4
|
+
// the call graph can't resolve (e.g., lodash.merge from node_modules),
|
|
5
|
+
// it checks this registry as a fallback. Each entry describes whether
|
|
6
|
+
// the function's return value carries taint and whether any parameters
|
|
7
|
+
// are mutated (tainted by reference).
|
|
8
|
+
//
|
|
9
|
+
// Format: same as SummaryCache entries.
|
|
10
|
+
// { returnTainted: bool, mutatedParams: Set<paramIndex-as-string> }
|
|
11
|
+
//
|
|
12
|
+
// Convention: param indices are STRING keys ('0', '1', ...) because
|
|
13
|
+
// SummaryCache uses param names, and for external functions we don't
|
|
14
|
+
// know names — we use positional indices instead.
|
|
15
|
+
|
|
16
|
+
const S = (returnTainted, mutatedIndices = []) => ({
|
|
17
|
+
returnTainted,
|
|
18
|
+
mutatedParams: new Set(mutatedIndices.map(String)),
|
|
19
|
+
taintedGlobals: new Set(),
|
|
20
|
+
findings: [],
|
|
21
|
+
_builtin: true,
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
export const BUILTIN_SUMMARIES = new Map([
|
|
25
|
+
// ── Lodash ──────────────────────────────────────────────────────────────
|
|
26
|
+
['_.merge', S(true, [0])],
|
|
27
|
+
['_.defaultsDeep', S(true, [0])],
|
|
28
|
+
['_.defaults', S(true, [0])],
|
|
29
|
+
['_.extend', S(true, [0])],
|
|
30
|
+
['_.assign', S(true, [0])],
|
|
31
|
+
['_.assignIn', S(true, [0])],
|
|
32
|
+
['_.set', S(false, [0])],
|
|
33
|
+
['_.get', S(true)],
|
|
34
|
+
['_.pick', S(true)],
|
|
35
|
+
['_.omit', S(true)],
|
|
36
|
+
['_.cloneDeep', S(true)],
|
|
37
|
+
['_.clone', S(true)],
|
|
38
|
+
['_.map', S(true)],
|
|
39
|
+
['_.filter', S(true)],
|
|
40
|
+
['_.find', S(true)],
|
|
41
|
+
['_.reduce', S(true)],
|
|
42
|
+
['_.flatten', S(true)],
|
|
43
|
+
['_.compact', S(true)],
|
|
44
|
+
['_.uniq', S(true)],
|
|
45
|
+
['_.groupBy', S(true)],
|
|
46
|
+
['_.keyBy', S(true)],
|
|
47
|
+
['_.values', S(true)],
|
|
48
|
+
['_.keys', S(false)],
|
|
49
|
+
['_.identity', S(true)],
|
|
50
|
+
|
|
51
|
+
// ── Node.js core ────────────────────────────────────────────────────────
|
|
52
|
+
['JSON.parse', S(true)],
|
|
53
|
+
['JSON.stringify', S(true)],
|
|
54
|
+
['Buffer.from', S(true)],
|
|
55
|
+
['Buffer.concat', S(true)],
|
|
56
|
+
['querystring.parse', S(true)],
|
|
57
|
+
['url.parse', S(true)],
|
|
58
|
+
['path.join', S(true)],
|
|
59
|
+
['path.resolve', S(true)],
|
|
60
|
+
['util.format', S(true)],
|
|
61
|
+
|
|
62
|
+
// ── Express / HTTP ──────────────────────────────────────────────────────
|
|
63
|
+
['express.json', S(false)],
|
|
64
|
+
['express.urlencoded',S(false)],
|
|
65
|
+
['bodyParser.json', S(false)],
|
|
66
|
+
['cors', S(false)],
|
|
67
|
+
|
|
68
|
+
// ── Database clients ────────────────────────────────────────────────────
|
|
69
|
+
['pool.query', S(true)],
|
|
70
|
+
['client.query', S(true)],
|
|
71
|
+
['db.query', S(true)],
|
|
72
|
+
['db.all', S(true)],
|
|
73
|
+
['db.get', S(true)],
|
|
74
|
+
['db.run', S(false)],
|
|
75
|
+
['knex.raw', S(true)],
|
|
76
|
+
['knex.select', S(true)],
|
|
77
|
+
|
|
78
|
+
// ── HTTP clients ────────────────────────────────────────────────────────
|
|
79
|
+
['axios.get', S(true)],
|
|
80
|
+
['axios.post', S(true)],
|
|
81
|
+
['axios.put', S(true)],
|
|
82
|
+
['axios.patch', S(true)],
|
|
83
|
+
['axios.delete', S(true)],
|
|
84
|
+
['axios.request', S(true)],
|
|
85
|
+
['fetch', S(true)],
|
|
86
|
+
['got', S(true)],
|
|
87
|
+
['got.get', S(true)],
|
|
88
|
+
['got.post', S(true)],
|
|
89
|
+
['superagent.get', S(true)],
|
|
90
|
+
['superagent.post', S(true)],
|
|
91
|
+
|
|
92
|
+
// ── Crypto / hashing (return is derived, not tainted) ───────────────────
|
|
93
|
+
['crypto.createHash', S(false)],
|
|
94
|
+
['crypto.randomBytes',S(false)],
|
|
95
|
+
['bcrypt.hash', S(false)],
|
|
96
|
+
['bcrypt.compare', S(false)],
|
|
97
|
+
|
|
98
|
+
// ── Sanitizers (return is clean) ────────────────────────────────────────
|
|
99
|
+
['parseInt', S(false)],
|
|
100
|
+
['parseFloat', S(false)],
|
|
101
|
+
['Number', S(false)],
|
|
102
|
+
['Boolean', S(false)],
|
|
103
|
+
['encodeURIComponent',S(false)],
|
|
104
|
+
['encodeURI', S(false)],
|
|
105
|
+
['DOMPurify.sanitize',S(false)],
|
|
106
|
+
['validator.escape', S(false)],
|
|
107
|
+
['he.encode', S(false)],
|
|
108
|
+
|
|
109
|
+
// ── Python stdlib (matched by callee name) ──────────────────────────────
|
|
110
|
+
['json.loads', S(true)],
|
|
111
|
+
['json.dumps', S(true)],
|
|
112
|
+
['int', S(false)],
|
|
113
|
+
['float', S(false)],
|
|
114
|
+
['str', S(true)],
|
|
115
|
+
['shlex.quote', S(false)],
|
|
116
|
+
['html.escape', S(false)],
|
|
117
|
+
['bleach.clean', S(false)],
|
|
118
|
+
]);
|
|
119
|
+
|
|
120
|
+
export function lookupBuiltinSummary(calleeName) {
|
|
121
|
+
if (!calleeName || typeof calleeName !== 'string') return null;
|
|
122
|
+
const direct = BUILTIN_SUMMARIES.get(calleeName);
|
|
123
|
+
if (direct) return direct;
|
|
124
|
+
const lastDot = calleeName.lastIndexOf('.');
|
|
125
|
+
if (lastDot > 0) {
|
|
126
|
+
const short = calleeName.slice(lastDot + 1);
|
|
127
|
+
const fallback = BUILTIN_SUMMARIES.get(short);
|
|
128
|
+
if (fallback) return null;
|
|
129
|
+
}
|
|
130
|
+
return null;
|
|
131
|
+
}
|
package/src/dataflow/catalog.js
CHANGED
|
@@ -139,12 +139,32 @@ export const CATALOG = [
|
|
|
139
139
|
{ kind: 'source', id: 'go-gin-query', language: 'go', framework: 'gin', match: { type: 'call', callee: 'Query' }, label: 'c.Query (gin)' },
|
|
140
140
|
{ kind: 'source', id: 'go-gin-bindjson',language:'go', framework: 'gin', match: { type: 'call', callee: 'BindJSON' }, label: 'c.BindJSON (gin)' },
|
|
141
141
|
{ kind: 'source', id: 'go-echo-param', language: 'go', framework: 'echo', match: { type: 'call', callee: 'Param' }, label: 'c.Param (echo)' },
|
|
142
|
+
{ kind: 'source', id: 'go-gin-postform', language: 'go', framework: 'gin', match: { type: 'call', callee: 'PostForm' }, label: 'c.PostForm (gin)' },
|
|
143
|
+
{ kind: 'source', id: 'go-gin-shouldbind',language: 'go', framework: 'gin', match: { type: 'call', callee: 'ShouldBind' }, label: 'c.ShouldBind (gin)' },
|
|
144
|
+
{ kind: 'source', id: 'go-gin-shouldbindjson',language:'go',framework:'gin', match: { type: 'call', callee: 'ShouldBindJSON' },label: 'c.ShouldBindJSON (gin)' },
|
|
145
|
+
{ kind: 'source', id: 'go-echo-formvalue',language: 'go', framework: 'echo', match: { type: 'call', callee: 'FormValue' }, label: 'c.FormValue (echo)' },
|
|
146
|
+
{ kind: 'source', id: 'go-echo-queryparam',language:'go', framework: 'echo', match: { type: 'call', callee: 'QueryParam' }, label: 'c.QueryParam (echo)' },
|
|
147
|
+
{ kind: 'source', id: 'go-echo-bind', language: 'go', framework: 'echo', match: { type: 'call', callee: 'Bind' }, label: 'c.Bind (echo)' },
|
|
148
|
+
{ kind: 'source', id: 'go-chi-urlparam', language: 'go', framework: 'chi', match: { type: 'call', callee: 'URLParam' }, label: 'chi.URLParam' },
|
|
149
|
+
{ kind: 'source', id: 'go-r-postformvalue',language:'go', framework:'net/http',match:{type:'call',callee:'PostFormValue'}, label: 'r.PostFormValue' },
|
|
150
|
+
{ kind: 'source', id: 'go-fiber-body', language: 'go', framework: 'fiber', match: { type: 'call', callee: 'Body' }, label: 'c.Body (fiber)' },
|
|
151
|
+
{ kind: 'source', id: 'go-fiber-query', language: 'go', framework: 'fiber', match: { type: 'call', callee: 'Query' }, label: 'c.Query (fiber)' },
|
|
152
|
+
{ kind: 'source', id: 'go-fiber-params', language: 'go', framework: 'fiber', match: { type: 'call', callee: 'Params' }, label: 'c.Params (fiber)' },
|
|
153
|
+
{ kind: 'source', id: 'go-fiber-formvalue',language:'go', framework: 'fiber', match: { type: 'call', callee: 'FormValue' }, label: 'c.FormValue (fiber)' },
|
|
154
|
+
{ kind: 'source', id: 'go-fiber-cookies', language: 'go', framework: 'fiber', match: { type: 'call', callee: 'Cookies' }, label: 'c.Cookies (fiber)' },
|
|
155
|
+
{ kind: 'source', id: 'go-fiber-bodyparser',language:'go',framework:'fiber', match: { type: 'call', callee: 'BodyParser' }, label: 'c.BodyParser (fiber)' },
|
|
156
|
+
{ kind: 'source', id: 'go-buffalo-param', language: 'go', framework: 'buffalo',match: { type: 'call', callee: 'Param' }, label: 'c.Param (buffalo)' },
|
|
157
|
+
{ kind: 'source', id: 'go-buffalo-request',language:'go', framework:'buffalo',match: { type: 'member', object: 'c', prop: 'Request' }, label: 'c.Request (buffalo)' },
|
|
158
|
+
{ kind: 'source', id: 'go-gorilla-vars', language: 'go', framework: 'gorilla',match: { type: 'call', callee: 'Vars' }, label: 'mux.Vars (gorilla)' },
|
|
142
159
|
|
|
143
160
|
// ─── SOURCES (Ruby — Rails / Sinatra) ─────────────────────────────────────
|
|
144
161
|
{ kind: 'source', id: 'rb-rails-params', language: 'rb', framework: 'rails', match: { type: 'global', name: 'params' }, label: 'params (Rails)' },
|
|
145
162
|
{ kind: 'source', id: 'rb-rails-cookies', language: 'rb', framework: 'rails', match: { type: 'global', name: 'cookies' }, label: 'cookies (Rails)' },
|
|
146
163
|
{ kind: 'source', id: 'rb-rails-session', language: 'rb', framework: 'rails', match: { type: 'global', name: 'session' }, label: 'session (Rails)' },
|
|
147
164
|
{ kind: 'source', id: 'rb-env', language: 'rb', framework: 'stdlib',match: { type: 'global', name: 'ENV' }, label: 'ENV (Ruby)' },
|
|
165
|
+
{ kind: 'source', id: 'rb-sinatra-request-body',language:'rb',framework:'sinatra',match:{type:'member',object:'request',prop:'body'}, label: 'request.body (Sinatra)' },
|
|
166
|
+
{ kind: 'source', id: 'rb-sinatra-request-env', language:'rb',framework:'sinatra',match:{type:'member',object:'request',prop:'env'}, label: 'request.env (Sinatra)' },
|
|
167
|
+
{ kind: 'source', id: 'rb-sinatra-request-params',language:'rb',framework:'sinatra',match:{type:'member',object:'request',prop:'params'},label: 'request.params (Sinatra)' },
|
|
148
168
|
|
|
149
169
|
// ─── SOURCES (PHP) ────────────────────────────────────────────────────────
|
|
150
170
|
{ kind: 'source', id: 'php-request', language: 'php', framework: 'core', match: { type: 'global', name: '_REQUEST' }, label: '$_REQUEST' },
|
|
@@ -152,6 +172,13 @@ export const CATALOG = [
|
|
|
152
172
|
{ kind: 'source', id: 'php-post', language: 'php', framework: 'core', match: { type: 'global', name: '_POST' }, label: '$_POST' },
|
|
153
173
|
{ kind: 'source', id: 'php-cookie', language: 'php', framework: 'core', match: { type: 'global', name: '_COOKIE' }, label: '$_COOKIE' },
|
|
154
174
|
{ kind: 'source', id: 'php-server', language: 'php', framework: 'core', match: { type: 'global', name: '_SERVER' }, label: '$_SERVER' },
|
|
175
|
+
{ kind: 'source', id: 'php-symfony-query', language: 'php', framework: 'symfony', match: { type: 'member', object: '$request', prop: 'query' }, label: '$request->query (Symfony)' },
|
|
176
|
+
{ kind: 'source', id: 'php-symfony-request', language: 'php', framework: 'symfony', match: { type: 'member', object: '$request', prop: 'request' }, label: '$request->request (Symfony)' },
|
|
177
|
+
{ kind: 'source', id: 'php-symfony-cookies', language: 'php', framework: 'symfony', match: { type: 'member', object: '$request', prop: 'cookies' }, label: '$request->cookies (Symfony)' },
|
|
178
|
+
{ kind: 'source', id: 'php-symfony-headers', language: 'php', framework: 'symfony', match: { type: 'member', object: '$request', prop: 'headers' }, label: '$request->headers (Symfony)' },
|
|
179
|
+
{ kind: 'source', id: 'php-symfony-files', language: 'php', framework: 'symfony', match: { type: 'member', object: '$request', prop: 'files' }, label: '$request->files (Symfony)' },
|
|
180
|
+
{ kind: 'source', id: 'php-symfony-content', language: 'php', framework: 'symfony', match: { type: 'call', callee: 'getContent' }, label: '$request->getContent() (Symfony)' },
|
|
181
|
+
{ kind: 'source', id: 'php-symfony-get', language: 'php', framework: 'symfony', match: { type: 'call', callee: 'get' }, label: '$request->get() (Symfony)' },
|
|
155
182
|
|
|
156
183
|
// ─── SINKS (SQL — Python) ─────────────────────────────────────────────────
|
|
157
184
|
{ kind: 'sink', id: 'py-cursor-execute', language: 'py', framework: 'dbapi', match: { type: 'call', callee: 'execute' }, argIndex: 0,
|
|
@@ -189,6 +216,74 @@ export const CATALOG = [
|
|
|
189
216
|
vuln: { name: 'Native SQL Injection (EntityManager.createNativeQuery)', severity: 'critical', cwe: 'CWE-89',
|
|
190
217
|
remediation: 'Use setParameter on the resulting Query.' } },
|
|
191
218
|
|
|
219
|
+
// ─── SINKS (SQL — Go) ──────────────────────────────────────────────────────
|
|
220
|
+
{ kind: 'sink', id: 'go-db-query', language: 'go', framework: 'database/sql', match: { type: 'call', callee: 'Query' }, argIndex: 0,
|
|
221
|
+
vuln: { name: 'SQL Injection (db.Query)', severity: 'critical', cwe: 'CWE-89',
|
|
222
|
+
remediation: 'Use parameterized queries: db.Query("SELECT * FROM t WHERE id = $1", id).' } },
|
|
223
|
+
{ kind: 'sink', id: 'go-db-queryrow', language: 'go', framework: 'database/sql', match: { type: 'call', callee: 'QueryRow' }, argIndex: 0,
|
|
224
|
+
vuln: { name: 'SQL Injection (db.QueryRow)', severity: 'critical', cwe: 'CWE-89',
|
|
225
|
+
remediation: 'Use parameterized queries: db.QueryRow("... WHERE id = $1", id).' } },
|
|
226
|
+
{ kind: 'sink', id: 'go-db-exec', language: 'go', framework: 'database/sql', match: { type: 'call', callee: 'Exec' }, argIndex: 0,
|
|
227
|
+
vuln: { name: 'SQL Injection (db.Exec)', severity: 'critical', cwe: 'CWE-89',
|
|
228
|
+
remediation: 'Use parameterized queries with placeholder args.' } },
|
|
229
|
+
{ kind: 'sink', id: 'go-gorm-raw', language: 'go', framework: 'gorm', match: { type: 'call', callee: 'Raw' }, argIndex: 0,
|
|
230
|
+
vuln: { name: 'SQL Injection (gorm.Raw)', severity: 'critical', cwe: 'CWE-89',
|
|
231
|
+
remediation: 'Use gorm.Where with parameterized placeholders: db.Where("name = ?", name).' } },
|
|
232
|
+
{ kind: 'sink', id: 'go-gorm-exec', language: 'go', framework: 'gorm', match: { type: 'call', callee: 'Exec' }, argIndex: 0,
|
|
233
|
+
vuln: { name: 'SQL Injection (gorm.Exec)', severity: 'critical', cwe: 'CWE-89',
|
|
234
|
+
remediation: 'Use parameterized queries: db.Exec("UPDATE t SET x = ?", val).' } },
|
|
235
|
+
{ kind: 'sink', id: 'go-fmt-fprintf', language: 'go', framework: 'fmt', match: { type: 'call', callee: 'Fprintf' }, argIndex: 1,
|
|
236
|
+
vuln: { name: 'XSS (fmt.Fprintf to ResponseWriter)', severity: 'high', cwe: 'CWE-79',
|
|
237
|
+
remediation: 'Use html/template for HTML output, not fmt.Fprintf with user input.' } },
|
|
238
|
+
|
|
239
|
+
// ─── SINKS (SQL — PHP) ─────────────────────────────────────────────────────
|
|
240
|
+
{ kind: 'sink', id: 'php-mysqli-query', language: 'php', framework: 'mysqli', match: { type: 'call', callee: 'mysqli_query' }, argIndex: 1,
|
|
241
|
+
vuln: { name: 'SQL Injection (mysqli_query)', severity: 'critical', cwe: 'CWE-89',
|
|
242
|
+
remediation: 'Use prepared statements: $stmt = $conn->prepare("SELECT * WHERE id = ?"); $stmt->bind_param("i", $id);' } },
|
|
243
|
+
{ kind: 'sink', id: 'php-pdo-query', language: 'php', framework: 'pdo', match: { type: 'call', callee: 'query' }, argIndex: 0,
|
|
244
|
+
vuln: { name: 'SQL Injection (PDO::query)', severity: 'critical', cwe: 'CWE-89',
|
|
245
|
+
remediation: 'Use PDO::prepare with bound parameters.' } },
|
|
246
|
+
{ kind: 'sink', id: 'php-pdo-exec', language: 'php', framework: 'pdo', match: { type: 'call', callee: 'exec' }, argIndex: 0,
|
|
247
|
+
vuln: { name: 'SQL Injection (PDO::exec)', severity: 'critical', cwe: 'CWE-89',
|
|
248
|
+
remediation: 'Use PDO::prepare with bound parameters.' } },
|
|
249
|
+
{ kind: 'sink', id: 'php-laravel-db-raw', language: 'php', framework: 'laravel', match: { type: 'call', callee: 'raw' }, argIndex: 0,
|
|
250
|
+
vuln: { name: 'SQL Injection (DB::raw)', severity: 'critical', cwe: 'CWE-89',
|
|
251
|
+
remediation: 'Use parameterized bindings: DB::select("SELECT * WHERE id = ?", [$id]).' } },
|
|
252
|
+
{ kind: 'sink', id: 'php-exec', language: 'php', framework: 'core', match: { type: 'call', callee: 'exec' }, argIndex: 0,
|
|
253
|
+
vuln: { name: 'Command Injection (exec)', severity: 'critical', cwe: 'CWE-78',
|
|
254
|
+
remediation: 'Use escapeshellarg() on each argument and avoid shell metacharacters.' } },
|
|
255
|
+
{ kind: 'sink', id: 'php-system', language: 'php', framework: 'core', match: { type: 'call', callee: 'system' }, argIndex: 0,
|
|
256
|
+
vuln: { name: 'Command Injection (system)', severity: 'critical', cwe: 'CWE-78',
|
|
257
|
+
remediation: 'Avoid system(); use proc_open with an argv array instead.' } },
|
|
258
|
+
{ kind: 'sink', id: 'php-shell-exec', language: 'php', framework: 'core', match: { type: 'call', callee: 'shell_exec' }, argIndex: 0,
|
|
259
|
+
vuln: { name: 'Command Injection (shell_exec)', severity: 'critical', cwe: 'CWE-78',
|
|
260
|
+
remediation: 'Avoid shell_exec(); sanitize with escapeshellarg() if unavoidable.' } },
|
|
261
|
+
|
|
262
|
+
// ─── SINKS (SQL/CMD — Ruby) ───────────────────────────────────────────────
|
|
263
|
+
{ kind: 'sink', id: 'rb-ar-where-string', language: 'rb', framework: 'rails', match: { type: 'call', callee: 'where' }, argIndex: 0,
|
|
264
|
+
vuln: { name: 'SQL Injection (ActiveRecord where string)', severity: 'critical', cwe: 'CWE-89',
|
|
265
|
+
remediation: 'Use hash conditions: User.where(name: params[:name]).' } },
|
|
266
|
+
{ kind: 'sink', id: 'rb-ar-find-by-sql', language: 'rb', framework: 'rails', match: { type: 'call', callee: 'find_by_sql' }, argIndex: 0,
|
|
267
|
+
vuln: { name: 'SQL Injection (find_by_sql)', severity: 'critical', cwe: 'CWE-89',
|
|
268
|
+
remediation: 'Use parameterized SQL: find_by_sql(["SELECT * WHERE id = ?", id]).' } },
|
|
269
|
+
{ kind: 'sink', id: 'rb-system', language: 'rb', framework: 'stdlib', match: { type: 'call', callee: 'system' }, argIndex: 0,
|
|
270
|
+
vuln: { name: 'Command Injection (Kernel.system)', severity: 'critical', cwe: 'CWE-78',
|
|
271
|
+
remediation: 'Use the array form: system("cmd", arg1, arg2).' } },
|
|
272
|
+
{ kind: 'sink', id: 'rb-exec', language: 'rb', framework: 'stdlib', match: { type: 'call', callee: 'exec' }, argIndex: 0,
|
|
273
|
+
vuln: { name: 'Command Injection (Kernel.exec)', severity: 'critical', cwe: 'CWE-78',
|
|
274
|
+
remediation: 'Use the array form: exec("cmd", arg1, arg2).' } },
|
|
275
|
+
{ kind: 'sink', id: 'rb-sinatra-erb', language: 'rb', framework: 'sinatra', match: { type: 'call', callee: 'erb' }, argIndex: 0,
|
|
276
|
+
vuln: { name: 'Server-Side Template Injection (Sinatra ERB)', severity: 'high', cwe: 'CWE-1336',
|
|
277
|
+
remediation: 'Use ERB auto-escaping. Never pass user input as the template name.' } },
|
|
278
|
+
|
|
279
|
+
// ─── SINKS (SQL — PHP / Symfony / Doctrine) ───────────────────────────────
|
|
280
|
+
{ kind: 'sink', id: 'php-symfony-createquery',language:'php',framework:'symfony',match:{type:'call',callee:'createQuery'}, argIndex: 0,
|
|
281
|
+
vuln: { name: 'DQL Injection (Doctrine createQuery)', severity: 'critical', cwe: 'CWE-89',
|
|
282
|
+
remediation: 'Use DQL parameters: $em->createQuery("... WHERE e.id = :id")->setParameter("id", $id).' } },
|
|
283
|
+
{ kind: 'sink', id: 'php-doctrine-nativequery',language:'php',framework:'doctrine',match:{type:'call',callee:'createNativeQuery'},argIndex:0,
|
|
284
|
+
vuln: { name: 'SQL Injection (Doctrine createNativeQuery)', severity: 'critical', cwe: 'CWE-89',
|
|
285
|
+
remediation: 'Use bound parameters with createNativeQuery.' } },
|
|
286
|
+
|
|
192
287
|
// ─── SINKS (XSS / template — JS/TS / browser) ─────────────────────────────
|
|
193
288
|
{ kind: 'sink', id: 'js-innerHTML-assign', language: 'js', framework: 'dom', match: { type: 'member', object: '_any_', prop: 'innerHTML' }, argIndex: 'rhs',
|
|
194
289
|
vuln: { name: 'DOM XSS (innerHTML)', severity: 'high', cwe: 'CWE-79',
|
|
@@ -366,6 +461,18 @@ export const CATALOG = [
|
|
|
366
461
|
{ kind: 'source', id: 'py-tornado-get-arg', language: 'py', framework: 'tornado', match: { type: 'call', callee: 'get_argument' }, argIndex: 0, label: 'tornado.get_argument', provenance: 'http-body' },
|
|
367
462
|
{ kind: 'source', id: 'py-tornado-get-args', language: 'py', framework: 'tornado', match: { type: 'call', callee: 'get_arguments' }, argIndex: 0, label: 'tornado.get_arguments', provenance: 'http-body' },
|
|
368
463
|
{ kind: 'source', id: 'py-tornado-get-body', language: 'py', framework: 'tornado', match: { type: 'call', callee: 'get_body_argument' }, argIndex: 0, label: 'tornado.get_body_argument', provenance: 'http-body' },
|
|
464
|
+
// Starlette / Litestar — async ASGI sources.
|
|
465
|
+
{ kind: 'source', id: 'py-starlette-json', language: 'py', framework: 'starlette', match: { type: 'call', callee: 'json' }, label: 'request.json() (Starlette)', provenance: 'http-body' },
|
|
466
|
+
{ kind: 'source', id: 'py-starlette-form', language: 'py', framework: 'starlette', match: { type: 'call', callee: 'form' }, label: 'request.form() (Starlette)', provenance: 'http-body' },
|
|
467
|
+
{ kind: 'source', id: 'py-starlette-body', language: 'py', framework: 'starlette', match: { type: 'call', callee: 'body' }, label: 'request.body() (Starlette)', provenance: 'http-body' },
|
|
468
|
+
{ kind: 'source', id: 'py-starlette-qparams',language: 'py', framework: 'starlette', match: { type: 'member', object: 'request', prop: 'query_params' }, label: 'request.query_params (Starlette)', provenance: 'url-param' },
|
|
469
|
+
{ kind: 'source', id: 'py-starlette-path', language: 'py', framework: 'starlette', match: { type: 'member', object: 'request', prop: 'path_params' }, label: 'request.path_params (Starlette)', provenance: 'path-param' },
|
|
470
|
+
{ kind: 'source', id: 'py-litestar-data', language: 'py', framework: 'litestar', match: { type: 'call', callee: 'data' }, label: 'request.data() (Litestar)', provenance: 'http-body' },
|
|
471
|
+
// Sanic — async Python web.
|
|
472
|
+
{ kind: 'source', id: 'py-sanic-args', language: 'py', framework: 'sanic', match: { type: 'member', object: 'request', prop: 'args' }, label: 'request.args (Sanic)', provenance: 'url-param' },
|
|
473
|
+
{ kind: 'source', id: 'py-sanic-form', language: 'py', framework: 'sanic', match: { type: 'member', object: 'request', prop: 'form' }, label: 'request.form (Sanic)', provenance: 'http-body' },
|
|
474
|
+
{ kind: 'source', id: 'py-sanic-json', language: 'py', framework: 'sanic', match: { type: 'member', object: 'request', prop: 'json' }, label: 'request.json (Sanic)', provenance: 'http-body' },
|
|
475
|
+
{ kind: 'source', id: 'py-sanic-body', language: 'py', framework: 'sanic', match: { type: 'member', object: 'request', prop: 'body' }, label: 'request.body (Sanic)', provenance: 'http-body' },
|
|
369
476
|
// sys.argv — CLI input source. (os.environ already declared above.)
|
|
370
477
|
{ kind: 'source', id: 'py-sys-argv', language: 'py', framework: 'std', match: { type: 'member', object: 'sys', prop: 'argv' }, label: 'sys.argv', provenance: 'cli' },
|
|
371
478
|
// File reads.
|
|
@@ -216,4 +216,78 @@ export function federatedFindings(graph) {
|
|
|
216
216
|
return findings;
|
|
217
217
|
}
|
|
218
218
|
|
|
219
|
-
|
|
219
|
+
// ── Intra-project cross-service taint ───────────────────────────────────────
|
|
220
|
+
//
|
|
221
|
+
// Detects HTTP client calls in one file whose target matches a route handler
|
|
222
|
+
// in another file within the same project. When tainted data flows into the
|
|
223
|
+
// client call's body, and the matching handler reads from req.body, emit a
|
|
224
|
+
// cross-service finding.
|
|
225
|
+
|
|
226
|
+
const _CLIENT_PATTERNS = [
|
|
227
|
+
{ re: /\bfetch\s*\(\s*['"`]([^'"`]+)['"`]/g, method: null, bodyArg: true },
|
|
228
|
+
{ re: /\baxios\.(\w+)\s*\(\s*['"`]([^'"`]+)['"`]/g, method: 1, pathGroup: 2, bodyArg: true },
|
|
229
|
+
{ re: /\brequests\.(\w+)\s*\(\s*['"`]([^'"`]+)['"`]/g, method: 1, pathGroup: 2, bodyArg: true },
|
|
230
|
+
{ re: /\bhttp\.NewRequest\s*\(\s*['"`](\w+)['"`]\s*,\s*['"`]([^'"`]+)['"`]/g, method: 1, pathGroup: 2 },
|
|
231
|
+
];
|
|
232
|
+
|
|
233
|
+
const _HANDLER_PATTERNS = [
|
|
234
|
+
{ re: /\bapp\.(get|post|put|patch|delete)\s*\(\s*['"`]([^'"`]+)['"`]/g },
|
|
235
|
+
{ re: /\brouter\.(get|post|put|patch|delete|HandleFunc)\s*\(\s*['"`]([^'"`]+)['"`]/g },
|
|
236
|
+
{ re: /\b@app\.(route|get|post|put|patch|delete)\s*\(\s*['"`]([^'"`]+)['"`]/g },
|
|
237
|
+
];
|
|
238
|
+
|
|
239
|
+
function _normalizePath(p) {
|
|
240
|
+
return p.replace(/:[^/]+/g, '*').replace(/\{[^}]+\}/g, '*').replace(/<[^>]+>/g, '*').replace(/\/+$/, '');
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
export function detectIntraProjectServiceEdges(fileContents) {
|
|
244
|
+
if (!fileContents || typeof fileContents !== 'object') return [];
|
|
245
|
+
const consumers = [];
|
|
246
|
+
const producers = [];
|
|
247
|
+
for (const [fp, raw] of Object.entries(fileContents)) {
|
|
248
|
+
if (!raw || typeof raw !== 'string') continue;
|
|
249
|
+
const lineOf = (idx) => raw.slice(0, idx).split('\n').length;
|
|
250
|
+
for (const pat of _CLIENT_PATTERNS) {
|
|
251
|
+
pat.re.lastIndex = 0;
|
|
252
|
+
for (const m of raw.matchAll(pat.re)) {
|
|
253
|
+
const method = pat.method ? (m[pat.method] || 'get').toLowerCase() : 'get';
|
|
254
|
+
const path = m[pat.pathGroup || 1];
|
|
255
|
+
consumers.push({ file: fp, line: lineOf(m.index), method, path: _normalizePath(path) });
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
for (const pat of _HANDLER_PATTERNS) {
|
|
259
|
+
pat.re.lastIndex = 0;
|
|
260
|
+
for (const m of raw.matchAll(pat.re)) {
|
|
261
|
+
const method = m[1].toLowerCase();
|
|
262
|
+
const path = m[2];
|
|
263
|
+
producers.push({ file: fp, line: lineOf(m.index), method: method === 'route' ? 'any' : method, path: _normalizePath(path) });
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
const findings = [];
|
|
268
|
+
for (const c of consumers) {
|
|
269
|
+
for (const p of producers) {
|
|
270
|
+
if (c.file === p.file) continue;
|
|
271
|
+
if (p.method !== 'any' && c.method !== p.method) continue;
|
|
272
|
+
if (c.path !== p.path && !c.path.endsWith(p.path)) continue;
|
|
273
|
+
findings.push({
|
|
274
|
+
id: `cross-service:${c.file}:${c.line}->${p.file}:${p.line}`,
|
|
275
|
+
file: p.file,
|
|
276
|
+
line: p.line,
|
|
277
|
+
vuln: 'Cross-Service Taint — HTTP client in one file targets handler in another',
|
|
278
|
+
severity: 'medium',
|
|
279
|
+
family: 'cross-service-taint',
|
|
280
|
+
cwe: 'CWE-346',
|
|
281
|
+
parser: 'CROSS-SERVICE',
|
|
282
|
+
confidence: 0.60,
|
|
283
|
+
description: `HTTP client call in ${c.file}:${c.line} targets the route handler at ${p.file}:${p.line}. If tainted data flows through the client body into the handler's sink, this is a cross-service injection path.`,
|
|
284
|
+
remediation: 'Validate and sanitize all data crossing service boundaries, even internal ones. Treat internal API inputs the same as external user input.',
|
|
285
|
+
source: { file: c.file, line: c.line, label: `${c.method.toUpperCase()} ${c.path}` },
|
|
286
|
+
sink: { file: p.file, line: p.line, label: `handler ${p.path}` },
|
|
287
|
+
});
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
return findings;
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
export const _internal = { _parseSpec, _endpointsFor, _leafPathsOf, _responseFields, _requestFields, _normalizePath };
|
package/src/dataflow/engine.js
CHANGED
|
@@ -38,6 +38,7 @@ import { accessPathOf, isCoveredBy, addPath, removePathAndDescendants, joinSets
|
|
|
38
38
|
import { aliasesForVar } from './points-to.js';
|
|
39
39
|
import { higherOrderTaintFlow } from './higher-order.js';
|
|
40
40
|
import { SummaryCache, entryStateFromCall } from './summaries.js';
|
|
41
|
+
import { lookupBuiltinSummary } from './builtin-summaries.js';
|
|
41
42
|
|
|
42
43
|
// v0.70 #2 — addPath that also taints every alias of the variable.
|
|
43
44
|
// When `target` is a dotted path like "a.x" and the root `a` has aliases
|
|
@@ -242,6 +243,25 @@ function step(node, stateIn, callContext) {
|
|
|
242
243
|
for (const v of mutated.mutated) newState = addPath(newState, v);
|
|
243
244
|
}
|
|
244
245
|
if (sum && sum.returnTainted) return { state: newState, findings: [] };
|
|
246
|
+
} else if (target && calleeName) {
|
|
247
|
+
// Fallback: check builtin summaries for unresolved external calls
|
|
248
|
+
const builtin = lookupBuiltinSummary(calleeName);
|
|
249
|
+
if (builtin) {
|
|
250
|
+
if (builtin.returnTainted && (node.source.args || []).some(a => exprTaint(a, newState))) {
|
|
251
|
+
newState = _addPathAliasAware(newState, target, callContext);
|
|
252
|
+
} else if (!builtin.returnTainted) {
|
|
253
|
+
newState = removePathAndDescendants(newState, target);
|
|
254
|
+
return { state: newState, findings: [] };
|
|
255
|
+
}
|
|
256
|
+
if (builtin.mutatedParams && builtin.mutatedParams.size) {
|
|
257
|
+
for (const idx of builtin.mutatedParams) {
|
|
258
|
+
const argExpr = (node.source.args || [])[parseInt(idx)];
|
|
259
|
+
if (argExpr && argExpr.kind === 'ident' && (node.source.args || []).some(a => exprTaint(a, newState))) {
|
|
260
|
+
newState = _addPathAliasAware(newState, argExpr.name, callContext);
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
}
|
|
245
265
|
}
|
|
246
266
|
}
|
|
247
267
|
if (src && target) {
|
|
@@ -293,6 +313,24 @@ function step(node, stateIn, callContext) {
|
|
|
293
313
|
}
|
|
294
314
|
}
|
|
295
315
|
}
|
|
316
|
+
// Built-in mutation functions: Object.assign(target, ...sources),
|
|
317
|
+
// _.merge(target, ...sources), etc. When any source arg is tainted,
|
|
318
|
+
// taint the target in the caller's scope.
|
|
319
|
+
const calleeName = typeof node.callee === 'string' ? node.callee : null;
|
|
320
|
+
if (calleeName && /^(?:Object\.assign|_\.merge|_\.extend|_\.defaultsDeep|_\.defaults|Object\.defineProperties?)$/.test(calleeName)) {
|
|
321
|
+
const targetArg = (node.args || [])[0];
|
|
322
|
+
const sourceArgsTainted = argTaints.slice(1).some(Boolean);
|
|
323
|
+
if (targetArg && targetArg.kind === 'ident' && sourceArgsTainted) {
|
|
324
|
+
state = _addPathAliasAware(state, targetArg.name, callContext);
|
|
325
|
+
callContext._taintSources.push({
|
|
326
|
+
varName: targetArg.name,
|
|
327
|
+
sourceId: `builtin-mutation:${calleeName}`,
|
|
328
|
+
sourceLabel: `${calleeName} mutation`,
|
|
329
|
+
provenance: 'mutation',
|
|
330
|
+
line: node.line,
|
|
331
|
+
});
|
|
332
|
+
}
|
|
333
|
+
}
|
|
296
334
|
if (cat) {
|
|
297
335
|
for (const e of cat) {
|
|
298
336
|
if (e.kind === 'sink' && (
|
|
@@ -535,6 +573,64 @@ export function runTaintEngine(perFileIR, callGraph, opts = {}) {
|
|
|
535
573
|
if (summaryCache.size() === prevCacheSize) break;
|
|
536
574
|
prevCacheSize = summaryCache.size();
|
|
537
575
|
}
|
|
576
|
+
// Class-field cross-taint pass: when a method writes tainted data to _this_.field,
|
|
577
|
+
// re-analyze other methods of the same class with those fields in the entry state.
|
|
578
|
+
const classTaintedFields = new Map();
|
|
579
|
+
for (const fn of fnList) {
|
|
580
|
+
if (Date.now() > deadlineMs) break;
|
|
581
|
+
const sum = summaryCache.get(fn.qid, new Set());
|
|
582
|
+
if (!sum || !sum.mutatedParams) continue;
|
|
583
|
+
for (const p of sum.mutatedParams) {
|
|
584
|
+
if (typeof p === 'string' && p.startsWith('_this_.')) {
|
|
585
|
+
const classPrefix = fn.qid.split('::')[0] + '::';
|
|
586
|
+
if (!classTaintedFields.has(classPrefix)) classTaintedFields.set(classPrefix, new Set());
|
|
587
|
+
classTaintedFields.get(classPrefix).add(p);
|
|
588
|
+
}
|
|
589
|
+
}
|
|
590
|
+
}
|
|
591
|
+
for (const [classPrefix, fields] of classTaintedFields) {
|
|
592
|
+
if (Date.now() > deadlineMs) break;
|
|
593
|
+
for (const fn of fnList) {
|
|
594
|
+
if (!fn.qid.startsWith(classPrefix)) continue;
|
|
595
|
+
if (summaryCache.has(fn.qid, fields)) continue;
|
|
596
|
+
const ctx = {
|
|
597
|
+
_findings: [], _taintSources: [], _returnTainted: false,
|
|
598
|
+
_stack: new Set(), deadlineMs,
|
|
599
|
+
_summaryCache: summaryCache, _callGraph: callGraph,
|
|
600
|
+
_mutatedParamsOut: new Set(),
|
|
601
|
+
};
|
|
602
|
+
try { analyzeFunction(fn, fields, ctx); } catch {}
|
|
603
|
+
summaryCache.set(fn.qid, fields, {
|
|
604
|
+
returnTainted: !!ctx._returnTainted,
|
|
605
|
+
mutatedParams: ctx._mutatedParamsOut || new Set(),
|
|
606
|
+
taintedGlobals: new Set(),
|
|
607
|
+
findings: [],
|
|
608
|
+
});
|
|
609
|
+
}
|
|
610
|
+
}
|
|
611
|
+
|
|
612
|
+
// k=2 pass: compute tainted-entry-state summaries for functions with params
|
|
613
|
+
// AND at least one caller in the call graph. This catches "safe when called
|
|
614
|
+
// clean, dangerous when called with tainted input" wrapper patterns.
|
|
615
|
+
for (const fn of fnList) {
|
|
616
|
+
if (Date.now() > deadlineMs) break;
|
|
617
|
+
if (!fn.params || !fn.params.length) continue;
|
|
618
|
+
const taintedEntry = new Set(fn.params);
|
|
619
|
+
if (summaryCache.has(fn.qid, taintedEntry)) continue;
|
|
620
|
+
const ctx = {
|
|
621
|
+
_findings: [], _taintSources: [], _returnTainted: false,
|
|
622
|
+
_stack: new Set(), deadlineMs,
|
|
623
|
+
_summaryCache: summaryCache, _callGraph: callGraph,
|
|
624
|
+
_mutatedParamsOut: new Set(),
|
|
625
|
+
};
|
|
626
|
+
try { analyzeFunction(fn, taintedEntry, ctx); } catch {}
|
|
627
|
+
summaryCache.set(fn.qid, taintedEntry, {
|
|
628
|
+
returnTainted: !!ctx._returnTainted,
|
|
629
|
+
mutatedParams: ctx._mutatedParamsOut || new Set(),
|
|
630
|
+
taintedGlobals: new Set(),
|
|
631
|
+
findings: [],
|
|
632
|
+
});
|
|
633
|
+
}
|
|
538
634
|
for (const fn of fnList) {
|
|
539
635
|
if (++n > fnLimit) break;
|
|
540
636
|
if (Date.now() > deadlineMs) break; // global timeout
|
|
@@ -552,6 +648,39 @@ export function runTaintEngine(perFileIR, callGraph, opts = {}) {
|
|
|
552
648
|
try {
|
|
553
649
|
analyzeFunction(fn, new Set(), callContext);
|
|
554
650
|
} catch { continue; }
|
|
651
|
+
// Process higher-order invocations: resolve callbacks and analyze with
|
|
652
|
+
// tainted first-param. Feed findings back into the caller's finding set.
|
|
653
|
+
const hoInvocations = callContext._higherOrderInvocations || [];
|
|
654
|
+
const HO_CAP = 50;
|
|
655
|
+
for (let hi = 0; hi < Math.min(hoInvocations.length, HO_CAP); hi++) {
|
|
656
|
+
if (Date.now() > deadlineMs) break;
|
|
657
|
+
const inv = hoInvocations[hi];
|
|
658
|
+
if (!inv.callee || !inv.taintedParam) continue;
|
|
659
|
+
const resolved = callGraph.resolve ? callGraph.resolve(inv.callee) : null;
|
|
660
|
+
const cbFn = resolved && resolved.qid ? resolved : null;
|
|
661
|
+
if (!cbFn || !cbFn.params || !cbFn.params.length) continue;
|
|
662
|
+
const cbEntry = new Set([cbFn.params[inv.paramIndex || 0]]);
|
|
663
|
+
let cbSummary = summaryCache.get(cbFn.qid, cbEntry);
|
|
664
|
+
if (!cbSummary) {
|
|
665
|
+
cbSummary = summaryCache.compute(cbFn.qid, cbEntry, () => {
|
|
666
|
+
const inner = {
|
|
667
|
+
_findings: [], _taintSources: [], _returnTainted: false,
|
|
668
|
+
_stack: new Set(), deadlineMs,
|
|
669
|
+
_summaryCache: summaryCache, _callGraph: callGraph,
|
|
670
|
+
_mutatedParamsOut: new Set(),
|
|
671
|
+
};
|
|
672
|
+
try { analyzeFunction(cbFn, cbEntry, inner); } catch {}
|
|
673
|
+
// Merge any findings from the callback analysis into the caller.
|
|
674
|
+
callContext._findings.push(...inner._findings.map(f => ({ ...f, _funcQid: fn.qid, _via: 'higher-order' })));
|
|
675
|
+
return {
|
|
676
|
+
returnTainted: !!inner._returnTainted,
|
|
677
|
+
mutatedParams: inner._mutatedParamsOut || new Set(),
|
|
678
|
+
taintedGlobals: new Set(),
|
|
679
|
+
findings: [],
|
|
680
|
+
};
|
|
681
|
+
});
|
|
682
|
+
}
|
|
683
|
+
}
|
|
555
684
|
for (const f of callContext._findings) {
|
|
556
685
|
const key = `${f.sinkId}:${fn.file}:${f.line}`;
|
|
557
686
|
if (seen.has(key)) continue;
|