circle-ir 4.14.1 → 4.14.2
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 +9 -2
- package/dist/analysis/config-loader.js.map +1 -1
- package/dist/analysis/passes/insecure-cookie-pass.d.ts +11 -4
- package/dist/analysis/passes/insecure-cookie-pass.d.ts.map +1 -1
- package/dist/analysis/passes/insecure-cookie-pass.js +30 -15
- package/dist/analysis/passes/insecure-cookie-pass.js.map +1 -1
- package/dist/analysis/passes/interprocedural-pass.d.ts.map +1 -1
- package/dist/analysis/passes/interprocedural-pass.js +11 -2
- package/dist/analysis/passes/interprocedural-pass.js.map +1 -1
- package/dist/analysis/passes/language-sources-pass.d.ts.map +1 -1
- package/dist/analysis/passes/language-sources-pass.js +52 -0
- package/dist/analysis/passes/language-sources-pass.js.map +1 -1
- package/dist/analysis/passes/scan-secrets-pass.d.ts.map +1 -1
- package/dist/analysis/passes/scan-secrets-pass.js +24 -3
- package/dist/analysis/passes/scan-secrets-pass.js.map +1 -1
- package/dist/analysis/passes/sink-filter-pass.d.ts +1 -1
- package/dist/analysis/passes/sink-filter-pass.d.ts.map +1 -1
- package/dist/analysis/passes/sink-filter-pass.js +48 -24
- package/dist/analysis/passes/sink-filter-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 +11 -3
- package/dist/analysis/passes/taint-propagation-pass.js.map +1 -1
- package/dist/analysis/taint-matcher.d.ts.map +1 -1
- package/dist/analysis/taint-matcher.js +5 -2
- package/dist/analysis/taint-matcher.js.map +1 -1
- package/dist/analysis/taint-propagation.d.ts +9 -0
- package/dist/analysis/taint-propagation.d.ts.map +1 -1
- package/dist/analysis/taint-propagation.js +120 -1
- package/dist/analysis/taint-propagation.js.map +1 -1
- package/dist/analyzer.js +1 -1
- package/dist/analyzer.js.map +1 -1
- package/dist/browser/circle-ir.js +264 -40
- package/dist/core/circle-ir-core.cjs +118 -4
- package/dist/core/circle-ir-core.js +118 -4
- package/dist/core/extractors/calls.js +19 -0
- package/dist/core/extractors/calls.js.map +1 -1
- package/dist/languages/plugins/go.d.ts.map +1 -1
- package/dist/languages/plugins/go.js +19 -0
- package/dist/languages/plugins/go.js.map +1 -1
- package/package.json +1 -1
|
@@ -8034,6 +8034,17 @@ function extractGoCallInfo(node) {
|
|
|
8034
8034
|
if (operand.type === "identifier") {
|
|
8035
8035
|
const resolved = resolveGoLocalReceiverType(opText, node);
|
|
8036
8036
|
receiver = resolved !== null ? resolved : opText;
|
|
8037
|
+
} else if (operand.type === "selector_expression" && methodName === "Get") {
|
|
8038
|
+
const fieldNode = operand.childForFieldName("field");
|
|
8039
|
+
const inner = operand.childForFieldName("operand");
|
|
8040
|
+
const fieldName = fieldNode ? getNodeText(fieldNode) : "";
|
|
8041
|
+
const innerName = inner && inner.type === "identifier" ? getNodeText(inner) : null;
|
|
8042
|
+
const innerType = innerName !== null ? resolveGoLocalReceiverType(innerName, node) : null;
|
|
8043
|
+
if (innerType === "Request" && (fieldName === "PostForm" || fieldName === "Form")) {
|
|
8044
|
+
receiver = fieldName === "PostForm" ? "RequestPostForm" : "RequestForm";
|
|
8045
|
+
} else {
|
|
8046
|
+
receiver = opText;
|
|
8047
|
+
}
|
|
8037
8048
|
} else {
|
|
8038
8049
|
receiver = opText;
|
|
8039
8050
|
}
|
|
@@ -13029,8 +13040,15 @@ var DEFAULT_SINKS = [
|
|
|
13029
13040
|
{ method: "setExpireTimeSeconds", class: "StaticFilesConfiguration", type: "path_traversal", cwe: "CWE-22", severity: "medium", arg_positions: [0] },
|
|
13030
13041
|
{ method: "configureJarCase", class: "StaticFilesConfiguration", type: "path_traversal", cwe: "CWE-22", severity: "high", arg_positions: [0] },
|
|
13031
13042
|
{ method: "toString", class: "StringUtils", type: "path_traversal", cwe: "CWE-22", severity: "medium", arg_positions: [0] },
|
|
13032
|
-
// Wildfly/Undertow servlet resource manager
|
|
13033
|
-
|
|
13043
|
+
// Wildfly/Undertow servlet resource manager.
|
|
13044
|
+
// ResourceManager / PathResourceManager / FileResourceManager /
|
|
13045
|
+
// ClassPathResourceManager are the interface and implementations WildFly
|
|
13046
|
+
// calls through (cognium-dev#622, CVE-2018-1047). Class-qualified only.
|
|
13047
|
+
{ method: "getResource", class: "ServletResourceManager", type: "path_traversal", cwe: "CWE-22", severity: "high", arg_positions: [0], languages: ["java"] },
|
|
13048
|
+
{ method: "getResource", class: "ResourceManager", type: "path_traversal", cwe: "CWE-22", severity: "high", arg_positions: [0], languages: ["java"] },
|
|
13049
|
+
{ method: "getResource", class: "PathResourceManager", type: "path_traversal", cwe: "CWE-22", severity: "high", arg_positions: [0], languages: ["java"] },
|
|
13050
|
+
{ method: "getResource", class: "FileResourceManager", type: "path_traversal", cwe: "CWE-22", severity: "high", arg_positions: [0], languages: ["java"] },
|
|
13051
|
+
{ method: "getResource", class: "ClassPathResourceManager", type: "path_traversal", cwe: "CWE-22", severity: "high", arg_positions: [0], languages: ["java"] },
|
|
13034
13052
|
// Yamcs file system bucket
|
|
13035
13053
|
{ method: "deleteObject", class: "FileSystemBucket", type: "path_traversal", cwe: "CWE-22", severity: "high", arg_positions: [0] },
|
|
13036
13054
|
// RocketMQ validators
|
|
@@ -16020,7 +16038,8 @@ function isInterproceduralTaintableType(typeName, language) {
|
|
|
16020
16038
|
}
|
|
16021
16039
|
function isParameterizedQueryCall(call, pattern) {
|
|
16022
16040
|
if (pattern.type !== "sql_injection") return false;
|
|
16023
|
-
const
|
|
16041
|
+
const positions = pattern.arg_positions ?? [];
|
|
16042
|
+
const queryPos = positions.length > 0 ? Math.min(...positions) : 0;
|
|
16024
16043
|
const queryArg = call.arguments.find((a) => a.position === queryPos);
|
|
16025
16044
|
if (queryArg) {
|
|
16026
16045
|
const queryText = queryArg.literal ?? queryArg.expression ?? "";
|
|
@@ -19364,11 +19383,26 @@ function propagateTaint(graphOrDfg, callsOrSources, sourcesOrSinks, sinksOrSanit
|
|
|
19364
19383
|
}
|
|
19365
19384
|
const taintInfo = taintByDefId.get(use.def_id);
|
|
19366
19385
|
if (taintInfo) {
|
|
19386
|
+
let sanMap = sanitizersByLine;
|
|
19387
|
+
if (identifierOutsideSanitizerCalls(
|
|
19388
|
+
use.variable,
|
|
19389
|
+
callsAtSink,
|
|
19390
|
+
sanitizersByLine.get(sink.line) ?? [],
|
|
19391
|
+
sink.type
|
|
19392
|
+
)) {
|
|
19393
|
+
const stripped = new Map(sanitizersByLine);
|
|
19394
|
+
const kept = (sanitizersByLine.get(sink.line) ?? []).filter(
|
|
19395
|
+
(s) => !sanitizerCoversSink(s, sink.type)
|
|
19396
|
+
);
|
|
19397
|
+
if (kept.length === 0) stripped.delete(sink.line);
|
|
19398
|
+
else stripped.set(sink.line, kept);
|
|
19399
|
+
sanMap = stripped;
|
|
19400
|
+
}
|
|
19367
19401
|
const isSanitized = checkSanitized(
|
|
19368
19402
|
taintInfo.line,
|
|
19369
19403
|
sink.line,
|
|
19370
19404
|
sink.type,
|
|
19371
|
-
|
|
19405
|
+
sanMap,
|
|
19372
19406
|
{
|
|
19373
19407
|
startDefId: use.def_id,
|
|
19374
19408
|
chainsByToDef: graph.chainsByToDef,
|
|
@@ -19522,6 +19556,86 @@ var KNOWN_SINK_TYPES = /* @__PURE__ */ new Set([
|
|
|
19522
19556
|
"crlf",
|
|
19523
19557
|
"mass_assignment"
|
|
19524
19558
|
]);
|
|
19559
|
+
function readBalancedArg(text, openParen) {
|
|
19560
|
+
let depth = 0;
|
|
19561
|
+
let quote = null;
|
|
19562
|
+
let escaped = false;
|
|
19563
|
+
for (let i2 = openParen; i2 < text.length; i2++) {
|
|
19564
|
+
const c = text[i2];
|
|
19565
|
+
if (quote) {
|
|
19566
|
+
if (escaped) {
|
|
19567
|
+
escaped = false;
|
|
19568
|
+
continue;
|
|
19569
|
+
}
|
|
19570
|
+
if (c === "\\") {
|
|
19571
|
+
escaped = true;
|
|
19572
|
+
continue;
|
|
19573
|
+
}
|
|
19574
|
+
if (c === quote) quote = null;
|
|
19575
|
+
continue;
|
|
19576
|
+
}
|
|
19577
|
+
if (c === '"' || c === "'") {
|
|
19578
|
+
quote = c;
|
|
19579
|
+
continue;
|
|
19580
|
+
}
|
|
19581
|
+
if (c === "(") depth++;
|
|
19582
|
+
else if (c === ")") {
|
|
19583
|
+
depth--;
|
|
19584
|
+
if (depth === 0) return text.slice(openParen + 1, i2);
|
|
19585
|
+
}
|
|
19586
|
+
}
|
|
19587
|
+
return null;
|
|
19588
|
+
}
|
|
19589
|
+
function sanitizerCallRefs(sanitizers, sinkType) {
|
|
19590
|
+
const refs = [];
|
|
19591
|
+
for (const san of sanitizers) {
|
|
19592
|
+
if (!sanitizerCoversSink(san, sinkType)) continue;
|
|
19593
|
+
const m = /(?:(\w+)\.)?(\w+)\(\)/.exec(san.method);
|
|
19594
|
+
if (!m?.[2]) continue;
|
|
19595
|
+
refs.push({ receiver: m[1], methodName: m[2] });
|
|
19596
|
+
}
|
|
19597
|
+
return refs;
|
|
19598
|
+
}
|
|
19599
|
+
function maskSanitizerCalls(expr, refs) {
|
|
19600
|
+
let out2 = expr;
|
|
19601
|
+
for (const ref of refs) {
|
|
19602
|
+
const needles = ref.receiver ? [`${ref.receiver}.${ref.methodName}`, ref.methodName] : [ref.methodName];
|
|
19603
|
+
for (const needle of needles) {
|
|
19604
|
+
const re = new RegExp(`\\b${needle.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}\\s*\\(`);
|
|
19605
|
+
for (let guard = 0; guard < 8; guard++) {
|
|
19606
|
+
const m = re.exec(out2);
|
|
19607
|
+
if (!m) break;
|
|
19608
|
+
const open = m.index + m[0].length - 1;
|
|
19609
|
+
const arg = readBalancedArg(out2, open);
|
|
19610
|
+
if (arg === null) break;
|
|
19611
|
+
out2 = out2.slice(0, m.index) + " " + out2.slice(open + arg.length + 2);
|
|
19612
|
+
}
|
|
19613
|
+
}
|
|
19614
|
+
}
|
|
19615
|
+
return out2;
|
|
19616
|
+
}
|
|
19617
|
+
function blankStringLiterals(expr) {
|
|
19618
|
+
return expr.replace(/"(?:[^"\\]|\\.)*"/g, '""').replace(/'(?:[^'\\]|\\.)*'/g, "''");
|
|
19619
|
+
}
|
|
19620
|
+
function identifierOutsideSanitizerCalls(variable, calls, sanitizers, sinkType) {
|
|
19621
|
+
if (!/^[A-Za-z_$][\w$]*$/.test(variable)) return false;
|
|
19622
|
+
const refs = sanitizerCallRefs(sanitizers, sinkType);
|
|
19623
|
+
if (refs.length === 0) return false;
|
|
19624
|
+
const re = new RegExp(
|
|
19625
|
+
`(?:^|[^A-Za-z0-9_$])${variable.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}(?:[^A-Za-z0-9_$]|$)`
|
|
19626
|
+
);
|
|
19627
|
+
for (const call of calls) {
|
|
19628
|
+
if (refs.some((r) => r.methodName === call.method_name)) continue;
|
|
19629
|
+
for (const arg of call.arguments) {
|
|
19630
|
+
const expr = arg.expression ?? "";
|
|
19631
|
+
if (!expr) continue;
|
|
19632
|
+
const masked = maskSanitizerCalls(expr, refs);
|
|
19633
|
+
if (masked === expr) continue;
|
|
19634
|
+
if (re.test(blankStringLiterals(masked))) return true;
|
|
19635
|
+
}
|
|
19636
|
+
}
|
|
19637
|
+
return false;
|
|
19638
|
+
}
|
|
19525
19639
|
function checkSanitized(fromLine, toLine, sinkType, sanitizersByLine, ctx) {
|
|
19526
19640
|
const isKnownSinkType = KNOWN_SINK_TYPES.has(sinkType);
|
|
19527
19641
|
const sanitizersAtTarget = sanitizersByLine.get(toLine);
|
|
@@ -26427,6 +26541,25 @@ var GoPlugin = class extends BaseLanguagePlugin {
|
|
|
26427
26541
|
confidence: 0.9,
|
|
26428
26542
|
returnTainted: true
|
|
26429
26543
|
},
|
|
26544
|
+
// #492 — `r.PostForm.Get` / `r.Form.Get`. The extractor rewrites only
|
|
26545
|
+
// those selectors on a declared `*http.Request` to these synthetic
|
|
26546
|
+
// classes, so `url.Values.Get` stays unmatched.
|
|
26547
|
+
{
|
|
26548
|
+
method: "Get",
|
|
26549
|
+
class: "RequestPostForm",
|
|
26550
|
+
type: "http_body",
|
|
26551
|
+
severity: "high",
|
|
26552
|
+
confidence: 0.9,
|
|
26553
|
+
returnTainted: true
|
|
26554
|
+
},
|
|
26555
|
+
{
|
|
26556
|
+
method: "Get",
|
|
26557
|
+
class: "RequestForm",
|
|
26558
|
+
type: "http_param",
|
|
26559
|
+
severity: "high",
|
|
26560
|
+
confidence: 0.9,
|
|
26561
|
+
returnTainted: true
|
|
26562
|
+
},
|
|
26430
26563
|
// Gin framework
|
|
26431
26564
|
// NOTE: `Context.Query` (http_param) and `Context.Param` (http_path) are
|
|
26432
26565
|
// registered identically in DEFAULT_SOURCES; the duplicates here emitted
|
|
@@ -28101,6 +28234,7 @@ var LanguageSourcesPass = class {
|
|
|
28101
28234
|
additionalSources.push(...findGoMultipartUploadSources(code, language));
|
|
28102
28235
|
additionalSources.push(...findGoGrpcRequestSources(code, language));
|
|
28103
28236
|
additionalSources.push(...findGoRequestBodySources(code, language));
|
|
28237
|
+
additionalSources.push(...findGoFormIndexSources(code, language));
|
|
28104
28238
|
const jsDOMSinks = findJavaScriptDOMSinks(code, language);
|
|
28105
28239
|
for (const s of jsDOMSinks) {
|
|
28106
28240
|
const alreadyExists = additionalSinks.some((x) => x.line === s.line && x.cwe === s.cwe);
|
|
@@ -28938,6 +29072,42 @@ function findGoGrpcRequestSources(sourceCode, language) {
|
|
|
28938
29072
|
}
|
|
28939
29073
|
return sources;
|
|
28940
29074
|
}
|
|
29075
|
+
function findGoFormIndexSources(sourceCode, language) {
|
|
29076
|
+
if (language !== "go") return [];
|
|
29077
|
+
if (!/\.(?:PostForm|Form|MultipartForm)\b/.test(sourceCode)) return [];
|
|
29078
|
+
const lines = sourceCode.split("\n");
|
|
29079
|
+
const reqNames = /* @__PURE__ */ new Set();
|
|
29080
|
+
const paramRe = /([A-Za-z_]\w*)\s+\*http\.Request\b/g;
|
|
29081
|
+
for (const line of lines) {
|
|
29082
|
+
let m;
|
|
29083
|
+
paramRe.lastIndex = 0;
|
|
29084
|
+
while ((m = paramRe.exec(line)) !== null) reqNames.add(m[1]);
|
|
29085
|
+
}
|
|
29086
|
+
if (reqNames.size === 0) return [];
|
|
29087
|
+
const alt = [...reqNames].map((n) => n.replace(/\$/g, "\\$")).join("|");
|
|
29088
|
+
const indexRe = new RegExp(
|
|
29089
|
+
`^\\s*(?:var\\s+)?([A-Za-z_]\\w*)\\s*(?:,\\s*\\w+\\s*)?:?=\\s*(?:${alt})\\s*\\.\\s*(?:(PostForm|Form)|MultipartForm\\s*\\.\\s*Value)\\s*\\[`
|
|
29090
|
+
);
|
|
29091
|
+
const sources = [];
|
|
29092
|
+
for (let i2 = 0; i2 < lines.length; i2++) {
|
|
29093
|
+
const line = lines[i2];
|
|
29094
|
+
if (/^\s*\/\//.test(line)) continue;
|
|
29095
|
+
const m = indexRe.exec(line);
|
|
29096
|
+
if (!m) continue;
|
|
29097
|
+
const variable = m[1];
|
|
29098
|
+
if (variable === "_" || variable === "") continue;
|
|
29099
|
+
const kind = m[2];
|
|
29100
|
+
sources.push({
|
|
29101
|
+
type: kind === "Form" ? "http_param" : "http_body",
|
|
29102
|
+
location: "HTTP request form index",
|
|
29103
|
+
severity: "high",
|
|
29104
|
+
line: i2 + 1,
|
|
29105
|
+
confidence: 0.9,
|
|
29106
|
+
variable
|
|
29107
|
+
});
|
|
29108
|
+
}
|
|
29109
|
+
return sources;
|
|
29110
|
+
}
|
|
28941
29111
|
function findGoRequestBodySources(sourceCode, language) {
|
|
28942
29112
|
if (language !== "go") return [];
|
|
28943
29113
|
if (!sourceCode.includes(".Body")) return [];
|
|
@@ -36360,7 +36530,7 @@ var SinkFilterPass = class {
|
|
|
36360
36530
|
constProp.synchronizedLines,
|
|
36361
36531
|
language
|
|
36362
36532
|
);
|
|
36363
|
-
filtered = filterSanitizedSinks(filtered, sanitizers, calls);
|
|
36533
|
+
filtered = filterSanitizedSinks(filtered, sanitizers, calls, sources);
|
|
36364
36534
|
if (language === "python") {
|
|
36365
36535
|
const { pyTaintedVars, pySanitizedVars } = langSources;
|
|
36366
36536
|
const sourceLines = ctx.code.split("\n");
|
|
@@ -36421,7 +36591,16 @@ var SinkFilterPass = class {
|
|
|
36421
36591
|
filtered = filtered.filter((sink) => {
|
|
36422
36592
|
if (sink.type !== "xss") return true;
|
|
36423
36593
|
const sinkLineText = sourceLines[sink.line - 1] ?? "";
|
|
36424
|
-
if (JS_XSS_SANITIZERS.some((p) => p.test(sinkLineText)))
|
|
36594
|
+
if (JS_XSS_SANITIZERS.some((p) => p.test(sinkLineText))) {
|
|
36595
|
+
const callsAtSink = callsByLine.get(sink.line) ?? [];
|
|
36596
|
+
const covering = sanitizers.filter(
|
|
36597
|
+
(s) => s.line === sink.line && sanitizerCoversSink(s, sink.type)
|
|
36598
|
+
);
|
|
36599
|
+
const rawOperand = sources.some(
|
|
36600
|
+
(s) => !!s.variable && identifierOutsideSanitizerCalls(s.variable, callsAtSink, covering, sink.type)
|
|
36601
|
+
);
|
|
36602
|
+
if (!rawOperand) return false;
|
|
36603
|
+
}
|
|
36425
36604
|
const assignmentMatch = sinkLineText.match(/\.(?:innerHTML|outerHTML)\s*=\s*(.+)/);
|
|
36426
36605
|
if (assignmentMatch) {
|
|
36427
36606
|
const rhs = assignmentMatch[1].trim().replace(/;$/, "").trim();
|
|
@@ -37491,8 +37670,12 @@ function filterCleanVariableSinks(sinks, calls, taintedVars, symbols, dfg, sanit
|
|
|
37491
37670
|
return true;
|
|
37492
37671
|
});
|
|
37493
37672
|
}
|
|
37494
|
-
function filterSanitizedSinks(sinks, sanitizers, calls) {
|
|
37673
|
+
function filterSanitizedSinks(sinks, sanitizers, calls, sources) {
|
|
37495
37674
|
if (!sanitizers || sanitizers.length === 0) return sinks;
|
|
37675
|
+
const sourceVars = [];
|
|
37676
|
+
for (const source of sources ?? []) {
|
|
37677
|
+
if (source.variable && /^[A-Za-z_$][\w$]*$/.test(source.variable)) sourceVars.push(source.variable);
|
|
37678
|
+
}
|
|
37496
37679
|
const sanitizersByLine = /* @__PURE__ */ new Map();
|
|
37497
37680
|
for (const san of sanitizers) {
|
|
37498
37681
|
const existing = sanitizersByLine.get(san.line) ?? [];
|
|
@@ -37508,22 +37691,25 @@ function filterSanitizedSinks(sinks, sanitizers, calls) {
|
|
|
37508
37691
|
return sinks.filter((sink) => {
|
|
37509
37692
|
const lineSanitizers = sanitizersByLine.get(sink.line);
|
|
37510
37693
|
if (!lineSanitizers || lineSanitizers.length === 0) return true;
|
|
37511
|
-
|
|
37512
|
-
|
|
37513
|
-
|
|
37514
|
-
|
|
37515
|
-
|
|
37516
|
-
|
|
37517
|
-
|
|
37518
|
-
|
|
37519
|
-
|
|
37520
|
-
|
|
37521
|
-
|
|
37522
|
-
|
|
37523
|
-
|
|
37524
|
-
|
|
37525
|
-
|
|
37526
|
-
|
|
37694
|
+
const lineCalls = callsByLine.get(sink.line) ?? [];
|
|
37695
|
+
const covering = lineSanitizers.filter((san) => sanitizerCoversSink(san, sink.type));
|
|
37696
|
+
if (covering.length === 0) return true;
|
|
37697
|
+
for (const variable of sourceVars) {
|
|
37698
|
+
if (identifierOutsideSanitizerCalls(variable, lineCalls, covering, sink.type)) return true;
|
|
37699
|
+
}
|
|
37700
|
+
for (const san of covering) {
|
|
37701
|
+
const sanMethodMatch = san.method.match(/(?:(\w+)\.)?(\w+)\(\)/);
|
|
37702
|
+
if (!sanMethodMatch) continue;
|
|
37703
|
+
const sanMethodName = sanMethodMatch[2];
|
|
37704
|
+
const sanClassName = sanMethodMatch[1];
|
|
37705
|
+
for (const call of lineCalls) {
|
|
37706
|
+
if (call.method_name === sanMethodName) continue;
|
|
37707
|
+
for (const arg of call.arguments) {
|
|
37708
|
+
const expr = arg.expression || "";
|
|
37709
|
+
if (sanClassName) {
|
|
37710
|
+
if (expr.includes(`${sanClassName}.${sanMethodName}(`)) return false;
|
|
37711
|
+
} else if (expr.includes(`${sanMethodName}(`)) {
|
|
37712
|
+
return false;
|
|
37527
37713
|
}
|
|
37528
37714
|
}
|
|
37529
37715
|
}
|
|
@@ -38879,9 +39065,13 @@ var TaintPropagationPass = class {
|
|
|
38879
39065
|
const sansAtSink = sanitizersByLine.get(f.sink_line);
|
|
38880
39066
|
if (sansAtSink && sansAtSink.length > 0) {
|
|
38881
39067
|
for (const san of sansAtSink) {
|
|
38882
|
-
if (sanitizerCoversSink(san, f.sink_type))
|
|
38883
|
-
|
|
39068
|
+
if (!sanitizerCoversSink(san, f.sink_type)) continue;
|
|
39069
|
+
const sinkVar2 = f.path.length > 0 ? f.path[f.path.length - 1].variable : void 0;
|
|
39070
|
+
const callsAtSink = graph.callsByLine.get(f.sink_line) ?? [];
|
|
39071
|
+
if (sinkVar2 && identifierOutsideSanitizerCalls(sinkVar2, callsAtSink, sansAtSink, f.sink_type)) {
|
|
39072
|
+
continue;
|
|
38884
39073
|
}
|
|
39074
|
+
return false;
|
|
38885
39075
|
}
|
|
38886
39076
|
}
|
|
38887
39077
|
const usesAtSink = graph.usesByLine.get(f.sink_line);
|
|
@@ -40468,10 +40658,14 @@ var InterproceduralPass = class {
|
|
|
40468
40658
|
}
|
|
40469
40659
|
const sansAtSink = sanitizersByLine.get(f.sink_line);
|
|
40470
40660
|
if (!sansAtSink || sansAtSink.length === 0) return true;
|
|
40661
|
+
const callsAtSink = graph.callsByLine.get(f.sink_line) ?? [];
|
|
40662
|
+
const sinkVar = f.path.length > 0 ? f.path[f.path.length - 1]?.variable : void 0;
|
|
40471
40663
|
for (const san of sansAtSink) {
|
|
40472
|
-
if (sanitizerCoversSink(san, f.sink_type))
|
|
40473
|
-
|
|
40664
|
+
if (!sanitizerCoversSink(san, f.sink_type)) continue;
|
|
40665
|
+
if (sinkVar && identifierOutsideSanitizerCalls(sinkVar, callsAtSink, sansAtSink, f.sink_type)) {
|
|
40666
|
+
continue;
|
|
40474
40667
|
}
|
|
40668
|
+
return false;
|
|
40475
40669
|
}
|
|
40476
40670
|
return true;
|
|
40477
40671
|
});
|
|
@@ -44420,6 +44614,14 @@ var PROVIDER_PATTERNS = [
|
|
|
44420
44614
|
level: "error",
|
|
44421
44615
|
fix: "Revoke the GitHub refresh token and store secrets outside source control."
|
|
44422
44616
|
},
|
|
44617
|
+
// Fine-grained PATs: `github_pat_` + 22 chars + `_` + 59 chars. (#553)
|
|
44618
|
+
{
|
|
44619
|
+
name: "GitHub fine-grained personal access token",
|
|
44620
|
+
regex: /\bgithub_pat_[A-Za-z0-9]{22}_[A-Za-z0-9]{59}\b/,
|
|
44621
|
+
severity: "critical",
|
|
44622
|
+
level: "error",
|
|
44623
|
+
fix: "Revoke the fine-grained personal access token at https://github.com/settings/tokens and store secrets outside source control."
|
|
44624
|
+
},
|
|
44423
44625
|
// GitLab personal / project / group access tokens use the `glpat-` prefix.
|
|
44424
44626
|
// Length is not fixed (legacy bodies are about 20 characters; routable
|
|
44425
44627
|
// tokens are longer), so the floor is 20 and there is no max. (#577)
|
|
@@ -44448,12 +44650,14 @@ var PROVIDER_PATTERNS = [
|
|
|
44448
44650
|
level: "error",
|
|
44449
44651
|
fix: "Rotate the Stripe restricted key in the Stripe Dashboard and load it from a secrets manager."
|
|
44450
44652
|
},
|
|
44653
|
+
// Publishable keys are meant to be embedded in a client. Keep the finding
|
|
44654
|
+
// so the key is still visible, but at informational severity. (#552)
|
|
44451
44655
|
{
|
|
44452
44656
|
name: "Stripe live publishable key",
|
|
44453
44657
|
regex: /\bpk_live_[A-Za-z0-9]{24,}\b/,
|
|
44454
|
-
severity: "
|
|
44455
|
-
level: "
|
|
44456
|
-
fix: "Publishable keys are not secret
|
|
44658
|
+
severity: "low",
|
|
44659
|
+
level: "note",
|
|
44660
|
+
fix: "Publishable keys are not secret. Prefer loading them from configuration rather than a backend source file."
|
|
44457
44661
|
},
|
|
44458
44662
|
// Test-mode secret and restricted keys still authorize the test account.
|
|
44459
44663
|
// Same length floor as the live forms. Severity is high, not critical (#576).
|
|
@@ -44478,6 +44682,17 @@ var PROVIDER_PATTERNS = [
|
|
|
44478
44682
|
level: "error",
|
|
44479
44683
|
fix: "Revoke the OpenAI key at https://platform.openai.com/api-keys and load from environment."
|
|
44480
44684
|
},
|
|
44685
|
+
// Project keys (`sk-proj-`) are a different prefix from the legacy 48-char
|
|
44686
|
+
// secret. OpenAI does not publish one length, so the body floor is 48 and
|
|
44687
|
+
// there is no max and no entropy gate. The hyphen after `proj` keeps this
|
|
44688
|
+
// from matching the legacy pattern. (#551)
|
|
44689
|
+
{
|
|
44690
|
+
name: "OpenAI project API key",
|
|
44691
|
+
regex: /\bsk-proj-[A-Za-z0-9_-]{48,}\b/,
|
|
44692
|
+
severity: "critical",
|
|
44693
|
+
level: "error",
|
|
44694
|
+
fix: "Revoke the OpenAI project key at https://platform.openai.com/api-keys and load from environment."
|
|
44695
|
+
},
|
|
44481
44696
|
{
|
|
44482
44697
|
name: "Anthropic API key",
|
|
44483
44698
|
regex: /\bsk-ant-[A-Za-z0-9_-]{90,}\b/,
|
|
@@ -44770,7 +44985,7 @@ var ScanSecretsPass = class {
|
|
|
44770
44985
|
for (const pattern of PROVIDER_PATTERNS) {
|
|
44771
44986
|
const m = pattern.regex.exec(lineText);
|
|
44772
44987
|
if (!m) continue;
|
|
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])) {
|
|
44988
|
+
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" || pattern.name === "OpenAI project API key" || pattern.name === "GitHub fine-grained personal access token") && PLACEHOLDER_RE.test(m[0])) {
|
|
44774
44989
|
continue;
|
|
44775
44990
|
}
|
|
44776
44991
|
if (pattern.name === "PEM private key" && !pemHasInlineBody(lines, i2)) {
|
|
@@ -45202,8 +45417,6 @@ var PY_SET_COOKIE_RECEIVERS = /* @__PURE__ */ new Set([
|
|
|
45202
45417
|
]);
|
|
45203
45418
|
var PY_SECURE_TRUE_RE = /\bsecure\s*=\s*True\b/;
|
|
45204
45419
|
var PY_HTTPONLY_TRUE_RE = /\bhttponly\s*=\s*True\b/i;
|
|
45205
|
-
var JAVA_SET_SECURE_TRUE_RE = /\.setSecure\s*\(\s*true\s*\)/;
|
|
45206
|
-
var JAVA_SET_HTTPONLY_TRUE_RE = /\.setHttpOnly\s*\(\s*true\s*\)/;
|
|
45207
45420
|
var GO_SECURE_TRUE_RE = /\bSecure\s*:\s*true\b/;
|
|
45208
45421
|
var GO_HTTPONLY_TRUE_RE = /\bHttpOnly\s*:\s*true\b/;
|
|
45209
45422
|
var RUST_SET_COOKIE_MACRO_RE = /(format!|write!|writeln!)\s*\(([^()]*Set-Cookie[^()]*)\)/gis;
|
|
@@ -45232,10 +45445,8 @@ var InsecureCookiePass = class {
|
|
|
45232
45445
|
this.emit(ctx, file, det, "python");
|
|
45233
45446
|
}
|
|
45234
45447
|
} else if (language === "java") {
|
|
45235
|
-
const hasSetSecureTrue = JAVA_SET_SECURE_TRUE_RE.test(code);
|
|
45236
|
-
const hasSetHttpOnlyTrue = JAVA_SET_HTTPONLY_TRUE_RE.test(code);
|
|
45237
45448
|
for (const call of graph.ir.calls) {
|
|
45238
|
-
const det = this.detectJavaCookieCtor(call,
|
|
45449
|
+
const det = this.detectJavaCookieCtor(call, code);
|
|
45239
45450
|
if (!det) continue;
|
|
45240
45451
|
insecureCookies.push(det);
|
|
45241
45452
|
this.emit(ctx, file, det, "java");
|
|
@@ -45314,15 +45525,28 @@ var InsecureCookiePass = class {
|
|
|
45314
45525
|
};
|
|
45315
45526
|
}
|
|
45316
45527
|
// ---------------- Java ----------------
|
|
45317
|
-
|
|
45528
|
+
/**
|
|
45529
|
+
* True when this cookie construction is followed by `var.setX(true)`.
|
|
45530
|
+
* The variable is the name assigned on the constructor line. An
|
|
45531
|
+
* unassigned `new Cookie(...)` only looks at that statement, so another
|
|
45532
|
+
* cookie's setter cannot cover it.
|
|
45533
|
+
*/
|
|
45534
|
+
javaCookieSetterTrue(code, call, setter) {
|
|
45535
|
+
const line = code.split("\n")[(call.location.line ?? 1) - 1] ?? "";
|
|
45536
|
+
const assign = /\b([A-Za-z_]\w*)\s*=\s*new\s+(?:[\w.]+\.)?Cookie\b/.exec(line);
|
|
45537
|
+
const varName = assign?.[1];
|
|
45538
|
+
const re = varName ? new RegExp(`\\b${varName}\\.${setter}\\s*\\(\\s*true\\s*\\)`) : new RegExp(`\\.${setter}\\s*\\(\\s*true\\s*\\)`);
|
|
45539
|
+
return re.test(varName ? code : line);
|
|
45540
|
+
}
|
|
45541
|
+
detectJavaCookieCtor(call, code) {
|
|
45318
45542
|
const method = call.method_name ?? "";
|
|
45319
45543
|
const isCookieCtor = method === "Cookie" || method.endsWith(".Cookie");
|
|
45320
45544
|
if (!isCookieCtor) return null;
|
|
45321
45545
|
const looksLikeCtor = call.is_constructor || !call.receiver && (call.receiver_type === "Cookie" || (call.receiver_type ?? "").endsWith(".Cookie")) || (call.resolution?.target ?? "").endsWith(".<init>");
|
|
45322
45546
|
if (!looksLikeCtor) return null;
|
|
45323
45547
|
if (call.arguments.length < 2) return null;
|
|
45324
|
-
const missingSecure = !
|
|
45325
|
-
const missingHttpOnly = !
|
|
45548
|
+
const missingSecure = !this.javaCookieSetterTrue(code, call, "setSecure");
|
|
45549
|
+
const missingHttpOnly = !this.javaCookieSetterTrue(code, call, "setHttpOnly");
|
|
45326
45550
|
if (!missingSecure && !missingHttpOnly) return null;
|
|
45327
45551
|
return {
|
|
45328
45552
|
line: call.location.line,
|
|
@@ -49936,7 +50160,7 @@ async function analyzeForAPI(code, filePath, language, options = {}) {
|
|
|
49936
50160
|
constPropResult.sanitizedVars,
|
|
49937
50161
|
constPropResult.synchronizedLines
|
|
49938
50162
|
);
|
|
49939
|
-
filteredSinks = filterSanitizedSinks(filteredSinks, taint.sanitizers ?? [], calls);
|
|
50163
|
+
filteredSinks = filterSanitizedSinks(filteredSinks, taint.sanitizers ?? [], calls, taint.sources);
|
|
49940
50164
|
let pythonTaintedVars = /* @__PURE__ */ new Map();
|
|
49941
50165
|
if (language === "python") {
|
|
49942
50166
|
pythonTaintedVars = buildPythonTaintedVars(code);
|
|
@@ -8080,6 +8080,17 @@ function extractGoCallInfo(node) {
|
|
|
8080
8080
|
if (operand.type === "identifier") {
|
|
8081
8081
|
const resolved = resolveGoLocalReceiverType(opText, node);
|
|
8082
8082
|
receiver = resolved !== null ? resolved : opText;
|
|
8083
|
+
} else if (operand.type === "selector_expression" && methodName === "Get") {
|
|
8084
|
+
const fieldNode = operand.childForFieldName("field");
|
|
8085
|
+
const inner = operand.childForFieldName("operand");
|
|
8086
|
+
const fieldName = fieldNode ? getNodeText(fieldNode) : "";
|
|
8087
|
+
const innerName = inner && inner.type === "identifier" ? getNodeText(inner) : null;
|
|
8088
|
+
const innerType = innerName !== null ? resolveGoLocalReceiverType(innerName, node) : null;
|
|
8089
|
+
if (innerType === "Request" && (fieldName === "PostForm" || fieldName === "Form")) {
|
|
8090
|
+
receiver = fieldName === "PostForm" ? "RequestPostForm" : "RequestForm";
|
|
8091
|
+
} else {
|
|
8092
|
+
receiver = opText;
|
|
8093
|
+
}
|
|
8083
8094
|
} else {
|
|
8084
8095
|
receiver = opText;
|
|
8085
8096
|
}
|
|
@@ -12448,8 +12459,15 @@ var DEFAULT_SINKS = [
|
|
|
12448
12459
|
{ method: "setExpireTimeSeconds", class: "StaticFilesConfiguration", type: "path_traversal", cwe: "CWE-22", severity: "medium", arg_positions: [0] },
|
|
12449
12460
|
{ method: "configureJarCase", class: "StaticFilesConfiguration", type: "path_traversal", cwe: "CWE-22", severity: "high", arg_positions: [0] },
|
|
12450
12461
|
{ method: "toString", class: "StringUtils", type: "path_traversal", cwe: "CWE-22", severity: "medium", arg_positions: [0] },
|
|
12451
|
-
// Wildfly/Undertow servlet resource manager
|
|
12452
|
-
|
|
12462
|
+
// Wildfly/Undertow servlet resource manager.
|
|
12463
|
+
// ResourceManager / PathResourceManager / FileResourceManager /
|
|
12464
|
+
// ClassPathResourceManager are the interface and implementations WildFly
|
|
12465
|
+
// calls through (cognium-dev#622, CVE-2018-1047). Class-qualified only.
|
|
12466
|
+
{ method: "getResource", class: "ServletResourceManager", type: "path_traversal", cwe: "CWE-22", severity: "high", arg_positions: [0], languages: ["java"] },
|
|
12467
|
+
{ method: "getResource", class: "ResourceManager", type: "path_traversal", cwe: "CWE-22", severity: "high", arg_positions: [0], languages: ["java"] },
|
|
12468
|
+
{ method: "getResource", class: "PathResourceManager", type: "path_traversal", cwe: "CWE-22", severity: "high", arg_positions: [0], languages: ["java"] },
|
|
12469
|
+
{ method: "getResource", class: "FileResourceManager", type: "path_traversal", cwe: "CWE-22", severity: "high", arg_positions: [0], languages: ["java"] },
|
|
12470
|
+
{ method: "getResource", class: "ClassPathResourceManager", type: "path_traversal", cwe: "CWE-22", severity: "high", arg_positions: [0], languages: ["java"] },
|
|
12453
12471
|
// Yamcs file system bucket
|
|
12454
12472
|
{ method: "deleteObject", class: "FileSystemBucket", type: "path_traversal", cwe: "CWE-22", severity: "high", arg_positions: [0] },
|
|
12455
12473
|
// RocketMQ validators
|
|
@@ -15320,7 +15338,8 @@ function isInterproceduralTaintableType(typeName, language) {
|
|
|
15320
15338
|
}
|
|
15321
15339
|
function isParameterizedQueryCall(call, pattern) {
|
|
15322
15340
|
if (pattern.type !== "sql_injection") return false;
|
|
15323
|
-
const
|
|
15341
|
+
const positions = pattern.arg_positions ?? [];
|
|
15342
|
+
const queryPos = positions.length > 0 ? Math.min(...positions) : 0;
|
|
15324
15343
|
const queryArg = call.arguments.find((a) => a.position === queryPos);
|
|
15325
15344
|
if (queryArg) {
|
|
15326
15345
|
const queryText = queryArg.literal ?? queryArg.expression ?? "";
|
|
@@ -17329,11 +17348,26 @@ function propagateTaint(graphOrDfg, callsOrSources, sourcesOrSinks, sinksOrSanit
|
|
|
17329
17348
|
}
|
|
17330
17349
|
const taintInfo = taintByDefId.get(use.def_id);
|
|
17331
17350
|
if (taintInfo) {
|
|
17351
|
+
let sanMap = sanitizersByLine;
|
|
17352
|
+
if (identifierOutsideSanitizerCalls(
|
|
17353
|
+
use.variable,
|
|
17354
|
+
callsAtSink,
|
|
17355
|
+
sanitizersByLine.get(sink.line) ?? [],
|
|
17356
|
+
sink.type
|
|
17357
|
+
)) {
|
|
17358
|
+
const stripped = new Map(sanitizersByLine);
|
|
17359
|
+
const kept = (sanitizersByLine.get(sink.line) ?? []).filter(
|
|
17360
|
+
(s) => !sanitizerCoversSink(s, sink.type)
|
|
17361
|
+
);
|
|
17362
|
+
if (kept.length === 0) stripped.delete(sink.line);
|
|
17363
|
+
else stripped.set(sink.line, kept);
|
|
17364
|
+
sanMap = stripped;
|
|
17365
|
+
}
|
|
17332
17366
|
const isSanitized = checkSanitized(
|
|
17333
17367
|
taintInfo.line,
|
|
17334
17368
|
sink.line,
|
|
17335
17369
|
sink.type,
|
|
17336
|
-
|
|
17370
|
+
sanMap,
|
|
17337
17371
|
{
|
|
17338
17372
|
startDefId: use.def_id,
|
|
17339
17373
|
chainsByToDef: graph.chainsByToDef,
|
|
@@ -17487,6 +17521,86 @@ var KNOWN_SINK_TYPES = /* @__PURE__ */ new Set([
|
|
|
17487
17521
|
"crlf",
|
|
17488
17522
|
"mass_assignment"
|
|
17489
17523
|
]);
|
|
17524
|
+
function readBalancedArg(text, openParen) {
|
|
17525
|
+
let depth = 0;
|
|
17526
|
+
let quote = null;
|
|
17527
|
+
let escaped = false;
|
|
17528
|
+
for (let i2 = openParen; i2 < text.length; i2++) {
|
|
17529
|
+
const c = text[i2];
|
|
17530
|
+
if (quote) {
|
|
17531
|
+
if (escaped) {
|
|
17532
|
+
escaped = false;
|
|
17533
|
+
continue;
|
|
17534
|
+
}
|
|
17535
|
+
if (c === "\\") {
|
|
17536
|
+
escaped = true;
|
|
17537
|
+
continue;
|
|
17538
|
+
}
|
|
17539
|
+
if (c === quote) quote = null;
|
|
17540
|
+
continue;
|
|
17541
|
+
}
|
|
17542
|
+
if (c === '"' || c === "'") {
|
|
17543
|
+
quote = c;
|
|
17544
|
+
continue;
|
|
17545
|
+
}
|
|
17546
|
+
if (c === "(") depth++;
|
|
17547
|
+
else if (c === ")") {
|
|
17548
|
+
depth--;
|
|
17549
|
+
if (depth === 0) return text.slice(openParen + 1, i2);
|
|
17550
|
+
}
|
|
17551
|
+
}
|
|
17552
|
+
return null;
|
|
17553
|
+
}
|
|
17554
|
+
function sanitizerCallRefs(sanitizers, sinkType) {
|
|
17555
|
+
const refs = [];
|
|
17556
|
+
for (const san of sanitizers) {
|
|
17557
|
+
if (!sanitizerCoversSink(san, sinkType)) continue;
|
|
17558
|
+
const m = /(?:(\w+)\.)?(\w+)\(\)/.exec(san.method);
|
|
17559
|
+
if (!m?.[2]) continue;
|
|
17560
|
+
refs.push({ receiver: m[1], methodName: m[2] });
|
|
17561
|
+
}
|
|
17562
|
+
return refs;
|
|
17563
|
+
}
|
|
17564
|
+
function maskSanitizerCalls(expr, refs) {
|
|
17565
|
+
let out2 = expr;
|
|
17566
|
+
for (const ref of refs) {
|
|
17567
|
+
const needles = ref.receiver ? [`${ref.receiver}.${ref.methodName}`, ref.methodName] : [ref.methodName];
|
|
17568
|
+
for (const needle of needles) {
|
|
17569
|
+
const re = new RegExp(`\\b${needle.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}\\s*\\(`);
|
|
17570
|
+
for (let guard = 0; guard < 8; guard++) {
|
|
17571
|
+
const m = re.exec(out2);
|
|
17572
|
+
if (!m) break;
|
|
17573
|
+
const open = m.index + m[0].length - 1;
|
|
17574
|
+
const arg = readBalancedArg(out2, open);
|
|
17575
|
+
if (arg === null) break;
|
|
17576
|
+
out2 = out2.slice(0, m.index) + " " + out2.slice(open + arg.length + 2);
|
|
17577
|
+
}
|
|
17578
|
+
}
|
|
17579
|
+
}
|
|
17580
|
+
return out2;
|
|
17581
|
+
}
|
|
17582
|
+
function blankStringLiterals(expr) {
|
|
17583
|
+
return expr.replace(/"(?:[^"\\]|\\.)*"/g, '""').replace(/'(?:[^'\\]|\\.)*'/g, "''");
|
|
17584
|
+
}
|
|
17585
|
+
function identifierOutsideSanitizerCalls(variable, calls, sanitizers, sinkType) {
|
|
17586
|
+
if (!/^[A-Za-z_$][\w$]*$/.test(variable)) return false;
|
|
17587
|
+
const refs = sanitizerCallRefs(sanitizers, sinkType);
|
|
17588
|
+
if (refs.length === 0) return false;
|
|
17589
|
+
const re = new RegExp(
|
|
17590
|
+
`(?:^|[^A-Za-z0-9_$])${variable.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}(?:[^A-Za-z0-9_$]|$)`
|
|
17591
|
+
);
|
|
17592
|
+
for (const call of calls) {
|
|
17593
|
+
if (refs.some((r) => r.methodName === call.method_name)) continue;
|
|
17594
|
+
for (const arg of call.arguments) {
|
|
17595
|
+
const expr = arg.expression ?? "";
|
|
17596
|
+
if (!expr) continue;
|
|
17597
|
+
const masked = maskSanitizerCalls(expr, refs);
|
|
17598
|
+
if (masked === expr) continue;
|
|
17599
|
+
if (re.test(blankStringLiterals(masked))) return true;
|
|
17600
|
+
}
|
|
17601
|
+
}
|
|
17602
|
+
return false;
|
|
17603
|
+
}
|
|
17490
17604
|
function checkSanitized(fromLine, toLine, sinkType, sanitizersByLine, ctx) {
|
|
17491
17605
|
const isKnownSinkType = KNOWN_SINK_TYPES.has(sinkType);
|
|
17492
17606
|
const sanitizersAtTarget = sanitizersByLine.get(toLine);
|