circle-ir 4.9.13 → 4.9.14
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 +25 -5
- package/dist/analysis/config-loader.js.map +1 -1
- package/dist/analysis/passes/sink-filter-pass.d.ts.map +1 -1
- package/dist/analysis/passes/sink-filter-pass.js +131 -4
- package/dist/analysis/passes/sink-filter-pass.js.map +1 -1
- package/dist/analysis/passes/taint-propagation-pass.d.ts +48 -0
- package/dist/analysis/passes/taint-propagation-pass.d.ts.map +1 -1
- package/dist/analysis/passes/taint-propagation-pass.js +166 -1
- package/dist/analysis/passes/taint-propagation-pass.js.map +1 -1
- package/dist/browser/circle-ir.js +197 -17
- package/dist/core/circle-ir-core.cjs +33 -5
- package/dist/core/circle-ir-core.js +33 -5
- package/dist/core/extractors/calls.js +19 -0
- package/dist/core/extractors/calls.js.map +1 -1
- package/package.json +1 -1
|
@@ -6700,6 +6700,14 @@ function buildResolutionContext(tree, cache) {
|
|
|
6700
6700
|
}
|
|
6701
6701
|
}
|
|
6702
6702
|
}
|
|
6703
|
+
const resourceDecls = getNodesFromCache(tree.rootNode, "resource", cache);
|
|
6704
|
+
for (const res of resourceDecls) {
|
|
6705
|
+
const typeNode = res.childForFieldName("type");
|
|
6706
|
+
const nameNode = res.childForFieldName("name");
|
|
6707
|
+
if (typeNode && nameNode) {
|
|
6708
|
+
context.localVarTypes.set(getNodeText(nameNode), getNodeText(typeNode));
|
|
6709
|
+
}
|
|
6710
|
+
}
|
|
6703
6711
|
const imports = getNodesFromCache(tree.rootNode, "import_declaration", cache);
|
|
6704
6712
|
for (const imp of imports) {
|
|
6705
6713
|
const text = getNodeText(imp);
|
|
@@ -12372,9 +12380,27 @@ var DEFAULT_SINKS = [
|
|
|
12372
12380
|
// Command Injection (CWE-78)
|
|
12373
12381
|
{ method: "exec", class: "Runtime", type: "command_injection", cwe: "CWE-78", severity: "critical", arg_positions: [0, 1] },
|
|
12374
12382
|
{ method: "start", class: "ProcessBuilder", type: "command_injection", cwe: "CWE-78", severity: "critical", arg_positions: [] },
|
|
12375
|
-
// ProcessBuilder constructor
|
|
12376
|
-
|
|
12377
|
-
|
|
12383
|
+
// ProcessBuilder constructor + command(): cognium-dev#351 — BOTH take
|
|
12384
|
+
// `String...` varargs (or a single List<String>), and EVERY element is a
|
|
12385
|
+
// command token, so no position is safe. Pinning these to `[0]` missed the
|
|
12386
|
+
// canonical shell-wrapper shape, where the payload is the last argument:
|
|
12387
|
+
//
|
|
12388
|
+
// new ProcessBuilder("bash", "-c", tainted) // payload at position 2
|
|
12389
|
+
// pb.command("bash", "-c", tainted)
|
|
12390
|
+
//
|
|
12391
|
+
// `Runtime.exec(String[])` with the identical array already fired, so the
|
|
12392
|
+
// taint was available and only the sink model was wrong.
|
|
12393
|
+
//
|
|
12394
|
+
// The positions are enumerated rather than left `[]` because the two layers
|
|
12395
|
+
// that read this field disagree about what an empty array means:
|
|
12396
|
+
// `isInDangerousPosition` (taint-matcher) does `arg_positions.includes(pos)`,
|
|
12397
|
+
// so `[]` matches NOTHING, while the sink-reachability loop in
|
|
12398
|
+
// taint-propagation treats `length === 0` as "any argument".
|
|
12399
|
+
// `ProcessBuilder.start` gets away with `[]` only because it takes no
|
|
12400
|
+
// arguments at all. Enumerating is unambiguous in both, and matches how the
|
|
12401
|
+
// logging sinks in this file already spell it.
|
|
12402
|
+
{ method: "ProcessBuilder", class: "constructor", type: "command_injection", cwe: "CWE-78", severity: "critical", arg_positions: [0, 1, 2, 3, 4, 5, 6, 7] },
|
|
12403
|
+
{ method: "command", class: "ProcessBuilder", type: "command_injection", cwe: "CWE-78", severity: "critical", arg_positions: [0, 1, 2, 3, 4, 5, 6, 7] },
|
|
12378
12404
|
// Commons Exec
|
|
12379
12405
|
// Note: bare class 'Executor' removed — it collided with java.util.concurrent.Executor
|
|
12380
12406
|
// (Executor.execute(Runnable) is not command injection). Apache Commons Exec users
|
|
@@ -13652,8 +13678,10 @@ var DEFAULT_SINKS = [
|
|
|
13652
13678
|
// =========================================================================
|
|
13653
13679
|
// Java CWE-Bench Enhancement Patterns (Collection/Builder)
|
|
13654
13680
|
// =========================================================================
|
|
13655
|
-
// Collection-based command injection (ProcessBuilder with List)
|
|
13656
|
-
|
|
13681
|
+
// Collection-based command injection (ProcessBuilder with List) — the
|
|
13682
|
+
// `ProcessBuilder.command` entry lives with the other ProcessBuilder sinks
|
|
13683
|
+
// above; a second copy here only risked the two drifting apart, which is
|
|
13684
|
+
// what happened (cognium-dev#351).
|
|
13657
13685
|
// ProcessBuilder.inheritIO removed in 3.83.0 (#124): no args, no command
|
|
13658
13686
|
// string flows into it. See note above next to the Process-related cluster.
|
|
13659
13687
|
// Jenkins DSL patterns
|
|
@@ -15583,7 +15611,7 @@ function isSafeGoExecCommandCall(call, pattern, language) {
|
|
|
15583
15611
|
const stripped = expr.slice(1, -1);
|
|
15584
15612
|
program = stripped.split("/").pop() ?? stripped;
|
|
15585
15613
|
}
|
|
15586
|
-
const
|
|
15614
|
+
const SHELL_PROGRAMS2 = /* @__PURE__ */ new Set([
|
|
15587
15615
|
"sh",
|
|
15588
15616
|
"bash",
|
|
15589
15617
|
"zsh",
|
|
@@ -15597,7 +15625,7 @@ function isSafeGoExecCommandCall(call, pattern, language) {
|
|
|
15597
15625
|
"powershell.exe",
|
|
15598
15626
|
"pwsh.exe"
|
|
15599
15627
|
]);
|
|
15600
|
-
if (
|
|
15628
|
+
if (SHELL_PROGRAMS2.has(program)) return false;
|
|
15601
15629
|
const PACKAGE_MANAGER_EXEC_SUBCOMMANDS = {
|
|
15602
15630
|
go: /* @__PURE__ */ new Set(["install", "run", "get"]),
|
|
15603
15631
|
npm: /* @__PURE__ */ new Set(["install", "i", "exec"]),
|
|
@@ -15648,7 +15676,7 @@ function isSafeJSChildProcessCall(call, pattern, language) {
|
|
|
15648
15676
|
const stripped = expr.slice(1, -1);
|
|
15649
15677
|
program = stripped.split("/").pop() ?? stripped;
|
|
15650
15678
|
}
|
|
15651
|
-
const
|
|
15679
|
+
const SHELL_PROGRAMS2 = /* @__PURE__ */ new Set([
|
|
15652
15680
|
"sh",
|
|
15653
15681
|
"bash",
|
|
15654
15682
|
"zsh",
|
|
@@ -15662,7 +15690,7 @@ function isSafeJSChildProcessCall(call, pattern, language) {
|
|
|
15662
15690
|
"powershell.exe",
|
|
15663
15691
|
"pwsh.exe"
|
|
15664
15692
|
]);
|
|
15665
|
-
if (
|
|
15693
|
+
if (SHELL_PROGRAMS2.has(program)) return false;
|
|
15666
15694
|
return true;
|
|
15667
15695
|
}
|
|
15668
15696
|
var CSHARP_SHELL_PROGRAMS = /* @__PURE__ */ new Set([
|
|
@@ -15895,7 +15923,7 @@ function isSafeRustCommandCall(call, pattern, language) {
|
|
|
15895
15923
|
if (language !== "rust") return false;
|
|
15896
15924
|
if (pattern.type !== "command_injection") return false;
|
|
15897
15925
|
if (pattern.class !== void 0 && pattern.class !== "Command") return false;
|
|
15898
|
-
const
|
|
15926
|
+
const SHELL_PROGRAMS2 = /* @__PURE__ */ new Set([
|
|
15899
15927
|
"sh",
|
|
15900
15928
|
"bash",
|
|
15901
15929
|
"zsh",
|
|
@@ -15930,13 +15958,13 @@ function isSafeRustCommandCall(call, pattern, language) {
|
|
|
15930
15958
|
const stripped = expr.slice(1, -1);
|
|
15931
15959
|
program = stripped.split("/").pop() ?? stripped;
|
|
15932
15960
|
}
|
|
15933
|
-
return !
|
|
15961
|
+
return !SHELL_PROGRAMS2.has(program);
|
|
15934
15962
|
}
|
|
15935
15963
|
if (pattern.method === "arg" || pattern.method === "args" || pattern.method === "spawn" || pattern.method === "output") {
|
|
15936
15964
|
const receiverText = call.receiver ?? "";
|
|
15937
15965
|
const program = extractProgram(receiverText);
|
|
15938
15966
|
if (program === null) return false;
|
|
15939
|
-
return !
|
|
15967
|
+
return !SHELL_PROGRAMS2.has(program);
|
|
15940
15968
|
}
|
|
15941
15969
|
return false;
|
|
15942
15970
|
}
|
|
@@ -33881,6 +33909,29 @@ var PROTOCOL_CLIENT_PACKAGES = [
|
|
|
33881
33909
|
];
|
|
33882
33910
|
var OS_EXEC_RECEIVER_RE = /\b(?:Runtime|ProcessBuilder|DefaultExecutor|Executor|Exec|Launcher|ProcStarter|ProcessExecutor|RuntimeUtil)\s*[.(]/;
|
|
33883
33911
|
var PROCESS_BUILDER_ARGV_FORM_RE = /\bnew\s+ProcessBuilder\s*\(\s*(?:Arrays\.asList\b|List\.of\b|Collections\.singletonList\b|new\s+ArrayList\b|new\s+String\s*\[\s*\]\s*\{|"[^"]*"\s*,)/;
|
|
33912
|
+
var PROCESS_BUILDER_COMMAND_ARGV_FORM_RE = /\.\s*command\s*\(\s*(?:Arrays\.asList\b|List\.of\b|Collections\.singletonList\b|new\s+ArrayList\b|new\s+String\s*\[\s*\]\s*\{|"[^"]*"\s*,)/;
|
|
33913
|
+
var SHELL_PROGRAMS = /* @__PURE__ */ new Set([
|
|
33914
|
+
"sh",
|
|
33915
|
+
"bash",
|
|
33916
|
+
"zsh",
|
|
33917
|
+
"ksh",
|
|
33918
|
+
"dash",
|
|
33919
|
+
"ash",
|
|
33920
|
+
"csh",
|
|
33921
|
+
"tcsh",
|
|
33922
|
+
"fish",
|
|
33923
|
+
"cmd",
|
|
33924
|
+
"command",
|
|
33925
|
+
"powershell",
|
|
33926
|
+
"pwsh"
|
|
33927
|
+
]);
|
|
33928
|
+
var ARGV_FIRST_LITERAL_RE = /(?:\bnew\s+ProcessBuilder\s*\(|\.\s*command\s*\()\s*(?:Arrays\.asList\s*\(|List\.of\s*\(|Collections\.singletonList\s*\(|new\s+ArrayList\s*(?:<[^>]*>)?\s*\(\s*(?:Arrays\.asList\s*\()?|new\s+String\s*\[\s*\]\s*\{)?\s*"([^"]*)"/;
|
|
33929
|
+
function argvFirstLiteralIsShell(lineText) {
|
|
33930
|
+
const m = ARGV_FIRST_LITERAL_RE.exec(lineText);
|
|
33931
|
+
if (!m) return false;
|
|
33932
|
+
const base = m[1].split(/[\\/]/).pop().replace(/\.exe$/i, "").toLowerCase();
|
|
33933
|
+
return SHELL_PROGRAMS.has(base);
|
|
33934
|
+
}
|
|
33884
33935
|
var JAVA_THROW_STATEMENT_RE = /^\s*throw\s+new\s+\w+(?:Exception|Error)\b[^;]*;\s*(?:\/\/.*|\/\*.*)?$/;
|
|
33885
33936
|
var JEXL_ENGINE_TYPES = /* @__PURE__ */ new Set(["JexlEngine", "Jexl", "JxltEngine"]);
|
|
33886
33937
|
var JEXL_EXPRESSION_TYPE_RE = /(?:Jexl)?(?:Expression|Script|Template)(?:Script)?$/;
|
|
@@ -34028,6 +34079,27 @@ function jsSsrfHostGuardedLines(code) {
|
|
|
34028
34079
|
return covered;
|
|
34029
34080
|
}
|
|
34030
34081
|
var CSHARP_ALLOWLIST_STRIP_RE = /\bRegex\s*\.\s*Replace\s*\([^,]*,\s*@?"\[\^(?:[A-Za-z0-9_\- ]|\\w|\\d|\\s)+\]"\s*,\s*@?"[A-Za-z0-9_]?"\s*\)/;
|
|
34082
|
+
function isUnwrittenLocalMapKeyRead(code, source) {
|
|
34083
|
+
if (source.type !== "plugin_param") return false;
|
|
34084
|
+
const lines = code.split("\n");
|
|
34085
|
+
const lineText = lines[source.line - 1] ?? "";
|
|
34086
|
+
const read = /\b([A-Za-z_]\w*)\s*\.\s*get\s*\(\s*"([^"]*)"\s*\)/.exec(lineText);
|
|
34087
|
+
if (!read) return false;
|
|
34088
|
+
const [, recv, key] = read;
|
|
34089
|
+
const ctorRe = new RegExp(
|
|
34090
|
+
`\\b${recv}\\s*=\\s*new\\s+(?:Hash|Tree|LinkedHash|ConcurrentHash|Concurrent|Identity|Weak)?Map\\s*(?:<[^>]*>)?\\s*\\(`
|
|
34091
|
+
);
|
|
34092
|
+
if (!lines.some((l) => ctorRe.test(l))) return false;
|
|
34093
|
+
const escapeRe2 = new RegExp(`\\w\\s*\\([^)]*(?<![\\w.])${recv}(?![\\w.])[^)]*\\)`);
|
|
34094
|
+
for (const l of lines) {
|
|
34095
|
+
if (!escapeRe2.test(l)) continue;
|
|
34096
|
+
const stripped = l.replace(new RegExp(`\\b${recv}\\s*\\.\\s*\\w+\\s*\\(`, "g"), "X(");
|
|
34097
|
+
if (escapeRe2.test(stripped)) return false;
|
|
34098
|
+
}
|
|
34099
|
+
const putRe = new RegExp(`\\b${recv}\\s*\\.\\s*put\\s*\\(\\s*"${key.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}"`);
|
|
34100
|
+
if (lines.some((l) => putRe.test(l))) return false;
|
|
34101
|
+
return true;
|
|
34102
|
+
}
|
|
34031
34103
|
var CSHARP_SANITIZER_RES = [
|
|
34032
34104
|
{ re: /\b(?:HtmlEncode|JavaScriptStringEncode)\s*\(/, type: "xss" },
|
|
34033
34105
|
{ re: /\bHtmlEncoder\s*\.\s*Encode\s*\(/, type: "xss" },
|
|
@@ -34280,9 +34352,10 @@ var SinkFilterPass = class {
|
|
|
34280
34352
|
const constProp = ctx.getResult("constant-propagation");
|
|
34281
34353
|
const langSources = ctx.getResult("language-sources");
|
|
34282
34354
|
const mergedSources = [...taintMatcher.sources, ...langSources.additionalSources];
|
|
34283
|
-
const
|
|
34355
|
+
const executableSources = ctx.code && ctx.language ? mergedSources.filter(
|
|
34284
34356
|
(s) => !isNonExecutableSourceLine(ctx.code, s.line, ctx.language)
|
|
34285
34357
|
) : mergedSources;
|
|
34358
|
+
const sources = ctx.code ? executableSources.filter((s) => !isUnwrittenLocalMapKeyRead(ctx.code, s)) : executableSources;
|
|
34286
34359
|
const sinks = [...taintMatcher.sinks];
|
|
34287
34360
|
for (const s of langSources.additionalSinks) {
|
|
34288
34361
|
if (!sinks.some((x) => x.line === s.line && x.cwe === s.cwe && x.type === s.type)) {
|
|
@@ -34550,10 +34623,19 @@ var SinkFilterPass = class {
|
|
|
34550
34623
|
const irTypes = ctx.graph.ir.types;
|
|
34551
34624
|
filtered = filtered.filter((sink) => {
|
|
34552
34625
|
if (sink.type !== "command_injection") return true;
|
|
34553
|
-
if (sink.method !== "ProcessBuilder" && sink.method !== "start")
|
|
34626
|
+
if (sink.method !== "ProcessBuilder" && sink.method !== "start" && sink.method !== "command") {
|
|
34627
|
+
return true;
|
|
34628
|
+
}
|
|
34554
34629
|
const sinkLineText = sourceLines[sink.line - 1] ?? "";
|
|
34555
|
-
|
|
34556
|
-
|
|
34630
|
+
const isCtorLine = /\bnew\s+ProcessBuilder\s*\(/.test(sinkLineText);
|
|
34631
|
+
const isCommandLine = /\.\s*command\s*\(/.test(sinkLineText);
|
|
34632
|
+
if (!isCtorLine && !isCommandLine) return true;
|
|
34633
|
+
if (argvFirstLiteralIsShell(sinkLineText)) return true;
|
|
34634
|
+
if (isCtorLine && PROCESS_BUILDER_ARGV_FORM_RE.test(sinkLineText)) return false;
|
|
34635
|
+
if (isCommandLine && PROCESS_BUILDER_COMMAND_ARGV_FORM_RE.test(sinkLineText)) {
|
|
34636
|
+
return false;
|
|
34637
|
+
}
|
|
34638
|
+
if (!isCtorLine) return true;
|
|
34557
34639
|
const ctorCall = irCalls.find(
|
|
34558
34640
|
(c) => c.method_name === "ProcessBuilder" && c.location.line === sink.line && (c.receiver === null || c.receiver === void 0)
|
|
34559
34641
|
);
|
|
@@ -37124,6 +37206,103 @@ function isReassignedToLiteralBetween(code, variable, srcLine, sinkLine) {
|
|
|
37124
37206
|
}
|
|
37125
37207
|
return false;
|
|
37126
37208
|
}
|
|
37209
|
+
var MASKABLE_STRING_LANGUAGES = /* @__PURE__ */ new Set(["python", "javascript", "typescript"]);
|
|
37210
|
+
function maskStringLiteralsForTokenScan(expr) {
|
|
37211
|
+
let out2 = "";
|
|
37212
|
+
let i2 = 0;
|
|
37213
|
+
const braceExprs = (body2) => {
|
|
37214
|
+
let acc = "";
|
|
37215
|
+
for (let j = 0; j < body2.length; ) {
|
|
37216
|
+
if (body2[j] === "{" && body2[j + 1] === "{") {
|
|
37217
|
+
j += 2;
|
|
37218
|
+
continue;
|
|
37219
|
+
}
|
|
37220
|
+
if (body2[j] === "}" && body2[j + 1] === "}") {
|
|
37221
|
+
j += 2;
|
|
37222
|
+
continue;
|
|
37223
|
+
}
|
|
37224
|
+
if (body2[j] === "{") {
|
|
37225
|
+
j++;
|
|
37226
|
+
let depth = 1;
|
|
37227
|
+
let inner = "";
|
|
37228
|
+
while (j < body2.length && depth > 0) {
|
|
37229
|
+
if (body2[j] === "{") depth++;
|
|
37230
|
+
else if (body2[j] === "}") {
|
|
37231
|
+
depth--;
|
|
37232
|
+
if (depth === 0) break;
|
|
37233
|
+
}
|
|
37234
|
+
inner += body2[j];
|
|
37235
|
+
j++;
|
|
37236
|
+
}
|
|
37237
|
+
j++;
|
|
37238
|
+
acc += " " + maskStringLiteralsForTokenScan(inner) + " ";
|
|
37239
|
+
continue;
|
|
37240
|
+
}
|
|
37241
|
+
j++;
|
|
37242
|
+
}
|
|
37243
|
+
return acc;
|
|
37244
|
+
};
|
|
37245
|
+
while (i2 < expr.length) {
|
|
37246
|
+
const ch = expr[i2];
|
|
37247
|
+
if (ch === "`") {
|
|
37248
|
+
i2++;
|
|
37249
|
+
let acc = "";
|
|
37250
|
+
while (i2 < expr.length && expr[i2] !== "`") {
|
|
37251
|
+
if (expr[i2] === "\\") {
|
|
37252
|
+
i2 += 2;
|
|
37253
|
+
continue;
|
|
37254
|
+
}
|
|
37255
|
+
if (expr[i2] === "$" && expr[i2 + 1] === "{") {
|
|
37256
|
+
i2 += 2;
|
|
37257
|
+
let depth = 1;
|
|
37258
|
+
let inner = "";
|
|
37259
|
+
while (i2 < expr.length && depth > 0) {
|
|
37260
|
+
if (expr[i2] === "{") depth++;
|
|
37261
|
+
else if (expr[i2] === "}") {
|
|
37262
|
+
depth--;
|
|
37263
|
+
if (depth === 0) break;
|
|
37264
|
+
}
|
|
37265
|
+
inner += expr[i2];
|
|
37266
|
+
i2++;
|
|
37267
|
+
}
|
|
37268
|
+
i2++;
|
|
37269
|
+
acc += " " + maskStringLiteralsForTokenScan(inner) + " ";
|
|
37270
|
+
continue;
|
|
37271
|
+
}
|
|
37272
|
+
i2++;
|
|
37273
|
+
}
|
|
37274
|
+
i2++;
|
|
37275
|
+
out2 += " " + acc + " ";
|
|
37276
|
+
continue;
|
|
37277
|
+
}
|
|
37278
|
+
if (ch === '"' || ch === "'") {
|
|
37279
|
+
const pm = /[A-Za-z_][A-Za-z0-9_]*$/.exec(out2);
|
|
37280
|
+
const prefix = pm ? pm[0] : "";
|
|
37281
|
+
const isPrefix = prefix.length > 0 && /^[rRbBuUfF]+$/.test(prefix);
|
|
37282
|
+
const isF = isPrefix && /[fF]/.test(prefix);
|
|
37283
|
+
if (isPrefix) out2 = out2.slice(0, out2.length - prefix.length) + " ";
|
|
37284
|
+
const triple = expr.startsWith(ch.repeat(3), i2);
|
|
37285
|
+
const delim = triple ? ch.repeat(3) : ch;
|
|
37286
|
+
i2 += delim.length;
|
|
37287
|
+
let body2 = "";
|
|
37288
|
+
while (i2 < expr.length && !expr.startsWith(delim, i2)) {
|
|
37289
|
+
if (expr[i2] === "\\") {
|
|
37290
|
+
body2 += " ";
|
|
37291
|
+
i2 += 2;
|
|
37292
|
+
continue;
|
|
37293
|
+
}
|
|
37294
|
+
body2 += expr[i2];
|
|
37295
|
+
i2++;
|
|
37296
|
+
}
|
|
37297
|
+
i2 += delim.length;
|
|
37298
|
+
out2 += isF ? " " + braceExprs(body2) + " " : " ";
|
|
37299
|
+
continue;
|
|
37300
|
+
}
|
|
37301
|
+
out2 += ch;
|
|
37302
|
+
i2++;
|
|
37303
|
+
}
|
|
37304
|
+
return out2;
|
|
37305
|
+
}
|
|
37127
37306
|
function detectExpressionScanFlows(calls, sources, sinks, sanitizers, unreachableLines, tainted, code, language, types) {
|
|
37128
37307
|
const flows = [];
|
|
37129
37308
|
const sourcesWithVar = sources.filter(
|
|
@@ -37477,7 +37656,8 @@ function detectExpressionScanFlows(calls, sources, sinks, sanitizers, unreachabl
|
|
|
37477
37656
|
}
|
|
37478
37657
|
const expr = arg.expression;
|
|
37479
37658
|
if (!expr) continue;
|
|
37480
|
-
const
|
|
37659
|
+
const scannable = language !== void 0 && MASKABLE_STRING_LANGUAGES.has(language) ? maskStringLiteralsForTokenScan(expr) : expr;
|
|
37660
|
+
const exprIdentMatches = scannable.match(/[\p{L}\p{N}_]+/gu);
|
|
37481
37661
|
let candidates = complexSources.length > 0 ? complexSources.slice() : null;
|
|
37482
37662
|
if (exprIdentMatches) {
|
|
37483
37663
|
for (const id of new Set(exprIdentMatches)) {
|
|
@@ -6746,6 +6746,14 @@ function buildResolutionContext(tree, cache) {
|
|
|
6746
6746
|
}
|
|
6747
6747
|
}
|
|
6748
6748
|
}
|
|
6749
|
+
const resourceDecls = getNodesFromCache(tree.rootNode, "resource", cache);
|
|
6750
|
+
for (const res of resourceDecls) {
|
|
6751
|
+
const typeNode = res.childForFieldName("type");
|
|
6752
|
+
const nameNode = res.childForFieldName("name");
|
|
6753
|
+
if (typeNode && nameNode) {
|
|
6754
|
+
context.localVarTypes.set(getNodeText(nameNode), getNodeText(typeNode));
|
|
6755
|
+
}
|
|
6756
|
+
}
|
|
6749
6757
|
const imports = getNodesFromCache(tree.rootNode, "import_declaration", cache);
|
|
6750
6758
|
for (const imp of imports) {
|
|
6751
6759
|
const text = getNodeText(imp);
|
|
@@ -11767,9 +11775,27 @@ var DEFAULT_SINKS = [
|
|
|
11767
11775
|
// Command Injection (CWE-78)
|
|
11768
11776
|
{ method: "exec", class: "Runtime", type: "command_injection", cwe: "CWE-78", severity: "critical", arg_positions: [0, 1] },
|
|
11769
11777
|
{ method: "start", class: "ProcessBuilder", type: "command_injection", cwe: "CWE-78", severity: "critical", arg_positions: [] },
|
|
11770
|
-
// ProcessBuilder constructor
|
|
11771
|
-
|
|
11772
|
-
|
|
11778
|
+
// ProcessBuilder constructor + command(): cognium-dev#351 — BOTH take
|
|
11779
|
+
// `String...` varargs (or a single List<String>), and EVERY element is a
|
|
11780
|
+
// command token, so no position is safe. Pinning these to `[0]` missed the
|
|
11781
|
+
// canonical shell-wrapper shape, where the payload is the last argument:
|
|
11782
|
+
//
|
|
11783
|
+
// new ProcessBuilder("bash", "-c", tainted) // payload at position 2
|
|
11784
|
+
// pb.command("bash", "-c", tainted)
|
|
11785
|
+
//
|
|
11786
|
+
// `Runtime.exec(String[])` with the identical array already fired, so the
|
|
11787
|
+
// taint was available and only the sink model was wrong.
|
|
11788
|
+
//
|
|
11789
|
+
// The positions are enumerated rather than left `[]` because the two layers
|
|
11790
|
+
// that read this field disagree about what an empty array means:
|
|
11791
|
+
// `isInDangerousPosition` (taint-matcher) does `arg_positions.includes(pos)`,
|
|
11792
|
+
// so `[]` matches NOTHING, while the sink-reachability loop in
|
|
11793
|
+
// taint-propagation treats `length === 0` as "any argument".
|
|
11794
|
+
// `ProcessBuilder.start` gets away with `[]` only because it takes no
|
|
11795
|
+
// arguments at all. Enumerating is unambiguous in both, and matches how the
|
|
11796
|
+
// logging sinks in this file already spell it.
|
|
11797
|
+
{ method: "ProcessBuilder", class: "constructor", type: "command_injection", cwe: "CWE-78", severity: "critical", arg_positions: [0, 1, 2, 3, 4, 5, 6, 7] },
|
|
11798
|
+
{ method: "command", class: "ProcessBuilder", type: "command_injection", cwe: "CWE-78", severity: "critical", arg_positions: [0, 1, 2, 3, 4, 5, 6, 7] },
|
|
11773
11799
|
// Commons Exec
|
|
11774
11800
|
// Note: bare class 'Executor' removed — it collided with java.util.concurrent.Executor
|
|
11775
11801
|
// (Executor.execute(Runnable) is not command injection). Apache Commons Exec users
|
|
@@ -13047,8 +13073,10 @@ var DEFAULT_SINKS = [
|
|
|
13047
13073
|
// =========================================================================
|
|
13048
13074
|
// Java CWE-Bench Enhancement Patterns (Collection/Builder)
|
|
13049
13075
|
// =========================================================================
|
|
13050
|
-
// Collection-based command injection (ProcessBuilder with List)
|
|
13051
|
-
|
|
13076
|
+
// Collection-based command injection (ProcessBuilder with List) — the
|
|
13077
|
+
// `ProcessBuilder.command` entry lives with the other ProcessBuilder sinks
|
|
13078
|
+
// above; a second copy here only risked the two drifting apart, which is
|
|
13079
|
+
// what happened (cognium-dev#351).
|
|
13052
13080
|
// ProcessBuilder.inheritIO removed in 3.83.0 (#124): no args, no command
|
|
13053
13081
|
// string flows into it. See note above next to the Process-related cluster.
|
|
13054
13082
|
// Jenkins DSL patterns
|
|
@@ -6680,6 +6680,14 @@ function buildResolutionContext(tree, cache) {
|
|
|
6680
6680
|
}
|
|
6681
6681
|
}
|
|
6682
6682
|
}
|
|
6683
|
+
const resourceDecls = getNodesFromCache(tree.rootNode, "resource", cache);
|
|
6684
|
+
for (const res of resourceDecls) {
|
|
6685
|
+
const typeNode = res.childForFieldName("type");
|
|
6686
|
+
const nameNode = res.childForFieldName("name");
|
|
6687
|
+
if (typeNode && nameNode) {
|
|
6688
|
+
context.localVarTypes.set(getNodeText(nameNode), getNodeText(typeNode));
|
|
6689
|
+
}
|
|
6690
|
+
}
|
|
6683
6691
|
const imports = getNodesFromCache(tree.rootNode, "import_declaration", cache);
|
|
6684
6692
|
for (const imp of imports) {
|
|
6685
6693
|
const text = getNodeText(imp);
|
|
@@ -11701,9 +11709,27 @@ var DEFAULT_SINKS = [
|
|
|
11701
11709
|
// Command Injection (CWE-78)
|
|
11702
11710
|
{ method: "exec", class: "Runtime", type: "command_injection", cwe: "CWE-78", severity: "critical", arg_positions: [0, 1] },
|
|
11703
11711
|
{ method: "start", class: "ProcessBuilder", type: "command_injection", cwe: "CWE-78", severity: "critical", arg_positions: [] },
|
|
11704
|
-
// ProcessBuilder constructor
|
|
11705
|
-
|
|
11706
|
-
|
|
11712
|
+
// ProcessBuilder constructor + command(): cognium-dev#351 — BOTH take
|
|
11713
|
+
// `String...` varargs (or a single List<String>), and EVERY element is a
|
|
11714
|
+
// command token, so no position is safe. Pinning these to `[0]` missed the
|
|
11715
|
+
// canonical shell-wrapper shape, where the payload is the last argument:
|
|
11716
|
+
//
|
|
11717
|
+
// new ProcessBuilder("bash", "-c", tainted) // payload at position 2
|
|
11718
|
+
// pb.command("bash", "-c", tainted)
|
|
11719
|
+
//
|
|
11720
|
+
// `Runtime.exec(String[])` with the identical array already fired, so the
|
|
11721
|
+
// taint was available and only the sink model was wrong.
|
|
11722
|
+
//
|
|
11723
|
+
// The positions are enumerated rather than left `[]` because the two layers
|
|
11724
|
+
// that read this field disagree about what an empty array means:
|
|
11725
|
+
// `isInDangerousPosition` (taint-matcher) does `arg_positions.includes(pos)`,
|
|
11726
|
+
// so `[]` matches NOTHING, while the sink-reachability loop in
|
|
11727
|
+
// taint-propagation treats `length === 0` as "any argument".
|
|
11728
|
+
// `ProcessBuilder.start` gets away with `[]` only because it takes no
|
|
11729
|
+
// arguments at all. Enumerating is unambiguous in both, and matches how the
|
|
11730
|
+
// logging sinks in this file already spell it.
|
|
11731
|
+
{ method: "ProcessBuilder", class: "constructor", type: "command_injection", cwe: "CWE-78", severity: "critical", arg_positions: [0, 1, 2, 3, 4, 5, 6, 7] },
|
|
11732
|
+
{ method: "command", class: "ProcessBuilder", type: "command_injection", cwe: "CWE-78", severity: "critical", arg_positions: [0, 1, 2, 3, 4, 5, 6, 7] },
|
|
11707
11733
|
// Commons Exec
|
|
11708
11734
|
// Note: bare class 'Executor' removed — it collided with java.util.concurrent.Executor
|
|
11709
11735
|
// (Executor.execute(Runnable) is not command injection). Apache Commons Exec users
|
|
@@ -12981,8 +13007,10 @@ var DEFAULT_SINKS = [
|
|
|
12981
13007
|
// =========================================================================
|
|
12982
13008
|
// Java CWE-Bench Enhancement Patterns (Collection/Builder)
|
|
12983
13009
|
// =========================================================================
|
|
12984
|
-
// Collection-based command injection (ProcessBuilder with List)
|
|
12985
|
-
|
|
13010
|
+
// Collection-based command injection (ProcessBuilder with List) — the
|
|
13011
|
+
// `ProcessBuilder.command` entry lives with the other ProcessBuilder sinks
|
|
13012
|
+
// above; a second copy here only risked the two drifting apart, which is
|
|
13013
|
+
// what happened (cognium-dev#351).
|
|
12986
13014
|
// ProcessBuilder.inheritIO removed in 3.83.0 (#124): no args, no command
|
|
12987
13015
|
// string flows into it. See note above next to the Process-related cluster.
|
|
12988
13016
|
// Jenkins DSL patterns
|
|
@@ -1049,6 +1049,25 @@ function buildResolutionContext(tree, cache) {
|
|
|
1049
1049
|
}
|
|
1050
1050
|
}
|
|
1051
1051
|
}
|
|
1052
|
+
// cognium-dev#350 — try-with-resources declarations are ALSO local variables.
|
|
1053
|
+
//
|
|
1054
|
+
// try (Connection c = …; Statement s = c.createStatement()) { s.executeQuery(q); }
|
|
1055
|
+
//
|
|
1056
|
+
// A resource is a `resource` node under `resource_specification`, and it holds
|
|
1057
|
+
// its type and name directly — there is no `variable_declarator` child — so
|
|
1058
|
+
// the loop above never sees it. Without an entry here the receiver type stays
|
|
1059
|
+
// `null`, the class-scoped `Statement.executeQuery` sink cannot match, and the
|
|
1060
|
+
// call degrades to the generic `external_taint_escape` fallback: the sink is
|
|
1061
|
+
// still reported, but as CWE-668 instead of CWE-89. That is the idiomatic JDBC
|
|
1062
|
+
// form and the shape most real Spring code uses.
|
|
1063
|
+
const resourceDecls = getNodesFromCache(tree.rootNode, 'resource', cache);
|
|
1064
|
+
for (const res of resourceDecls) {
|
|
1065
|
+
const typeNode = res.childForFieldName('type');
|
|
1066
|
+
const nameNode = res.childForFieldName('name');
|
|
1067
|
+
if (typeNode && nameNode) {
|
|
1068
|
+
context.localVarTypes.set(getNodeText(nameNode), getNodeText(typeNode));
|
|
1069
|
+
}
|
|
1070
|
+
}
|
|
1052
1071
|
// Collect imports — map simple class name to FQN
|
|
1053
1072
|
const imports = getNodesFromCache(tree.rootNode, 'import_declaration', cache);
|
|
1054
1073
|
for (const imp of imports) {
|