circle-ir 3.181.0 → 3.185.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/analysis/config-loader.d.ts.map +1 -1
- package/dist/analysis/config-loader.js +97 -0
- package/dist/analysis/config-loader.js.map +1 -1
- package/dist/analysis/passes/language-sources-pass.d.ts.map +1 -1
- package/dist/analysis/passes/language-sources-pass.js +207 -0
- package/dist/analysis/passes/language-sources-pass.js.map +1 -1
- package/dist/analysis/passes/taint-propagation-pass.d.ts.map +1 -1
- package/dist/analysis/passes/taint-propagation-pass.js +158 -0
- package/dist/analysis/passes/taint-propagation-pass.js.map +1 -1
- package/dist/analysis/taint-matcher.js +27 -1
- package/dist/analysis/taint-matcher.js.map +1 -1
- package/dist/analysis/taint-propagation.d.ts.map +1 -1
- package/dist/analysis/taint-propagation.js +47 -11
- package/dist/analysis/taint-propagation.js.map +1 -1
- package/dist/browser/circle-ir.js +413 -10
- package/dist/core/circle-ir-core.cjs +189 -9
- package/dist/core/circle-ir-core.js +189 -9
- package/dist/core/extractors/dfg.js +99 -0
- package/dist/core/extractors/dfg.js.map +1 -1
- package/package.json +1 -1
|
@@ -10020,6 +10020,10 @@ function processGoBlock(node, defs, uses, scopeStack, counters) {
|
|
|
10020
10020
|
} else if (child.type === "for_statement") {
|
|
10021
10021
|
const rangeClause = findChildByTypeGo(child, "range_clause");
|
|
10022
10022
|
if (rangeClause) {
|
|
10023
|
+
const right = rangeClause.childForFieldName("right");
|
|
10024
|
+
if (right) {
|
|
10025
|
+
extractGoUses(right, uses, scopeStack);
|
|
10026
|
+
}
|
|
10023
10027
|
const left = rangeClause.childForFieldName("left");
|
|
10024
10028
|
if (left) {
|
|
10025
10029
|
extractGoLhsDefs(left, defs, scopeStack, child.startPosition.row + 1);
|
|
@@ -10027,6 +10031,7 @@ function processGoBlock(node, defs, uses, scopeStack, counters) {
|
|
|
10027
10031
|
}
|
|
10028
10032
|
} else if (child.type === "call_expression") {
|
|
10029
10033
|
extractGoUses(child, uses, scopeStack);
|
|
10034
|
+
recordGoOpaqueCodecDestDef(child, defs, scopeStack);
|
|
10030
10035
|
} else if (child.type === "return_statement") {
|
|
10031
10036
|
for (let i2 = 0; i2 < child.childCount; i2++) {
|
|
10032
10037
|
const expr = child.child(i2);
|
|
@@ -10119,6 +10124,63 @@ function findChildByTypeGo(node, type) {
|
|
|
10119
10124
|
}
|
|
10120
10125
|
return null;
|
|
10121
10126
|
}
|
|
10127
|
+
var GO_OPAQUE_CODEC_METHODS = /* @__PURE__ */ new Set([
|
|
10128
|
+
"Unmarshal",
|
|
10129
|
+
// json/xml/yaml/toml/gob
|
|
10130
|
+
"Decode",
|
|
10131
|
+
// json.NewDecoder(r).Decode(&dest), gob.Decoder.Decode
|
|
10132
|
+
"NewDecoder",
|
|
10133
|
+
// wrapper; handled via chained Decode above
|
|
10134
|
+
"UnmarshalYAML",
|
|
10135
|
+
"UnmarshalJSON",
|
|
10136
|
+
"UnmarshalText",
|
|
10137
|
+
"UnmarshalBinary"
|
|
10138
|
+
]);
|
|
10139
|
+
function recordGoOpaqueCodecDestDef(call, defs, scopeStack) {
|
|
10140
|
+
const fn = call.childForFieldName("function");
|
|
10141
|
+
if (!fn || fn.type !== "selector_expression") return;
|
|
10142
|
+
const fieldNode = fn.childForFieldName("field");
|
|
10143
|
+
if (!fieldNode) return;
|
|
10144
|
+
const method = getNodeText(fieldNode);
|
|
10145
|
+
if (!GO_OPAQUE_CODEC_METHODS.has(method)) return;
|
|
10146
|
+
const argsNode = call.childForFieldName("arguments");
|
|
10147
|
+
if (!argsNode) return;
|
|
10148
|
+
const args2 = [];
|
|
10149
|
+
for (let i2 = 0; i2 < argsNode.childCount; i2++) {
|
|
10150
|
+
const c = argsNode.child(i2);
|
|
10151
|
+
if (!c) continue;
|
|
10152
|
+
if (c.type === "," || c.type === "(" || c.type === ")") continue;
|
|
10153
|
+
args2.push(c);
|
|
10154
|
+
}
|
|
10155
|
+
if (args2.length === 0) return;
|
|
10156
|
+
const destArg = args2.length >= 2 ? args2[1] : args2[0];
|
|
10157
|
+
const destName = extractGoAddressableVarName(destArg);
|
|
10158
|
+
if (!destName || destName === "_") return;
|
|
10159
|
+
const line = call.startPosition.row + 1;
|
|
10160
|
+
const def = {
|
|
10161
|
+
id: defs.length + 1,
|
|
10162
|
+
variable: destName,
|
|
10163
|
+
kind: "local",
|
|
10164
|
+
line
|
|
10165
|
+
};
|
|
10166
|
+
defs.push(def);
|
|
10167
|
+
currentScope(scopeStack).set(destName, def.id);
|
|
10168
|
+
}
|
|
10169
|
+
function extractGoAddressableVarName(node) {
|
|
10170
|
+
if (node.type === "unary_expression") {
|
|
10171
|
+
const operand = node.childForFieldName("operand") ?? node.child(1) ?? null;
|
|
10172
|
+
if (operand) return extractGoAddressableVarName(operand);
|
|
10173
|
+
return null;
|
|
10174
|
+
}
|
|
10175
|
+
if (node.type === "identifier") {
|
|
10176
|
+
return getNodeText(node);
|
|
10177
|
+
}
|
|
10178
|
+
if (node.type === "selector_expression") {
|
|
10179
|
+
const field = node.childForFieldName("field");
|
|
10180
|
+
if (field) return getNodeText(field);
|
|
10181
|
+
}
|
|
10182
|
+
return null;
|
|
10183
|
+
}
|
|
10122
10184
|
|
|
10123
10185
|
// src/core/extractors/runtime-registrations.ts
|
|
10124
10186
|
var HTTP_VERB_METHODS = /* @__PURE__ */ new Set([
|
|
@@ -11264,6 +11326,49 @@ var DEFAULT_SOURCES = [
|
|
|
11264
11326
|
// Server-side interceptor receives `Metadata headers`; `headers.get(KEY)`
|
|
11265
11327
|
// returns caller-supplied header values.
|
|
11266
11328
|
{ method: "get", class: "Metadata", type: "http_header", severity: "high", return_tainted: true, languages: ["java"] },
|
|
11329
|
+
// --- WebSocket transport channels (cognium-dev #213 second slice) ---
|
|
11330
|
+
//
|
|
11331
|
+
// Server-side WebSocket handlers receive attacker-authored frames on
|
|
11332
|
+
// every call to a receive-shaped method. Untrusted the moment they
|
|
11333
|
+
// return, regardless of any application-level auth on the socket.
|
|
11334
|
+
//
|
|
11335
|
+
// Python — FastAPI / Starlette (`from fastapi import WebSocket`):
|
|
11336
|
+
// data = await websocket.receive_text()
|
|
11337
|
+
// data = await websocket.receive_bytes()
|
|
11338
|
+
// data = await websocket.receive_json() # parsed dict/list
|
|
11339
|
+
// data = await websocket.receive() # {'type', 'text'|'bytes'}
|
|
11340
|
+
{ method: "receive_text", class: "WebSocket", type: "network_input", severity: "high", return_tainted: true, languages: ["python"] },
|
|
11341
|
+
{ method: "receive_bytes", class: "WebSocket", type: "network_input", severity: "high", return_tainted: true, languages: ["python"] },
|
|
11342
|
+
{ method: "receive_json", class: "WebSocket", type: "http_body", severity: "high", return_tainted: true, languages: ["python"] },
|
|
11343
|
+
//
|
|
11344
|
+
// `receive` intentionally class-scoped to WebSocket (Starlette pattern).
|
|
11345
|
+
// Broadening to unqualified would collide with queue/signal `.receive()`.
|
|
11346
|
+
{ method: "receive", class: "WebSocket", type: "network_input", severity: "high", return_tainted: true, languages: ["python"] },
|
|
11347
|
+
// Django Channels `AsyncJsonWebsocketConsumer` / `WebsocketConsumer`
|
|
11348
|
+
// expose the same receive_* names on `self`; the class filter matches
|
|
11349
|
+
// `WebSocket` only, so add unqualified fallbacks for the receive_json /
|
|
11350
|
+
// receive_text convention (broad — no class filter possible without
|
|
11351
|
+
// hardcoding Django's consumer names).
|
|
11352
|
+
{ method: "receive_json", type: "http_body", severity: "high", return_tainted: true, languages: ["python"] },
|
|
11353
|
+
{ method: "receive_text", type: "network_input", severity: "high", return_tainted: true, languages: ["python"] },
|
|
11354
|
+
{ method: "receive_bytes", type: "network_input", severity: "high", return_tainted: true, languages: ["python"] },
|
|
11355
|
+
// Go — gorilla/websocket (`github.com/gorilla/websocket`):
|
|
11356
|
+
// messageType, message, err := conn.ReadMessage()
|
|
11357
|
+
// _, r, err := conn.NextReader()
|
|
11358
|
+
{ method: "ReadMessage", class: "Conn", type: "network_input", severity: "high", return_tainted: true, languages: ["go"] },
|
|
11359
|
+
{ method: "NextReader", class: "Conn", type: "network_input", severity: "high", return_tainted: true, languages: ["go"] },
|
|
11360
|
+
// nhooyr.io/websocket exposes a `Read` method on `*websocket.Conn`;
|
|
11361
|
+
// signature is `func (c *Conn) Read(ctx) (MessageType, []byte, error)`.
|
|
11362
|
+
{ method: "Read", class: "Conn", type: "network_input", severity: "high", return_tainted: true, languages: ["go"] },
|
|
11363
|
+
// Java — Jakarta / Java WebSocket API (`javax.websocket` / `jakarta.websocket`):
|
|
11364
|
+
// session.getBasicRemote().sendText(input); // OUT (not a source)
|
|
11365
|
+
// @OnMessage public void onMessage(String msg) — `msg` is a callback
|
|
11366
|
+
// param, not a return; handled via param_tainted when annotation is set.
|
|
11367
|
+
{ annotation: "OnMessage", type: "network_input", severity: "high", param_tainted: true, languages: ["java"] },
|
|
11368
|
+
// Spring `@MessageMapping` STOMP-over-WebSocket handlers receive
|
|
11369
|
+
// untrusted payloads on every dispatched frame.
|
|
11370
|
+
{ annotation: "MessageMapping", type: "http_body", severity: "high", param_tainted: true, languages: ["java"] },
|
|
11371
|
+
{ annotation: "SubscribeMapping", type: "http_body", severity: "high", param_tainted: true, languages: ["java"] },
|
|
11267
11372
|
// --- Cache reads (second-order taint — Redis / Memcached / Django cache) ---
|
|
11268
11373
|
// The cache round-trip is a canonical second-order sink: whatever was
|
|
11269
11374
|
// written previously (potentially attacker-controlled) resurfaces on read.
|
|
@@ -13336,6 +13441,60 @@ var DEFAULT_SANITIZERS = [
|
|
|
13336
13441
|
// Python Type coercion
|
|
13337
13442
|
{ method: "int", removes: ["sql_injection", "command_injection", "xss"] },
|
|
13338
13443
|
{ method: "float", removes: ["sql_injection", "command_injection"] },
|
|
13444
|
+
// Python URL encoding (cognium-dev #213 fifth slice).
|
|
13445
|
+
//
|
|
13446
|
+
// urllib.parse.quote(x) — RFC-3986 percent-encode; safe for URL path
|
|
13447
|
+
// urllib.parse.quote_plus(x) — same + space→+; safe for query string
|
|
13448
|
+
// urllib.parse.urlencode(d) — encode a dict of pairs
|
|
13449
|
+
//
|
|
13450
|
+
// Bounded to URL-context sinks. Class-scoped to the specific
|
|
13451
|
+
// `urllib.parse` receiver to avoid colliding with unrelated bare
|
|
13452
|
+
// `quote(...)` calls in other libraries.
|
|
13453
|
+
{ method: "quote", class: "urllib.parse", removes: ["ssrf", "open_redirect", "xss", "path_traversal"] },
|
|
13454
|
+
{ method: "quote_plus", class: "urllib.parse", removes: ["ssrf", "open_redirect", "xss", "path_traversal"] },
|
|
13455
|
+
{ method: "urlencode", class: "urllib.parse", removes: ["ssrf", "open_redirect", "xss"] },
|
|
13456
|
+
// Bare aliases — `from urllib.parse import quote` then unqualified.
|
|
13457
|
+
{ method: "quote_plus", removes: ["ssrf", "open_redirect", "xss", "path_traversal"] },
|
|
13458
|
+
{ method: "urlencode", removes: ["ssrf", "open_redirect", "xss"] },
|
|
13459
|
+
// `quote` bare is intentionally NOT registered — it collides with
|
|
13460
|
+
// shlex.quote (bare-imported) which is a command_injection sanitizer,
|
|
13461
|
+
// not a URL sanitizer. The class-scoped variant above catches the
|
|
13462
|
+
// qualified `urllib.parse.quote(...)` shape; unqualified callers who
|
|
13463
|
+
// want URL-context credit should use `quote_plus` (which does not
|
|
13464
|
+
// collide with any command_injection sanitizer).
|
|
13465
|
+
// Python XSS — additional common sanitizers.
|
|
13466
|
+
//
|
|
13467
|
+
// bleach.clean(...) — already covered above (line 2898)
|
|
13468
|
+
// bleach.linkify(...) — turns URLs into anchor tags; sanitizes as well
|
|
13469
|
+
// django.utils.html.escape / strip_tags — Django's XSS escape helpers
|
|
13470
|
+
// jinja2.escape — Jinja2's Markup escape (aliased from markupsafe)
|
|
13471
|
+
// flask.escape — Flask re-export of markupsafe.escape
|
|
13472
|
+
// saxutils.escape — stdlib xml.sax.saxutils.escape for XML docs
|
|
13473
|
+
{ method: "linkify", class: "bleach", removes: ["xss"] },
|
|
13474
|
+
// Bare `linkify(...)` alias — `from bleach import linkify`.
|
|
13475
|
+
{ method: "linkify", removes: ["xss"] },
|
|
13476
|
+
{ method: "escape", class: "django.utils.html", removes: ["xss"] },
|
|
13477
|
+
{ method: "strip_tags", class: "django.utils.html", removes: ["xss"] },
|
|
13478
|
+
{ method: "escape", class: "jinja2", removes: ["xss"] },
|
|
13479
|
+
{ method: "escape", class: "flask", removes: ["xss"] },
|
|
13480
|
+
{ method: "escape", class: "saxutils", removes: ["xss"] },
|
|
13481
|
+
{ method: "escape", class: "xml.sax.saxutils", removes: ["xss"] },
|
|
13482
|
+
{ method: "quoteattr", class: "saxutils", removes: ["xss"] },
|
|
13483
|
+
{ method: "quoteattr", class: "xml.sax.saxutils", removes: ["xss"] },
|
|
13484
|
+
// Python ReDoS — `re.escape(user)` when building a regex from user input
|
|
13485
|
+
// strips regex metacharacters. Downstream `re.compile / re.match` cannot
|
|
13486
|
+
// interpret user-supplied alternations or quantifiers. Also covers the
|
|
13487
|
+
// `code_injection` categorization that `re.compile` currently emits
|
|
13488
|
+
// (a re.escape-wrapped pattern cannot execute anything, so both are safe).
|
|
13489
|
+
{ method: "escape", class: "re", removes: ["redos", "code_injection"] },
|
|
13490
|
+
// Python SQLAlchemy — `text(...).bindparams(...)` binds params safely.
|
|
13491
|
+
// The `bindparams` call is the sanitizer; the parent `text` wraps the
|
|
13492
|
+
// template. Also add `expression.literal` for explicit SQL literals.
|
|
13493
|
+
{ method: "bindparams", removes: ["sql_injection"] },
|
|
13494
|
+
// psycopg2 sql-composition helpers
|
|
13495
|
+
{ method: "Identifier", class: "sql", removes: ["sql_injection"] },
|
|
13496
|
+
{ method: "Literal", class: "sql", removes: ["sql_injection"] },
|
|
13497
|
+
{ method: "Placeholder", class: "sql", removes: ["sql_injection"] },
|
|
13339
13498
|
// =========================================================================
|
|
13340
13499
|
// Rust Sanitizers
|
|
13341
13500
|
// =========================================================================
|
|
@@ -15259,7 +15418,14 @@ function matchesSanitizerPattern(call, pattern) {
|
|
|
15259
15418
|
return false;
|
|
15260
15419
|
}
|
|
15261
15420
|
if (pattern.class) {
|
|
15262
|
-
if (!call.receiver
|
|
15421
|
+
if (!call.receiver) {
|
|
15422
|
+
const target = call.resolution?.target;
|
|
15423
|
+
const expectedTail = `${pattern.class}.${pattern.method}`;
|
|
15424
|
+
if (target && (target === expectedTail || target.endsWith("." + expectedTail))) {
|
|
15425
|
+
} else {
|
|
15426
|
+
return false;
|
|
15427
|
+
}
|
|
15428
|
+
} else if (!receiverMightBeClass(call.receiver, pattern.class)) {
|
|
15263
15429
|
return false;
|
|
15264
15430
|
}
|
|
15265
15431
|
}
|
|
@@ -17002,15 +17168,21 @@ function propagateTaint(graphOrDfg, callsOrSources, sourcesOrSinks, sinksOrSanit
|
|
|
17002
17168
|
const callsAtSink = callsByLine.get(sink.line) ?? [];
|
|
17003
17169
|
for (const call of callsAtSink) {
|
|
17004
17170
|
for (const arg of call.arguments) {
|
|
17005
|
-
if (
|
|
17006
|
-
if (sink.argPositions
|
|
17007
|
-
|
|
17008
|
-
continue;
|
|
17009
|
-
}
|
|
17171
|
+
if (sink.argPositions && sink.argPositions.length > 0) {
|
|
17172
|
+
if (!sink.argPositions.includes(arg.position)) {
|
|
17173
|
+
continue;
|
|
17010
17174
|
}
|
|
17011
|
-
|
|
17012
|
-
|
|
17175
|
+
}
|
|
17176
|
+
const candidateUses = arg.variable ? usesAtSink.filter((u) => u.variable === arg.variable) : usesAtSink;
|
|
17177
|
+
{
|
|
17178
|
+
for (const use of candidateUses) {
|
|
17179
|
+
if (use.def_id !== null) {
|
|
17013
17180
|
if (allTaintedDefIds.has(use.def_id)) {
|
|
17181
|
+
if (!arg.variable) {
|
|
17182
|
+
if (typeof arg.expression !== "string" || arg.expression.length === 0) continue;
|
|
17183
|
+
const re = new RegExp(`(?:^|[^A-Za-z0-9_$])${use.variable.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}(?:[^A-Za-z0-9_$]|$)`);
|
|
17184
|
+
if (!re.test(arg.expression)) continue;
|
|
17185
|
+
}
|
|
17014
17186
|
const taintInfo = taintByDefId.get(use.def_id);
|
|
17015
17187
|
if (taintInfo) {
|
|
17016
17188
|
const isSanitized = checkSanitized(
|
|
@@ -17048,7 +17220,15 @@ function propagateTaint(graphOrDfg, callsOrSources, sourcesOrSinks, sinksOrSanit
|
|
|
17048
17220
|
}
|
|
17049
17221
|
}
|
|
17050
17222
|
}
|
|
17051
|
-
|
|
17223
|
+
const seen = /* @__PURE__ */ new Set();
|
|
17224
|
+
const deduped = [];
|
|
17225
|
+
for (const f of flows) {
|
|
17226
|
+
const key = `${f.source.line}|${f.sink.line}|${f.sink.type}|${f.path.map((p) => p.variable).join(">")}`;
|
|
17227
|
+
if (seen.has(key)) continue;
|
|
17228
|
+
seen.add(key);
|
|
17229
|
+
deduped.push(f);
|
|
17230
|
+
}
|
|
17231
|
+
return { taintedVars, flows: deduped, reachableSinks };
|
|
17052
17232
|
}
|
|
17053
17233
|
function findInitialTaint(sources, callsByLine, defsByLine) {
|
|
17054
17234
|
const tainted = [];
|
|
@@ -25571,7 +25751,22 @@ var PYTHON_TAINTED_PATTERNS2 = [
|
|
|
25571
25751
|
// configs/sources/python.json but was not in the forward-taint regex
|
|
25572
25752
|
// registry, so `name = input()` was not added to pyTaintedVars. Closes
|
|
25573
25753
|
// the deferred `getattr(obj, input())()` reflection-invocation shape.
|
|
25574
|
-
{ pattern: /\binput\s*\(/, type: "io_input" }
|
|
25754
|
+
{ pattern: /\binput\s*\(/, type: "io_input" },
|
|
25755
|
+
// WebSocket transport channels (cognium-dev #213 second slice).
|
|
25756
|
+
// FastAPI / Starlette (`from fastapi import WebSocket`) and Django
|
|
25757
|
+
// Channels consumers both expose `receive_*` methods that return
|
|
25758
|
+
// untrusted frame payloads on every call. Registered in
|
|
25759
|
+
// config-loader.ts as return-tainted; the forward-taint regex here
|
|
25760
|
+
// enables `data = await websocket.receive_text()` → `data` tainted,
|
|
25761
|
+
// which the DFG-less Python path needs so downstream sinks that
|
|
25762
|
+
// consume `data` are flagged.
|
|
25763
|
+
//
|
|
25764
|
+
// Deliberately not adding a bare `.receive\s*\(` here — that would
|
|
25765
|
+
// match too many unrelated APIs (queue receive, signal receive, etc.)
|
|
25766
|
+
// and produce spurious sources on every `x = q.receive()` in the wild.
|
|
25767
|
+
{ pattern: /\.receive_text\s*\(/, type: "network_input" },
|
|
25768
|
+
{ pattern: /\.receive_bytes\s*\(/, type: "network_input" },
|
|
25769
|
+
{ pattern: /\.receive_json\s*\(/, type: "http_body" }
|
|
25575
25770
|
];
|
|
25576
25771
|
var LanguageSourcesPass = class {
|
|
25577
25772
|
name = "language-sources";
|
|
@@ -26263,6 +26458,31 @@ function findJavaScriptAssignmentSources(sourceCode, language) {
|
|
|
26263
26458
|
}
|
|
26264
26459
|
}
|
|
26265
26460
|
}
|
|
26461
|
+
sources.push(...findJavaScriptCallbackParamSources(sourceCode));
|
|
26462
|
+
return sources;
|
|
26463
|
+
}
|
|
26464
|
+
function findJavaScriptCallbackParamSources(sourceCode) {
|
|
26465
|
+
const sources = [];
|
|
26466
|
+
const CB_EVENT_RE = /\.on\s*\(\s*['"](?:message|text|binary)['"]\s*,\s*(?:async\s+)?(?:function\s*\*?\s*(?:\w+\s*)?\(\s*(\w+)|\(\s*(\w+)\s*(?:[,):]|$)|(\w+)\s*=>)/;
|
|
26467
|
+
const lines = sourceCode.split("\n");
|
|
26468
|
+
for (let i2 = 0; i2 < lines.length; i2++) {
|
|
26469
|
+
const line = lines[i2];
|
|
26470
|
+
const trimmed = line.trimStart();
|
|
26471
|
+
if (trimmed.startsWith("//") || trimmed.startsWith("*")) continue;
|
|
26472
|
+
const m = line.match(CB_EVENT_RE);
|
|
26473
|
+
if (!m) continue;
|
|
26474
|
+
const paramName = m[1] ?? m[2] ?? m[3];
|
|
26475
|
+
if (!paramName) continue;
|
|
26476
|
+
if (paramName === "err" || paramName === "error") continue;
|
|
26477
|
+
sources.push({
|
|
26478
|
+
type: "network_input",
|
|
26479
|
+
location: `WebSocket .on(...) callback param '${paramName}' at line ${i2 + 1}`,
|
|
26480
|
+
severity: "high",
|
|
26481
|
+
line: i2 + 1,
|
|
26482
|
+
confidence: 0.95,
|
|
26483
|
+
variable: paramName
|
|
26484
|
+
});
|
|
26485
|
+
}
|
|
26266
26486
|
return sources;
|
|
26267
26487
|
}
|
|
26268
26488
|
function findPythonAssignmentSources(sourceCode, language) {
|
|
@@ -26545,6 +26765,14 @@ function buildJavaScriptTaintedVars(sourceCode, language) {
|
|
|
26545
26765
|
if (!["javascript", "typescript"].includes(language)) return /* @__PURE__ */ new Map();
|
|
26546
26766
|
const tainted = /* @__PURE__ */ new Map();
|
|
26547
26767
|
const lines = sourceCode.split("\n");
|
|
26768
|
+
const CB_EVENT_RE = /\.on\s*\(\s*['"](?:message|text|binary)['"]\s*,\s*(?:async\s+)?(?:function\s*\*?\s*(?:\w+\s*)?\(\s*(\w+)|\(\s*(\w+)\s*(?:[,):]|$)|(\w+)\s*=>)/;
|
|
26769
|
+
for (let i2 = 0; i2 < lines.length; i2++) {
|
|
26770
|
+
const m = lines[i2].match(CB_EVENT_RE);
|
|
26771
|
+
if (!m) continue;
|
|
26772
|
+
const paramName = m[1] ?? m[2] ?? m[3];
|
|
26773
|
+
if (!paramName || paramName === "err" || paramName === "error") continue;
|
|
26774
|
+
tainted.set(paramName, i2 + 1);
|
|
26775
|
+
}
|
|
26548
26776
|
for (let i2 = 0; i2 < lines.length; i2++) {
|
|
26549
26777
|
const line = lines[i2];
|
|
26550
26778
|
const trimmed = line.trimStart();
|
|
@@ -26741,6 +26969,79 @@ function findBashTaintSources(sourceCode, dfg) {
|
|
|
26741
26969
|
});
|
|
26742
26970
|
}
|
|
26743
26971
|
}
|
|
26972
|
+
const readMatch = /^read\s*\(/.test(trimmed) ? null : trimmed.match(/^read\b([^#(]*)$/);
|
|
26973
|
+
if (readMatch) {
|
|
26974
|
+
const argStr = readMatch[1].trim();
|
|
26975
|
+
const noRedirect = argStr.replace(/\s*<[^<].*$/, "").trim();
|
|
26976
|
+
const tokens = noRedirect.split(/\s+/);
|
|
26977
|
+
const varNames = [];
|
|
26978
|
+
for (let ti = 0; ti < tokens.length; ti++) {
|
|
26979
|
+
const tok = tokens[ti];
|
|
26980
|
+
if (!tok) continue;
|
|
26981
|
+
if (tok.startsWith("-")) {
|
|
26982
|
+
if (/^-[antpidu]$|^-N$/.test(tok)) ti++;
|
|
26983
|
+
continue;
|
|
26984
|
+
}
|
|
26985
|
+
if (/^[A-Za-z_][\w]*$/.test(tok)) varNames.push(tok);
|
|
26986
|
+
}
|
|
26987
|
+
if (varNames.length === 0) varNames.push("REPLY");
|
|
26988
|
+
for (const v of varNames) {
|
|
26989
|
+
const already = sources.some((s) => s.line === lineNumber && s.variable === v);
|
|
26990
|
+
if (already) continue;
|
|
26991
|
+
sources.push({
|
|
26992
|
+
type: "io_input",
|
|
26993
|
+
location: `read \u2192 $${v} (stdin)`,
|
|
26994
|
+
severity: "high",
|
|
26995
|
+
line: lineNumber,
|
|
26996
|
+
confidence: 0.9,
|
|
26997
|
+
variable: v
|
|
26998
|
+
});
|
|
26999
|
+
}
|
|
27000
|
+
}
|
|
27001
|
+
const mapfileMatch = /^(?:mapfile|readarray)\s*\(/.test(trimmed) ? null : trimmed.match(/^(?:mapfile|readarray)\b([^#(]*)$/);
|
|
27002
|
+
if (mapfileMatch) {
|
|
27003
|
+
const argStr = mapfileMatch[1].trim();
|
|
27004
|
+
const tokens = argStr.split(/\s+/);
|
|
27005
|
+
const varNames = [];
|
|
27006
|
+
for (let ti = 0; ti < tokens.length; ti++) {
|
|
27007
|
+
const tok = tokens[ti];
|
|
27008
|
+
if (!tok) continue;
|
|
27009
|
+
if (tok.startsWith("-")) {
|
|
27010
|
+
if (/^-[cCnOsud]$/.test(tok)) ti++;
|
|
27011
|
+
continue;
|
|
27012
|
+
}
|
|
27013
|
+
if (/^[A-Za-z_][\w]*$/.test(tok)) varNames.push(tok);
|
|
27014
|
+
}
|
|
27015
|
+
if (varNames.length === 0) varNames.push("MAPFILE");
|
|
27016
|
+
for (const v of varNames) {
|
|
27017
|
+
const already = sources.some((s) => s.line === lineNumber && s.variable === v);
|
|
27018
|
+
if (already) continue;
|
|
27019
|
+
sources.push({
|
|
27020
|
+
type: "io_input",
|
|
27021
|
+
location: `mapfile \u2192 $${v} (stdin array)`,
|
|
27022
|
+
severity: "high",
|
|
27023
|
+
line: lineNumber,
|
|
27024
|
+
confidence: 0.9,
|
|
27025
|
+
variable: v
|
|
27026
|
+
});
|
|
27027
|
+
}
|
|
27028
|
+
}
|
|
27029
|
+
const getoptsMatch = trimmed.match(/\bgetopts\s+["'][^"']+["']\s+(\w+)/);
|
|
27030
|
+
if (getoptsMatch) {
|
|
27031
|
+
const flagVar = getoptsMatch[1];
|
|
27032
|
+
for (const v of [flagVar, "OPTARG"]) {
|
|
27033
|
+
const already = sources.some((s) => s.line === lineNumber && s.variable === v);
|
|
27034
|
+
if (already) continue;
|
|
27035
|
+
sources.push({
|
|
27036
|
+
type: "io_input",
|
|
27037
|
+
location: `getopts \u2192 $${v} (CLI arg)`,
|
|
27038
|
+
severity: "high",
|
|
27039
|
+
line: lineNumber,
|
|
27040
|
+
confidence: 0.9,
|
|
27041
|
+
variable: v
|
|
27042
|
+
});
|
|
27043
|
+
}
|
|
27044
|
+
}
|
|
26744
27045
|
const envRe = /\$([A-Z][A-Z0-9_]{2,})|\$\{([A-Z][A-Z0-9_]{2,})\}/g;
|
|
26745
27046
|
let em;
|
|
26746
27047
|
while ((em = envRe.exec(line)) !== null) {
|
|
@@ -33641,6 +33942,18 @@ var TaintPropagationPass = class {
|
|
|
33641
33942
|
if (isFP) continue;
|
|
33642
33943
|
pushIfNew(f);
|
|
33643
33944
|
}
|
|
33945
|
+
if (ctx.language === "go" && typeof ctx.code === "string") {
|
|
33946
|
+
const goGlobalFlows = detectGoPackageGlobalFlows(
|
|
33947
|
+
ctx.code,
|
|
33948
|
+
calls,
|
|
33949
|
+
sources,
|
|
33950
|
+
sinks,
|
|
33951
|
+
constProp.unreachableLines
|
|
33952
|
+
) ?? [];
|
|
33953
|
+
for (const f of goGlobalFlows) {
|
|
33954
|
+
pushIfNew(f);
|
|
33955
|
+
}
|
|
33956
|
+
}
|
|
33644
33957
|
const sanitizedNames = constProp.sanitizedVars;
|
|
33645
33958
|
let finalFlows = sanitizedNames.size === 0 ? flows : flows.filter((f) => {
|
|
33646
33959
|
if (f.path.length === 0) return true;
|
|
@@ -33855,6 +34168,96 @@ function isInJavaSanitizedMethod(code, types, sinkLine, sinkType) {
|
|
|
33855
34168
|
}
|
|
33856
34169
|
return false;
|
|
33857
34170
|
}
|
|
34171
|
+
function detectGoPackageGlobalFlows(code, _calls, sources, sinks, unreachableLines) {
|
|
34172
|
+
const flows = [];
|
|
34173
|
+
if (!code || sinks.length === 0) return flows;
|
|
34174
|
+
const lines = code.split("\n");
|
|
34175
|
+
const packageVars = /* @__PURE__ */ new Set();
|
|
34176
|
+
let inVarBlock = false;
|
|
34177
|
+
for (const line of lines) {
|
|
34178
|
+
const stripped = line.replace(/\s+$/, "");
|
|
34179
|
+
if (!stripped) continue;
|
|
34180
|
+
if (/^[ \t]/.test(line)) {
|
|
34181
|
+
if (inVarBlock) {
|
|
34182
|
+
const m = stripped.match(/^\s*([A-Za-z_][A-Za-z0-9_]*)\s+/);
|
|
34183
|
+
if (m) packageVars.add(m[1]);
|
|
34184
|
+
if (/^\s*\)/.test(stripped)) inVarBlock = false;
|
|
34185
|
+
}
|
|
34186
|
+
continue;
|
|
34187
|
+
}
|
|
34188
|
+
if (/^var\s*\(/.test(stripped)) {
|
|
34189
|
+
inVarBlock = true;
|
|
34190
|
+
continue;
|
|
34191
|
+
}
|
|
34192
|
+
if (inVarBlock && /^\)/.test(stripped)) {
|
|
34193
|
+
inVarBlock = false;
|
|
34194
|
+
continue;
|
|
34195
|
+
}
|
|
34196
|
+
const varOne = stripped.match(/^var\s+([A-Za-z_][A-Za-z0-9_]*)\b/);
|
|
34197
|
+
if (varOne) packageVars.add(varOne[1]);
|
|
34198
|
+
}
|
|
34199
|
+
if (packageVars.size === 0) return flows;
|
|
34200
|
+
const sourceVarNames = /* @__PURE__ */ new Set();
|
|
34201
|
+
for (const s of sources) {
|
|
34202
|
+
if (typeof s.variable === "string" && s.variable.length > 0) {
|
|
34203
|
+
sourceVarNames.add(s.variable);
|
|
34204
|
+
}
|
|
34205
|
+
}
|
|
34206
|
+
const GO_SOURCE_RE = /\b(r\.(?:URL\.Query|Form|PostForm|Header|Cookie|Body)|mux\.Vars|c\.(?:Query|Param|PostForm|GetHeader|Cookie)|ctx\.(?:Query|Param|PostForm|GetHeader|Cookie))\b/;
|
|
34207
|
+
const writes = [];
|
|
34208
|
+
for (let i2 = 0; i2 < lines.length; i2++) {
|
|
34209
|
+
const line = lines[i2];
|
|
34210
|
+
if (unreachableLines.has(i2 + 1)) continue;
|
|
34211
|
+
if (/:=/.test(line)) continue;
|
|
34212
|
+
const m = line.match(/^\s*([A-Za-z_][A-Za-z0-9_]*)\s*=\s*(.+)$/);
|
|
34213
|
+
if (!m) continue;
|
|
34214
|
+
const [, lhs, rhs] = m;
|
|
34215
|
+
if (!packageVars.has(lhs)) continue;
|
|
34216
|
+
let taintedBy = null;
|
|
34217
|
+
if (GO_SOURCE_RE.test(rhs)) {
|
|
34218
|
+
const src = sources.find((s) => s.line === i2 + 1);
|
|
34219
|
+
taintedBy = src ? { line: src.line, type: src.type } : { line: i2 + 1, type: "http_param" };
|
|
34220
|
+
} else {
|
|
34221
|
+
for (const v of sourceVarNames) {
|
|
34222
|
+
const re = new RegExp(`(?<![A-Za-z0-9_$])${v.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}(?![A-Za-z0-9_$])`);
|
|
34223
|
+
if (re.test(rhs)) {
|
|
34224
|
+
const src = sources.find((s) => s.variable === v);
|
|
34225
|
+
if (src) {
|
|
34226
|
+
taintedBy = { line: src.line, type: src.type };
|
|
34227
|
+
break;
|
|
34228
|
+
}
|
|
34229
|
+
}
|
|
34230
|
+
}
|
|
34231
|
+
}
|
|
34232
|
+
if (!taintedBy) continue;
|
|
34233
|
+
writes.push({ varName: lhs, line: i2 + 1, sourceLine: taintedBy.line, sourceType: taintedBy.type });
|
|
34234
|
+
}
|
|
34235
|
+
if (writes.length === 0) return flows;
|
|
34236
|
+
for (const sink of sinks) {
|
|
34237
|
+
if (unreachableLines.has(sink.line)) continue;
|
|
34238
|
+
const sinkLineText = lines[sink.line - 1] ?? "";
|
|
34239
|
+
if (!sinkLineText) continue;
|
|
34240
|
+
for (const w of writes) {
|
|
34241
|
+
if (w.line >= sink.line) continue;
|
|
34242
|
+
const re = new RegExp(`(?<![A-Za-z0-9_$])${w.varName.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}(?![A-Za-z0-9_$])`);
|
|
34243
|
+
if (!re.test(sinkLineText)) continue;
|
|
34244
|
+
flows.push({
|
|
34245
|
+
source_line: w.sourceLine,
|
|
34246
|
+
sink_line: sink.line,
|
|
34247
|
+
source_type: w.sourceType,
|
|
34248
|
+
sink_type: sink.type,
|
|
34249
|
+
path: [
|
|
34250
|
+
{ variable: w.varName, line: w.sourceLine, type: "source" },
|
|
34251
|
+
{ variable: w.varName, line: w.line, type: "assignment" },
|
|
34252
|
+
{ variable: w.varName, line: sink.line, type: "sink" }
|
|
34253
|
+
],
|
|
34254
|
+
confidence: 0.9,
|
|
34255
|
+
sanitized: false
|
|
34256
|
+
});
|
|
34257
|
+
}
|
|
34258
|
+
}
|
|
34259
|
+
return flows;
|
|
34260
|
+
}
|
|
33858
34261
|
function detectCollectionFlows(calls, sources, sinks, taintedVars, unreachableLines, code, types) {
|
|
33859
34262
|
const flows = [];
|
|
33860
34263
|
const callsByLine = /* @__PURE__ */ new Map();
|