circle-ir 4.13.2 → 4.14.1
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/README.md +3 -3
- package/configs/sinks/golang.json +20 -0
- package/dist/analysis/config-loader.d.ts.map +1 -1
- package/dist/analysis/config-loader.js +7 -2
- package/dist/analysis/config-loader.js.map +1 -1
- package/dist/analysis/interprocedural.js +1 -1
- package/dist/analysis/interprocedural.js.map +1 -1
- package/dist/analysis/passes/library-profile-source-gate-pass.d.ts +19 -18
- package/dist/analysis/passes/library-profile-source-gate-pass.d.ts.map +1 -1
- package/dist/analysis/passes/library-profile-source-gate-pass.js +16 -15
- package/dist/analysis/passes/library-profile-source-gate-pass.js.map +1 -1
- package/dist/analysis/passes/scan-secrets-pass.d.ts +2 -2
- package/dist/analysis/passes/scan-secrets-pass.d.ts.map +1 -1
- package/dist/analysis/passes/scan-secrets-pass.js +23 -7
- package/dist/analysis/passes/scan-secrets-pass.js.map +1 -1
- package/dist/analysis/passes/sink-filter-pass.d.ts.map +1 -1
- package/dist/analysis/passes/sink-filter-pass.js +47 -14
- package/dist/analysis/passes/sink-filter-pass.js.map +1 -1
- package/dist/analysis/taint-matcher.d.ts.map +1 -1
- package/dist/analysis/taint-matcher.js +49 -9
- package/dist/analysis/taint-matcher.js.map +1 -1
- package/dist/analysis/taint-propagation.d.ts +0 -6
- package/dist/analysis/taint-propagation.d.ts.map +1 -1
- package/dist/analysis/taint-propagation.js +12 -2
- package/dist/analysis/taint-propagation.js.map +1 -1
- package/dist/analyzer.d.ts.map +1 -1
- package/dist/analyzer.js +40 -11
- package/dist/analyzer.js.map +1 -1
- package/dist/browser/circle-ir.js +193 -31
- package/dist/core/circle-ir-core.cjs +75 -16
- package/dist/core/circle-ir-core.js +75 -16
- package/dist/core/extractors/calls.js +18 -1
- package/dist/core/extractors/calls.js.map +1 -1
- package/dist/core/extractors/dfg.js +38 -8
- package/dist/core/extractors/dfg.js.map +1 -1
- package/dist/languages/plugins/go.d.ts.map +1 -1
- package/dist/languages/plugins/go.js +40 -0
- package/dist/languages/plugins/go.js.map +1 -1
- package/package.json +1 -1
|
@@ -6184,6 +6184,13 @@ function csharpBareName(node) {
|
|
|
6184
6184
|
}
|
|
6185
6185
|
return getNodeText(node);
|
|
6186
6186
|
}
|
|
6187
|
+
function csharpCreatedSimpleType(creation) {
|
|
6188
|
+
const typeNode = creation.childForFieldName("type");
|
|
6189
|
+
if (!typeNode) return null;
|
|
6190
|
+
const bare = csharpBareName(typeNode).replace(/<[^>]*>/g, "");
|
|
6191
|
+
const simple = bare.split(".").pop()?.trim() ?? "";
|
|
6192
|
+
return simple && simple !== "var" ? simple : null;
|
|
6193
|
+
}
|
|
6187
6194
|
function extractCSharpCalls(tree, cache) {
|
|
6188
6195
|
const calls = [];
|
|
6189
6196
|
const typeMap = buildCSharpReceiverTypeMap(tree, cache);
|
|
@@ -6192,11 +6199,15 @@ function extractCSharpCalls(tree, cache) {
|
|
|
6192
6199
|
const fn = inv.childForFieldName("function");
|
|
6193
6200
|
let methodName = "unknown";
|
|
6194
6201
|
let receiver = null;
|
|
6202
|
+
let receiverTypeFromCreation = null;
|
|
6195
6203
|
if (fn?.type === "member_access_expression") {
|
|
6196
6204
|
const nameNode = fn.childForFieldName("name");
|
|
6197
6205
|
const exprNode = fn.childForFieldName("expression");
|
|
6198
6206
|
methodName = nameNode ? csharpBareName(nameNode) : "unknown";
|
|
6199
6207
|
receiver = exprNode ? getNodeText(exprNode) : null;
|
|
6208
|
+
if (exprNode?.type === "object_creation_expression") {
|
|
6209
|
+
receiverTypeFromCreation = csharpCreatedSimpleType(exprNode);
|
|
6210
|
+
}
|
|
6200
6211
|
} else if (fn) {
|
|
6201
6212
|
methodName = csharpBareName(fn);
|
|
6202
6213
|
}
|
|
@@ -6214,7 +6225,7 @@ function extractCSharpCalls(tree, cache) {
|
|
|
6214
6225
|
calls.push({
|
|
6215
6226
|
method_name: methodName,
|
|
6216
6227
|
receiver,
|
|
6217
|
-
receiver_type: receiver ? typeMap.get(receiver) ?? null : null,
|
|
6228
|
+
receiver_type: receiverTypeFromCreation ?? (receiver ? typeMap.get(receiver) ?? null : null),
|
|
6218
6229
|
receiver_type_fqn: null,
|
|
6219
6230
|
arguments: args2,
|
|
6220
6231
|
location: { line: inv.startPosition.row + 1, column: inv.startPosition.column },
|
|
@@ -10185,6 +10196,36 @@ function extractFieldDefs(body2, startId) {
|
|
|
10185
10196
|
function extractJSParameterDefs(params, startId) {
|
|
10186
10197
|
const defs = [];
|
|
10187
10198
|
let id = startId;
|
|
10199
|
+
const addPatternDefs = (patternNode, line) => {
|
|
10200
|
+
if (patternNode.type === "identifier" || patternNode.type === "shorthand_property_identifier_pattern") {
|
|
10201
|
+
defs.push({
|
|
10202
|
+
id: id++,
|
|
10203
|
+
variable: getNodeText(patternNode),
|
|
10204
|
+
line,
|
|
10205
|
+
kind: "param"
|
|
10206
|
+
});
|
|
10207
|
+
return;
|
|
10208
|
+
}
|
|
10209
|
+
if (patternNode.type === "rest_pattern" || patternNode.type === "rest_element") {
|
|
10210
|
+
const nameNode = patternNode.namedChildCount > 0 ? patternNode.namedChild(0) : null;
|
|
10211
|
+
if (!nameNode) return;
|
|
10212
|
+
addPatternDefs(nameNode, line);
|
|
10213
|
+
return;
|
|
10214
|
+
}
|
|
10215
|
+
if (patternNode.type === "assignment_pattern") {
|
|
10216
|
+
const leftNode = patternNode.childForFieldName("left");
|
|
10217
|
+
if (leftNode) addPatternDefs(leftNode, line);
|
|
10218
|
+
return;
|
|
10219
|
+
}
|
|
10220
|
+
if (patternNode.type === "object_pattern" || patternNode.type === "array_pattern") {
|
|
10221
|
+
const destructDefs = extractDestructuringDefs(patternNode, id);
|
|
10222
|
+
for (const def of destructDefs) {
|
|
10223
|
+
def.kind = "param";
|
|
10224
|
+
defs.push(def);
|
|
10225
|
+
id++;
|
|
10226
|
+
}
|
|
10227
|
+
}
|
|
10228
|
+
};
|
|
10188
10229
|
for (let i2 = 0; i2 < params.childCount; i2++) {
|
|
10189
10230
|
const param = params.child(i2);
|
|
10190
10231
|
if (!param) continue;
|
|
@@ -10232,14 +10273,7 @@ function extractJSParameterDefs(params, startId) {
|
|
|
10232
10273
|
}
|
|
10233
10274
|
} else if (param.type === "required_parameter" || param.type === "optional_parameter") {
|
|
10234
10275
|
const patternNode = param.childForFieldName("pattern");
|
|
10235
|
-
if (patternNode
|
|
10236
|
-
defs.push({
|
|
10237
|
-
id: id++,
|
|
10238
|
-
variable: getNodeText(patternNode),
|
|
10239
|
-
line: param.startPosition.row + 1,
|
|
10240
|
-
kind: "param"
|
|
10241
|
-
});
|
|
10242
|
-
}
|
|
10276
|
+
if (patternNode) addPatternDefs(patternNode, param.startPosition.row + 1);
|
|
10243
10277
|
}
|
|
10244
10278
|
}
|
|
10245
10279
|
return defs;
|
|
@@ -11995,6 +12029,9 @@ var DEFAULT_SOURCES = [
|
|
|
11995
12029
|
// entry.getName() returns a path that may contain ../ — flowing into File()/FileOutputStream()
|
|
11996
12030
|
// is a classic Zip-Slip vulnerability.
|
|
11997
12031
|
{ method: "getName", class: "ZipEntry", type: "file_input", severity: "high", return_tainted: true },
|
|
12032
|
+
// JarEntry extends ZipEntry. A declared JarEntry does not match the ZipEntry
|
|
12033
|
+
// row, and a receiver not named `entry` misses the name heuristic. cognium-dev#620.
|
|
12034
|
+
{ method: "getName", class: "JarEntry", type: "file_input", severity: "high", return_tainted: true },
|
|
11998
12035
|
{ method: "getName", class: "ZipArchiveEntry", type: "file_input", severity: "high", return_tainted: true },
|
|
11999
12036
|
{ method: "getName", class: "TarArchiveEntry", type: "file_input", severity: "high", return_tainted: true },
|
|
12000
12037
|
{ method: "getName", class: "ArchiveEntry", type: "file_input", severity: "high", return_tainted: true },
|
|
@@ -12751,8 +12788,10 @@ var DEFAULT_SINKS = [
|
|
|
12751
12788
|
{ method: "checkout", class: "SCM", type: "command_injection", cwe: "CWE-78", severity: "critical", arg_positions: [0] },
|
|
12752
12789
|
{ method: "retrieve", class: "LibraryAdder", type: "command_injection", cwe: "CWE-78", severity: "critical", arg_positions: [0] },
|
|
12753
12790
|
{ method: "add", class: "LibraryAdder", type: "command_injection", cwe: "CWE-78", severity: "critical", arg_positions: [0] },
|
|
12754
|
-
// Jenkins CPS
|
|
12755
|
-
|
|
12791
|
+
// Jenkins CPS compiles the script argument. That is code injection (CWE-94),
|
|
12792
|
+
// the same class as GroovyShell.evaluate. Registered as command_injection it
|
|
12793
|
+
// was dropped by the CWE-78 constructor allowlist (#129). cognium-dev#619.
|
|
12794
|
+
{ method: "CpsFlowExecution", class: "constructor", type: "code_injection", cwe: "CWE-94", severity: "critical", arg_positions: [0] },
|
|
12756
12795
|
{ method: "CpsFlowDefinition", class: "constructor", type: "command_injection", cwe: "CWE-78", severity: "critical", arg_positions: [0] },
|
|
12757
12796
|
{ method: "FlowExecution", class: "constructor", type: "command_injection", cwe: "CWE-78", severity: "critical", arg_positions: [0] },
|
|
12758
12797
|
// ActiveMQ control commands
|
|
@@ -15750,7 +15789,8 @@ function findSources(calls, types, patterns, sourceLines, language) {
|
|
|
15750
15789
|
if (skipMethods.includes(method.name)) continue;
|
|
15751
15790
|
for (const param of method.parameters) {
|
|
15752
15791
|
const hasCSharpBindingAttr = language === "csharp" && param.annotations.some((a) => CSHARP_BINDING_ATTRS.has(a));
|
|
15753
|
-
const
|
|
15792
|
+
const isDestructureBinding = param.name.startsWith("{") || param.name.startsWith("[");
|
|
15793
|
+
const isTaintable = hasCSharpBindingAttr ? true : isDestructureBinding ? true : param.type ? isInterproceduralTaintableType(param.type, language) : true;
|
|
15754
15794
|
if (isTaintable) {
|
|
15755
15795
|
const paramLine = param.line ?? method.start_line;
|
|
15756
15796
|
sources.push({
|
|
@@ -15980,12 +16020,14 @@ function isInterproceduralTaintableType(typeName, language) {
|
|
|
15980
16020
|
}
|
|
15981
16021
|
function isParameterizedQueryCall(call, pattern) {
|
|
15982
16022
|
if (pattern.type !== "sql_injection") return false;
|
|
15983
|
-
const
|
|
16023
|
+
const queryPos = pattern.arg_positions.length > 0 ? Math.min(...pattern.arg_positions) : 0;
|
|
16024
|
+
const queryArg = call.arguments.find((a) => a.position === queryPos);
|
|
15984
16025
|
if (queryArg) {
|
|
15985
16026
|
const queryText = queryArg.literal ?? queryArg.expression ?? "";
|
|
15986
16027
|
const hasPlaceholders = /(\?(?:\s|,|$|\))|\$\d+|:\w+|%s)/.test(queryText);
|
|
15987
16028
|
const hasConcatenation = /\+\s*[^+]/.test(queryText) || queryText.includes("${");
|
|
15988
|
-
|
|
16029
|
+
const hasBoundArg = queryPos === 0 ? call.arguments.length >= 2 : call.arguments.some((a) => a.position > queryPos);
|
|
16030
|
+
if (hasPlaceholders && !hasConcatenation && hasBoundArg) {
|
|
15989
16031
|
return true;
|
|
15990
16032
|
}
|
|
15991
16033
|
}
|
|
@@ -16635,6 +16677,14 @@ function isSafeJinjaRenderCall(call, pattern, language, sourceLines) {
|
|
|
16635
16677
|
}
|
|
16636
16678
|
return false;
|
|
16637
16679
|
}
|
|
16680
|
+
function isArgumentlessJsResponseWriter(call, pattern, language) {
|
|
16681
|
+
if (pattern.type !== "xss" || pattern.class !== "Response") return false;
|
|
16682
|
+
if (language !== "javascript" && language !== "typescript") return false;
|
|
16683
|
+
const positions = pattern.arg_positions;
|
|
16684
|
+
if (!positions || positions.length === 0) return false;
|
|
16685
|
+
const argc = call.arguments?.length ?? 0;
|
|
16686
|
+
return positions.every((pos) => pos >= argc);
|
|
16687
|
+
}
|
|
16638
16688
|
function findSinks(calls, patterns, typeHierarchy, language, sourceLines, types) {
|
|
16639
16689
|
const patternsForLanguage = language === void 0 ? patterns : patterns.filter((p) => sinkPatternAppliesTo(p, language));
|
|
16640
16690
|
const sinkMap = /* @__PURE__ */ new Map();
|
|
@@ -16695,6 +16745,9 @@ function findSinks(calls, patterns, typeHierarchy, language, sourceLines, types)
|
|
|
16695
16745
|
continue;
|
|
16696
16746
|
}
|
|
16697
16747
|
}
|
|
16748
|
+
if (isArgumentlessJsResponseWriter(call, pattern, language)) {
|
|
16749
|
+
continue;
|
|
16750
|
+
}
|
|
16698
16751
|
if ((pattern.type === "command_injection" || pattern.type === "sql_injection") && call.method_name === "exec" && (language === "javascript" || language === "typescript") && isRegexReceiver(call.receiver, sourceLines)) {
|
|
16699
16752
|
continue;
|
|
16700
16753
|
}
|
|
@@ -19226,6 +19279,9 @@ function buildSanitizersByLine(sanitizers) {
|
|
|
19226
19279
|
}
|
|
19227
19280
|
return out2;
|
|
19228
19281
|
}
|
|
19282
|
+
function concatOutsideStrings(expression) {
|
|
19283
|
+
return expression.replace(/"(?:\\.|[^"\\])*"|'(?:\\.|[^'\\])*'/g, " ").includes("+");
|
|
19284
|
+
}
|
|
19229
19285
|
function propagateTaint(graphOrDfg, callsOrSources, sourcesOrSinks, sinksOrSanitizers, sanitizersArg) {
|
|
19230
19286
|
let graph;
|
|
19231
19287
|
let sources;
|
|
@@ -19257,6 +19313,7 @@ function propagateTaint(graphOrDfg, callsOrSources, sourcesOrSinks, sinksOrSanit
|
|
|
19257
19313
|
}
|
|
19258
19314
|
const taintedVars = [];
|
|
19259
19315
|
const flows = [];
|
|
19316
|
+
const javaConcat = graph.ir.meta.language === "java";
|
|
19260
19317
|
const reachableSinks = /* @__PURE__ */ new Map();
|
|
19261
19318
|
const defsByLine = graph.defsByLine;
|
|
19262
19319
|
const usesByLine = graph.usesByLine;
|
|
@@ -19293,12 +19350,14 @@ function propagateTaint(graphOrDfg, callsOrSources, sourcesOrSinks, sinksOrSanit
|
|
|
19293
19350
|
continue;
|
|
19294
19351
|
}
|
|
19295
19352
|
}
|
|
19296
|
-
const
|
|
19353
|
+
const expr = typeof arg.expression === "string" ? arg.expression : "";
|
|
19354
|
+
const compoundConcat = javaConcat && !!arg.variable && expr !== arg.variable && concatOutsideStrings(expr);
|
|
19355
|
+
const candidateUses = arg.variable && !compoundConcat ? usesAtSink.filter((u) => u.variable === arg.variable) : usesAtSink;
|
|
19297
19356
|
{
|
|
19298
19357
|
for (const use of candidateUses) {
|
|
19299
19358
|
if (use.def_id !== null) {
|
|
19300
19359
|
if (allTaintedDefIds.has(use.def_id)) {
|
|
19301
|
-
if (!arg.variable) {
|
|
19360
|
+
if (!arg.variable || compoundConcat) {
|
|
19302
19361
|
if (typeof arg.expression !== "string" || arg.expression.length === 0) continue;
|
|
19303
19362
|
const re = new RegExp(`(?:^|[^A-Za-z0-9_$])${use.variable.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}(?:[^A-Za-z0-9_$]|$)`);
|
|
19304
19363
|
if (!re.test(arg.expression)) continue;
|
|
@@ -19961,6 +20020,8 @@ function analyzeInterprocedural(graphOrTypes, callsOrSources, dfgOrSinks, source
|
|
|
19961
20020
|
"QueryRowContext",
|
|
19962
20021
|
"Exec",
|
|
19963
20022
|
"ExecContext",
|
|
20023
|
+
"Prepare",
|
|
20024
|
+
"PrepareContext",
|
|
19964
20025
|
// Go html/template escape helpers — explicit safe utilities.
|
|
19965
20026
|
// Belt-and-suspenders with the sanitizer config so external_taint_escape
|
|
19966
20027
|
// is suppressed regardless of class-prefix matching. (cognium-dev #102 FP-27)
|
|
@@ -26230,6 +26291,16 @@ var VuePlugin = class extends BaseLanguagePlugin {
|
|
|
26230
26291
|
};
|
|
26231
26292
|
|
|
26232
26293
|
// src/languages/plugins/go.ts
|
|
26294
|
+
function sqlSinks(cls, methods) {
|
|
26295
|
+
return methods.map(([method, argPositions]) => ({
|
|
26296
|
+
method,
|
|
26297
|
+
class: cls,
|
|
26298
|
+
type: "sql_injection",
|
|
26299
|
+
cwe: "CWE-89",
|
|
26300
|
+
severity: "critical",
|
|
26301
|
+
argPositions
|
|
26302
|
+
}));
|
|
26303
|
+
}
|
|
26233
26304
|
var GoPlugin = class extends BaseLanguagePlugin {
|
|
26234
26305
|
id = "go";
|
|
26235
26306
|
name = "Go";
|
|
@@ -26451,6 +26522,35 @@ var GoPlugin = class extends BaseLanguagePlugin {
|
|
|
26451
26522
|
severity: "critical",
|
|
26452
26523
|
argPositions: [0]
|
|
26453
26524
|
},
|
|
26525
|
+
// database/sql context and transaction forms (#583). The query is
|
|
26526
|
+
// argument 0 for the plain forms and argument 1 for the *Context
|
|
26527
|
+
// forms (argument 0 is the context). Conn is the same shape as DB.
|
|
26528
|
+
...sqlSinks("DB", [
|
|
26529
|
+
["QueryContext", [1]],
|
|
26530
|
+
["QueryRowContext", [1]],
|
|
26531
|
+
["ExecContext", [1]],
|
|
26532
|
+
["Prepare", [0]],
|
|
26533
|
+
["PrepareContext", [1]]
|
|
26534
|
+
]),
|
|
26535
|
+
...sqlSinks("Tx", [
|
|
26536
|
+
["Exec", [0]],
|
|
26537
|
+
["QueryRow", [0]],
|
|
26538
|
+
["QueryContext", [1]],
|
|
26539
|
+
["QueryRowContext", [1]],
|
|
26540
|
+
["ExecContext", [1]],
|
|
26541
|
+
["Prepare", [0]],
|
|
26542
|
+
["PrepareContext", [1]]
|
|
26543
|
+
]),
|
|
26544
|
+
...sqlSinks("Conn", [
|
|
26545
|
+
["Query", [0]],
|
|
26546
|
+
["QueryRow", [0]],
|
|
26547
|
+
["Exec", [0]],
|
|
26548
|
+
["QueryContext", [1]],
|
|
26549
|
+
["QueryRowContext", [1]],
|
|
26550
|
+
["ExecContext", [1]],
|
|
26551
|
+
["Prepare", [0]],
|
|
26552
|
+
["PrepareContext", [1]]
|
|
26553
|
+
]),
|
|
26454
26554
|
// Command Injection — argPositions intentionally empty so all variadic
|
|
26455
26555
|
// positions are scanned. `exec.Command("sh", "-c", taintedCmd)` puts the
|
|
26456
26556
|
// injection at arg[2]; `exec.Command(taintedName, args...)` puts it at
|
|
@@ -37274,6 +37374,20 @@ function evaluateSimpleExpression(expr, symbols) {
|
|
|
37274
37374
|
if (result !== null && !isNaN(result)) return String(Math.floor(result));
|
|
37275
37375
|
return expr;
|
|
37276
37376
|
}
|
|
37377
|
+
var ARGUMENT_IDENT_SKIP = /* @__PURE__ */ new Set(["new", "true", "false", "null", "this", "super"]);
|
|
37378
|
+
function isStringConcat(expression) {
|
|
37379
|
+
return expression.replace(/"(?:\\.|[^"\\])*"|'(?:\\.|[^'\\])*'/g, " ").includes("+");
|
|
37380
|
+
}
|
|
37381
|
+
function argumentIdentifiers(expression) {
|
|
37382
|
+
const stripped = expression.replace(/"(?:\\.|[^"\\])*"|'(?:\\.|[^'\\])*'/g, " ");
|
|
37383
|
+
const found = stripped.match(/[A-Za-z_$][A-Za-z0-9_$]*/g) ?? [];
|
|
37384
|
+
const out2 = [];
|
|
37385
|
+
for (const id of found) {
|
|
37386
|
+
if (ARGUMENT_IDENT_SKIP.has(id)) continue;
|
|
37387
|
+
if (!out2.includes(id)) out2.push(id);
|
|
37388
|
+
}
|
|
37389
|
+
return out2;
|
|
37390
|
+
}
|
|
37277
37391
|
function isStringLiteralExpression(expr) {
|
|
37278
37392
|
const trimmed = expr.trim();
|
|
37279
37393
|
if (trimmed.length < 2) return false;
|
|
@@ -37345,20 +37459,27 @@ function filterCleanVariableSinks(sinks, calls, taintedVars, symbols, dfg, sanit
|
|
|
37345
37459
|
dangerousArgCount++;
|
|
37346
37460
|
if (language === "bash" && arg.expression === call.method_name && !arg.variable && arg.literal == null) continue;
|
|
37347
37461
|
if (arg.variable && !arg.expression?.includes("[")) {
|
|
37348
|
-
const
|
|
37349
|
-
const
|
|
37350
|
-
|
|
37351
|
-
|
|
37352
|
-
|
|
37353
|
-
|
|
37354
|
-
|
|
37355
|
-
|
|
37356
|
-
|
|
37357
|
-
|
|
37462
|
+
const expr = arg.expression ?? "";
|
|
37463
|
+
const concatNames = language === "java" && isStringConcat(expr) ? argumentIdentifiers(expr) : [];
|
|
37464
|
+
const names = concatNames.length > 0 ? concatNames : [arg.variable];
|
|
37465
|
+
let argClean = true;
|
|
37466
|
+
for (const varName of names) {
|
|
37467
|
+
const scopedName = methodName ? `${methodName}:${varName}` : varName;
|
|
37468
|
+
if (fieldNames.has(varName) && !isInSynchronizedBlock) {
|
|
37469
|
+
argClean = false;
|
|
37470
|
+
break;
|
|
37471
|
+
}
|
|
37472
|
+
if (sanitizedVars?.has(scopedName) || sanitizedVars?.has(varName)) continue;
|
|
37473
|
+
if (taintedVars.has(scopedName) || taintedVars.has(varName)) {
|
|
37474
|
+
argClean = false;
|
|
37475
|
+
break;
|
|
37476
|
+
}
|
|
37477
|
+
const symbolValue = symbols.get(scopedName) ?? symbols.get(varName);
|
|
37478
|
+
if (symbolValue && symbolValue.type !== "unknown") continue;
|
|
37479
|
+
argClean = false;
|
|
37480
|
+
break;
|
|
37358
37481
|
}
|
|
37359
|
-
|
|
37360
|
-
if (symbolValue && symbolValue.type !== "unknown") continue;
|
|
37361
|
-
allArgsAreClean = false;
|
|
37482
|
+
if (!argClean) allArgsAreClean = false;
|
|
37362
37483
|
} else {
|
|
37363
37484
|
if (arg.literal != null) continue;
|
|
37364
37485
|
if (arg.expression && !arg.variable && isStringLiteralExpression(arg.expression)) continue;
|
|
@@ -44259,7 +44380,7 @@ function isGeneratedFile(file) {
|
|
|
44259
44380
|
var PROVIDER_PATTERNS = [
|
|
44260
44381
|
{
|
|
44261
44382
|
name: "AWS access key",
|
|
44262
|
-
regex: /\
|
|
44383
|
+
regex: /\b(?:AKIA|ASIA)[0-9A-Z]{16}\b/,
|
|
44263
44384
|
severity: "critical",
|
|
44264
44385
|
level: "error",
|
|
44265
44386
|
fix: "Rotate the AWS access key immediately and move it to an environment variable or AWS Secrets Manager."
|
|
@@ -44334,6 +44455,22 @@ var PROVIDER_PATTERNS = [
|
|
|
44334
44455
|
level: "warning",
|
|
44335
44456
|
fix: "Publishable keys are not secret but should still not be checked in to back-end source files; verify front-end vs back-end context."
|
|
44336
44457
|
},
|
|
44458
|
+
// Test-mode secret and restricted keys still authorize the test account.
|
|
44459
|
+
// Same length floor as the live forms. Severity is high, not critical (#576).
|
|
44460
|
+
{
|
|
44461
|
+
name: "Stripe test secret key",
|
|
44462
|
+
regex: /\bsk_test_[A-Za-z0-9]{24,}\b/,
|
|
44463
|
+
severity: "high",
|
|
44464
|
+
level: "warning",
|
|
44465
|
+
fix: "Rotate the Stripe test secret key in the Stripe Dashboard and load it from a secrets manager."
|
|
44466
|
+
},
|
|
44467
|
+
{
|
|
44468
|
+
name: "Stripe test restricted key",
|
|
44469
|
+
regex: /\brk_test_[A-Za-z0-9]{24,}\b/,
|
|
44470
|
+
severity: "high",
|
|
44471
|
+
level: "warning",
|
|
44472
|
+
fix: "Rotate the Stripe test restricted key in the Stripe Dashboard and load it from a secrets manager."
|
|
44473
|
+
},
|
|
44337
44474
|
{
|
|
44338
44475
|
name: "OpenAI API key",
|
|
44339
44476
|
regex: /\bsk-[A-Za-z0-9]{48}\b/,
|
|
@@ -44633,7 +44770,7 @@ var ScanSecretsPass = class {
|
|
|
44633
44770
|
for (const pattern of PROVIDER_PATTERNS) {
|
|
44634
44771
|
const m = pattern.regex.exec(lineText);
|
|
44635
44772
|
if (!m) continue;
|
|
44636
|
-
if ((pattern.name === "Stripe live restricted key" || pattern.name === "GitLab personal access token") && PLACEHOLDER_RE.test(m[0])) {
|
|
44773
|
+
if ((pattern.name === "Stripe live restricted key" || pattern.name === "Stripe test secret key" || pattern.name === "Stripe test restricted key" || pattern.name === "GitLab personal access token") && PLACEHOLDER_RE.test(m[0])) {
|
|
44637
44774
|
continue;
|
|
44638
44775
|
}
|
|
44639
44776
|
if (pattern.name === "PEM private key" && !pemHasInlineBody(lines, i2)) {
|
|
@@ -49402,6 +49539,22 @@ function longestLineLength(code) {
|
|
|
49402
49539
|
return max;
|
|
49403
49540
|
}
|
|
49404
49541
|
var MAX_ANALYZABLE_LINE_LENGTH = 1e4;
|
|
49542
|
+
function neutralizeOverlongLines(code) {
|
|
49543
|
+
if (longestLineLength(code) <= MAX_ANALYZABLE_LINE_LENGTH) {
|
|
49544
|
+
return { code, dropped: 0 };
|
|
49545
|
+
}
|
|
49546
|
+
let out2 = "";
|
|
49547
|
+
let start2 = 0;
|
|
49548
|
+
let dropped = 0;
|
|
49549
|
+
for (let i2 = 0; i2 <= code.length; i2++) {
|
|
49550
|
+
if (i2 !== code.length && code.charCodeAt(i2) !== 10) continue;
|
|
49551
|
+
if (i2 - start2 > MAX_ANALYZABLE_LINE_LENGTH) dropped++;
|
|
49552
|
+
else if (i2 > start2) out2 += code.slice(start2, i2);
|
|
49553
|
+
if (i2 < code.length) out2 += "\n";
|
|
49554
|
+
start2 = i2 + 1;
|
|
49555
|
+
}
|
|
49556
|
+
return { code: out2, dropped };
|
|
49557
|
+
}
|
|
49405
49558
|
async function analyze(code, filePath, language, options = {}) {
|
|
49406
49559
|
if (!initialized) {
|
|
49407
49560
|
await initAnalyzer(options);
|
|
@@ -49420,6 +49573,15 @@ async function analyze(code, filePath, language, options = {}) {
|
|
|
49420
49573
|
parseGrammar = "tsx";
|
|
49421
49574
|
}
|
|
49422
49575
|
}
|
|
49576
|
+
const neutralized = neutralizeOverlongLines(code);
|
|
49577
|
+
if (neutralized.dropped > 0) {
|
|
49578
|
+
logger.warn("Neutralized over-long lines before analysis", {
|
|
49579
|
+
filePath,
|
|
49580
|
+
language,
|
|
49581
|
+
droppedLines: neutralized.dropped
|
|
49582
|
+
});
|
|
49583
|
+
code = neutralized.code;
|
|
49584
|
+
}
|
|
49423
49585
|
logger.debug("Analyzing file", { filePath, language, parseGrammar, codeLength: code.length });
|
|
49424
49586
|
const phaseTimingEnabled = globalThis.__circleIrPassTiming === true;
|
|
49425
49587
|
const phaseStart = () => phaseTimingEnabled ? Date.now() : 0;
|
|
@@ -6230,6 +6230,13 @@ function csharpBareName(node) {
|
|
|
6230
6230
|
}
|
|
6231
6231
|
return getNodeText(node);
|
|
6232
6232
|
}
|
|
6233
|
+
function csharpCreatedSimpleType(creation) {
|
|
6234
|
+
const typeNode = creation.childForFieldName("type");
|
|
6235
|
+
if (!typeNode) return null;
|
|
6236
|
+
const bare = csharpBareName(typeNode).replace(/<[^>]*>/g, "");
|
|
6237
|
+
const simple = bare.split(".").pop()?.trim() ?? "";
|
|
6238
|
+
return simple && simple !== "var" ? simple : null;
|
|
6239
|
+
}
|
|
6233
6240
|
function extractCSharpCalls(tree, cache) {
|
|
6234
6241
|
const calls = [];
|
|
6235
6242
|
const typeMap = buildCSharpReceiverTypeMap(tree, cache);
|
|
@@ -6238,11 +6245,15 @@ function extractCSharpCalls(tree, cache) {
|
|
|
6238
6245
|
const fn = inv.childForFieldName("function");
|
|
6239
6246
|
let methodName = "unknown";
|
|
6240
6247
|
let receiver = null;
|
|
6248
|
+
let receiverTypeFromCreation = null;
|
|
6241
6249
|
if (fn?.type === "member_access_expression") {
|
|
6242
6250
|
const nameNode = fn.childForFieldName("name");
|
|
6243
6251
|
const exprNode = fn.childForFieldName("expression");
|
|
6244
6252
|
methodName = nameNode ? csharpBareName(nameNode) : "unknown";
|
|
6245
6253
|
receiver = exprNode ? getNodeText(exprNode) : null;
|
|
6254
|
+
if (exprNode?.type === "object_creation_expression") {
|
|
6255
|
+
receiverTypeFromCreation = csharpCreatedSimpleType(exprNode);
|
|
6256
|
+
}
|
|
6246
6257
|
} else if (fn) {
|
|
6247
6258
|
methodName = csharpBareName(fn);
|
|
6248
6259
|
}
|
|
@@ -6260,7 +6271,7 @@ function extractCSharpCalls(tree, cache) {
|
|
|
6260
6271
|
calls.push({
|
|
6261
6272
|
method_name: methodName,
|
|
6262
6273
|
receiver,
|
|
6263
|
-
receiver_type: receiver ? typeMap.get(receiver) ?? null : null,
|
|
6274
|
+
receiver_type: receiverTypeFromCreation ?? (receiver ? typeMap.get(receiver) ?? null : null),
|
|
6264
6275
|
receiver_type_fqn: null,
|
|
6265
6276
|
arguments: args2,
|
|
6266
6277
|
location: { line: inv.startPosition.row + 1, column: inv.startPosition.column },
|
|
@@ -10231,6 +10242,36 @@ function extractFieldDefs(body2, startId) {
|
|
|
10231
10242
|
function extractJSParameterDefs(params, startId) {
|
|
10232
10243
|
const defs = [];
|
|
10233
10244
|
let id = startId;
|
|
10245
|
+
const addPatternDefs = (patternNode, line) => {
|
|
10246
|
+
if (patternNode.type === "identifier" || patternNode.type === "shorthand_property_identifier_pattern") {
|
|
10247
|
+
defs.push({
|
|
10248
|
+
id: id++,
|
|
10249
|
+
variable: getNodeText(patternNode),
|
|
10250
|
+
line,
|
|
10251
|
+
kind: "param"
|
|
10252
|
+
});
|
|
10253
|
+
return;
|
|
10254
|
+
}
|
|
10255
|
+
if (patternNode.type === "rest_pattern" || patternNode.type === "rest_element") {
|
|
10256
|
+
const nameNode = patternNode.namedChildCount > 0 ? patternNode.namedChild(0) : null;
|
|
10257
|
+
if (!nameNode) return;
|
|
10258
|
+
addPatternDefs(nameNode, line);
|
|
10259
|
+
return;
|
|
10260
|
+
}
|
|
10261
|
+
if (patternNode.type === "assignment_pattern") {
|
|
10262
|
+
const leftNode = patternNode.childForFieldName("left");
|
|
10263
|
+
if (leftNode) addPatternDefs(leftNode, line);
|
|
10264
|
+
return;
|
|
10265
|
+
}
|
|
10266
|
+
if (patternNode.type === "object_pattern" || patternNode.type === "array_pattern") {
|
|
10267
|
+
const destructDefs = extractDestructuringDefs(patternNode, id);
|
|
10268
|
+
for (const def of destructDefs) {
|
|
10269
|
+
def.kind = "param";
|
|
10270
|
+
defs.push(def);
|
|
10271
|
+
id++;
|
|
10272
|
+
}
|
|
10273
|
+
}
|
|
10274
|
+
};
|
|
10234
10275
|
for (let i2 = 0; i2 < params.childCount; i2++) {
|
|
10235
10276
|
const param = params.child(i2);
|
|
10236
10277
|
if (!param) continue;
|
|
@@ -10278,14 +10319,7 @@ function extractJSParameterDefs(params, startId) {
|
|
|
10278
10319
|
}
|
|
10279
10320
|
} else if (param.type === "required_parameter" || param.type === "optional_parameter") {
|
|
10280
10321
|
const patternNode = param.childForFieldName("pattern");
|
|
10281
|
-
if (patternNode
|
|
10282
|
-
defs.push({
|
|
10283
|
-
id: id++,
|
|
10284
|
-
variable: getNodeText(patternNode),
|
|
10285
|
-
line: param.startPosition.row + 1,
|
|
10286
|
-
kind: "param"
|
|
10287
|
-
});
|
|
10288
|
-
}
|
|
10322
|
+
if (patternNode) addPatternDefs(patternNode, param.startPosition.row + 1);
|
|
10289
10323
|
}
|
|
10290
10324
|
}
|
|
10291
10325
|
return defs;
|
|
@@ -11414,6 +11448,9 @@ var DEFAULT_SOURCES = [
|
|
|
11414
11448
|
// entry.getName() returns a path that may contain ../ — flowing into File()/FileOutputStream()
|
|
11415
11449
|
// is a classic Zip-Slip vulnerability.
|
|
11416
11450
|
{ method: "getName", class: "ZipEntry", type: "file_input", severity: "high", return_tainted: true },
|
|
11451
|
+
// JarEntry extends ZipEntry. A declared JarEntry does not match the ZipEntry
|
|
11452
|
+
// row, and a receiver not named `entry` misses the name heuristic. cognium-dev#620.
|
|
11453
|
+
{ method: "getName", class: "JarEntry", type: "file_input", severity: "high", return_tainted: true },
|
|
11417
11454
|
{ method: "getName", class: "ZipArchiveEntry", type: "file_input", severity: "high", return_tainted: true },
|
|
11418
11455
|
{ method: "getName", class: "TarArchiveEntry", type: "file_input", severity: "high", return_tainted: true },
|
|
11419
11456
|
{ method: "getName", class: "ArchiveEntry", type: "file_input", severity: "high", return_tainted: true },
|
|
@@ -12170,8 +12207,10 @@ var DEFAULT_SINKS = [
|
|
|
12170
12207
|
{ method: "checkout", class: "SCM", type: "command_injection", cwe: "CWE-78", severity: "critical", arg_positions: [0] },
|
|
12171
12208
|
{ method: "retrieve", class: "LibraryAdder", type: "command_injection", cwe: "CWE-78", severity: "critical", arg_positions: [0] },
|
|
12172
12209
|
{ method: "add", class: "LibraryAdder", type: "command_injection", cwe: "CWE-78", severity: "critical", arg_positions: [0] },
|
|
12173
|
-
// Jenkins CPS
|
|
12174
|
-
|
|
12210
|
+
// Jenkins CPS compiles the script argument. That is code injection (CWE-94),
|
|
12211
|
+
// the same class as GroovyShell.evaluate. Registered as command_injection it
|
|
12212
|
+
// was dropped by the CWE-78 constructor allowlist (#129). cognium-dev#619.
|
|
12213
|
+
{ method: "CpsFlowExecution", class: "constructor", type: "code_injection", cwe: "CWE-94", severity: "critical", arg_positions: [0] },
|
|
12175
12214
|
{ method: "CpsFlowDefinition", class: "constructor", type: "command_injection", cwe: "CWE-78", severity: "critical", arg_positions: [0] },
|
|
12176
12215
|
{ method: "FlowExecution", class: "constructor", type: "command_injection", cwe: "CWE-78", severity: "critical", arg_positions: [0] },
|
|
12177
12216
|
// ActiveMQ control commands
|
|
@@ -15050,7 +15089,8 @@ function findSources(calls, types, patterns, sourceLines, language) {
|
|
|
15050
15089
|
if (skipMethods.includes(method.name)) continue;
|
|
15051
15090
|
for (const param of method.parameters) {
|
|
15052
15091
|
const hasCSharpBindingAttr = language === "csharp" && param.annotations.some((a) => CSHARP_BINDING_ATTRS.has(a));
|
|
15053
|
-
const
|
|
15092
|
+
const isDestructureBinding = param.name.startsWith("{") || param.name.startsWith("[");
|
|
15093
|
+
const isTaintable = hasCSharpBindingAttr ? true : isDestructureBinding ? true : param.type ? isInterproceduralTaintableType(param.type, language) : true;
|
|
15054
15094
|
if (isTaintable) {
|
|
15055
15095
|
const paramLine = param.line ?? method.start_line;
|
|
15056
15096
|
sources.push({
|
|
@@ -15280,12 +15320,14 @@ function isInterproceduralTaintableType(typeName, language) {
|
|
|
15280
15320
|
}
|
|
15281
15321
|
function isParameterizedQueryCall(call, pattern) {
|
|
15282
15322
|
if (pattern.type !== "sql_injection") return false;
|
|
15283
|
-
const
|
|
15323
|
+
const queryPos = pattern.arg_positions.length > 0 ? Math.min(...pattern.arg_positions) : 0;
|
|
15324
|
+
const queryArg = call.arguments.find((a) => a.position === queryPos);
|
|
15284
15325
|
if (queryArg) {
|
|
15285
15326
|
const queryText = queryArg.literal ?? queryArg.expression ?? "";
|
|
15286
15327
|
const hasPlaceholders = /(\?(?:\s|,|$|\))|\$\d+|:\w+|%s)/.test(queryText);
|
|
15287
15328
|
const hasConcatenation = /\+\s*[^+]/.test(queryText) || queryText.includes("${");
|
|
15288
|
-
|
|
15329
|
+
const hasBoundArg = queryPos === 0 ? call.arguments.length >= 2 : call.arguments.some((a) => a.position > queryPos);
|
|
15330
|
+
if (hasPlaceholders && !hasConcatenation && hasBoundArg) {
|
|
15289
15331
|
return true;
|
|
15290
15332
|
}
|
|
15291
15333
|
}
|
|
@@ -15935,6 +15977,14 @@ function isSafeJinjaRenderCall(call, pattern, language, sourceLines) {
|
|
|
15935
15977
|
}
|
|
15936
15978
|
return false;
|
|
15937
15979
|
}
|
|
15980
|
+
function isArgumentlessJsResponseWriter(call, pattern, language) {
|
|
15981
|
+
if (pattern.type !== "xss" || pattern.class !== "Response") return false;
|
|
15982
|
+
if (language !== "javascript" && language !== "typescript") return false;
|
|
15983
|
+
const positions = pattern.arg_positions;
|
|
15984
|
+
if (!positions || positions.length === 0) return false;
|
|
15985
|
+
const argc = call.arguments?.length ?? 0;
|
|
15986
|
+
return positions.every((pos) => pos >= argc);
|
|
15987
|
+
}
|
|
15938
15988
|
function findSinks(calls, patterns, typeHierarchy, language, sourceLines, types) {
|
|
15939
15989
|
const patternsForLanguage = language === void 0 ? patterns : patterns.filter((p) => sinkPatternAppliesTo(p, language));
|
|
15940
15990
|
const sinkMap = /* @__PURE__ */ new Map();
|
|
@@ -15995,6 +16045,9 @@ function findSinks(calls, patterns, typeHierarchy, language, sourceLines, types)
|
|
|
15995
16045
|
continue;
|
|
15996
16046
|
}
|
|
15997
16047
|
}
|
|
16048
|
+
if (isArgumentlessJsResponseWriter(call, pattern, language)) {
|
|
16049
|
+
continue;
|
|
16050
|
+
}
|
|
15998
16051
|
if ((pattern.type === "command_injection" || pattern.type === "sql_injection") && call.method_name === "exec" && (language === "javascript" || language === "typescript") && isRegexReceiver(call.receiver, sourceLines)) {
|
|
15999
16052
|
continue;
|
|
16000
16053
|
}
|
|
@@ -17191,6 +17244,9 @@ function buildSanitizersByLine(sanitizers) {
|
|
|
17191
17244
|
}
|
|
17192
17245
|
return out2;
|
|
17193
17246
|
}
|
|
17247
|
+
function concatOutsideStrings(expression) {
|
|
17248
|
+
return expression.replace(/"(?:\\.|[^"\\])*"|'(?:\\.|[^'\\])*'/g, " ").includes("+");
|
|
17249
|
+
}
|
|
17194
17250
|
function propagateTaint(graphOrDfg, callsOrSources, sourcesOrSinks, sinksOrSanitizers, sanitizersArg) {
|
|
17195
17251
|
let graph;
|
|
17196
17252
|
let sources;
|
|
@@ -17222,6 +17278,7 @@ function propagateTaint(graphOrDfg, callsOrSources, sourcesOrSinks, sinksOrSanit
|
|
|
17222
17278
|
}
|
|
17223
17279
|
const taintedVars = [];
|
|
17224
17280
|
const flows = [];
|
|
17281
|
+
const javaConcat = graph.ir.meta.language === "java";
|
|
17225
17282
|
const reachableSinks = /* @__PURE__ */ new Map();
|
|
17226
17283
|
const defsByLine = graph.defsByLine;
|
|
17227
17284
|
const usesByLine = graph.usesByLine;
|
|
@@ -17258,12 +17315,14 @@ function propagateTaint(graphOrDfg, callsOrSources, sourcesOrSinks, sinksOrSanit
|
|
|
17258
17315
|
continue;
|
|
17259
17316
|
}
|
|
17260
17317
|
}
|
|
17261
|
-
const
|
|
17318
|
+
const expr = typeof arg.expression === "string" ? arg.expression : "";
|
|
17319
|
+
const compoundConcat = javaConcat && !!arg.variable && expr !== arg.variable && concatOutsideStrings(expr);
|
|
17320
|
+
const candidateUses = arg.variable && !compoundConcat ? usesAtSink.filter((u) => u.variable === arg.variable) : usesAtSink;
|
|
17262
17321
|
{
|
|
17263
17322
|
for (const use of candidateUses) {
|
|
17264
17323
|
if (use.def_id !== null) {
|
|
17265
17324
|
if (allTaintedDefIds.has(use.def_id)) {
|
|
17266
|
-
if (!arg.variable) {
|
|
17325
|
+
if (!arg.variable || compoundConcat) {
|
|
17267
17326
|
if (typeof arg.expression !== "string" || arg.expression.length === 0) continue;
|
|
17268
17327
|
const re = new RegExp(`(?:^|[^A-Za-z0-9_$])${use.variable.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}(?:[^A-Za-z0-9_$]|$)`);
|
|
17269
17328
|
if (!re.test(arg.expression)) continue;
|