circle-ir 4.14.0 → 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 +95 -38
- 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 +14 -5
- package/dist/analysis/taint-matcher.js.map +1 -1
- package/dist/analysis/taint-propagation.d.ts +7 -4
- package/dist/analysis/taint-propagation.d.ts.map +1 -1
- package/dist/analysis/taint-propagation.js +132 -3
- 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 +339 -64
- package/dist/core/circle-ir-core.cjs +159 -15
- package/dist/core/circle-ir-core.js +159 -15
- package/dist/core/extractors/calls.js +19 -0
- 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 +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
|
}
|
|
@@ -10196,6 +10207,36 @@ function extractFieldDefs(body2, startId) {
|
|
|
10196
10207
|
function extractJSParameterDefs(params, startId) {
|
|
10197
10208
|
const defs = [];
|
|
10198
10209
|
let id = startId;
|
|
10210
|
+
const addPatternDefs = (patternNode, line) => {
|
|
10211
|
+
if (patternNode.type === "identifier" || patternNode.type === "shorthand_property_identifier_pattern") {
|
|
10212
|
+
defs.push({
|
|
10213
|
+
id: id++,
|
|
10214
|
+
variable: getNodeText(patternNode),
|
|
10215
|
+
line,
|
|
10216
|
+
kind: "param"
|
|
10217
|
+
});
|
|
10218
|
+
return;
|
|
10219
|
+
}
|
|
10220
|
+
if (patternNode.type === "rest_pattern" || patternNode.type === "rest_element") {
|
|
10221
|
+
const nameNode = patternNode.namedChildCount > 0 ? patternNode.namedChild(0) : null;
|
|
10222
|
+
if (!nameNode) return;
|
|
10223
|
+
addPatternDefs(nameNode, line);
|
|
10224
|
+
return;
|
|
10225
|
+
}
|
|
10226
|
+
if (patternNode.type === "assignment_pattern") {
|
|
10227
|
+
const leftNode = patternNode.childForFieldName("left");
|
|
10228
|
+
if (leftNode) addPatternDefs(leftNode, line);
|
|
10229
|
+
return;
|
|
10230
|
+
}
|
|
10231
|
+
if (patternNode.type === "object_pattern" || patternNode.type === "array_pattern") {
|
|
10232
|
+
const destructDefs = extractDestructuringDefs(patternNode, id);
|
|
10233
|
+
for (const def of destructDefs) {
|
|
10234
|
+
def.kind = "param";
|
|
10235
|
+
defs.push(def);
|
|
10236
|
+
id++;
|
|
10237
|
+
}
|
|
10238
|
+
}
|
|
10239
|
+
};
|
|
10199
10240
|
for (let i2 = 0; i2 < params.childCount; i2++) {
|
|
10200
10241
|
const param = params.child(i2);
|
|
10201
10242
|
if (!param) continue;
|
|
@@ -10243,14 +10284,7 @@ function extractJSParameterDefs(params, startId) {
|
|
|
10243
10284
|
}
|
|
10244
10285
|
} else if (param.type === "required_parameter" || param.type === "optional_parameter") {
|
|
10245
10286
|
const patternNode = param.childForFieldName("pattern");
|
|
10246
|
-
if (patternNode
|
|
10247
|
-
defs.push({
|
|
10248
|
-
id: id++,
|
|
10249
|
-
variable: getNodeText(patternNode),
|
|
10250
|
-
line: param.startPosition.row + 1,
|
|
10251
|
-
kind: "param"
|
|
10252
|
-
});
|
|
10253
|
-
}
|
|
10287
|
+
if (patternNode) addPatternDefs(patternNode, param.startPosition.row + 1);
|
|
10254
10288
|
}
|
|
10255
10289
|
}
|
|
10256
10290
|
return defs;
|
|
@@ -13006,8 +13040,15 @@ var DEFAULT_SINKS = [
|
|
|
13006
13040
|
{ method: "setExpireTimeSeconds", class: "StaticFilesConfiguration", type: "path_traversal", cwe: "CWE-22", severity: "medium", arg_positions: [0] },
|
|
13007
13041
|
{ method: "configureJarCase", class: "StaticFilesConfiguration", type: "path_traversal", cwe: "CWE-22", severity: "high", arg_positions: [0] },
|
|
13008
13042
|
{ method: "toString", class: "StringUtils", type: "path_traversal", cwe: "CWE-22", severity: "medium", arg_positions: [0] },
|
|
13009
|
-
// Wildfly/Undertow servlet resource manager
|
|
13010
|
-
|
|
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"] },
|
|
13011
13052
|
// Yamcs file system bucket
|
|
13012
13053
|
{ method: "deleteObject", class: "FileSystemBucket", type: "path_traversal", cwe: "CWE-22", severity: "high", arg_positions: [0] },
|
|
13013
13054
|
// RocketMQ validators
|
|
@@ -15766,7 +15807,8 @@ function findSources(calls, types, patterns, sourceLines, language) {
|
|
|
15766
15807
|
if (skipMethods.includes(method.name)) continue;
|
|
15767
15808
|
for (const param of method.parameters) {
|
|
15768
15809
|
const hasCSharpBindingAttr = language === "csharp" && param.annotations.some((a) => CSHARP_BINDING_ATTRS.has(a));
|
|
15769
|
-
const
|
|
15810
|
+
const isDestructureBinding = param.name.startsWith("{") || param.name.startsWith("[");
|
|
15811
|
+
const isTaintable = hasCSharpBindingAttr ? true : isDestructureBinding ? true : param.type ? isInterproceduralTaintableType(param.type, language) : true;
|
|
15770
15812
|
if (isTaintable) {
|
|
15771
15813
|
const paramLine = param.line ?? method.start_line;
|
|
15772
15814
|
sources.push({
|
|
@@ -15996,7 +16038,8 @@ function isInterproceduralTaintableType(typeName, language) {
|
|
|
15996
16038
|
}
|
|
15997
16039
|
function isParameterizedQueryCall(call, pattern) {
|
|
15998
16040
|
if (pattern.type !== "sql_injection") return false;
|
|
15999
|
-
const
|
|
16041
|
+
const positions = pattern.arg_positions ?? [];
|
|
16042
|
+
const queryPos = positions.length > 0 ? Math.min(...positions) : 0;
|
|
16000
16043
|
const queryArg = call.arguments.find((a) => a.position === queryPos);
|
|
16001
16044
|
if (queryArg) {
|
|
16002
16045
|
const queryText = queryArg.literal ?? queryArg.expression ?? "";
|
|
@@ -19255,6 +19298,9 @@ function buildSanitizersByLine(sanitizers) {
|
|
|
19255
19298
|
}
|
|
19256
19299
|
return out2;
|
|
19257
19300
|
}
|
|
19301
|
+
function concatOutsideStrings(expression) {
|
|
19302
|
+
return expression.replace(/"(?:\\.|[^"\\])*"|'(?:\\.|[^'\\])*'/g, " ").includes("+");
|
|
19303
|
+
}
|
|
19258
19304
|
function propagateTaint(graphOrDfg, callsOrSources, sourcesOrSinks, sinksOrSanitizers, sanitizersArg) {
|
|
19259
19305
|
let graph;
|
|
19260
19306
|
let sources;
|
|
@@ -19286,6 +19332,7 @@ function propagateTaint(graphOrDfg, callsOrSources, sourcesOrSinks, sinksOrSanit
|
|
|
19286
19332
|
}
|
|
19287
19333
|
const taintedVars = [];
|
|
19288
19334
|
const flows = [];
|
|
19335
|
+
const javaConcat = graph.ir.meta.language === "java";
|
|
19289
19336
|
const reachableSinks = /* @__PURE__ */ new Map();
|
|
19290
19337
|
const defsByLine = graph.defsByLine;
|
|
19291
19338
|
const usesByLine = graph.usesByLine;
|
|
@@ -19322,23 +19369,40 @@ function propagateTaint(graphOrDfg, callsOrSources, sourcesOrSinks, sinksOrSanit
|
|
|
19322
19369
|
continue;
|
|
19323
19370
|
}
|
|
19324
19371
|
}
|
|
19325
|
-
const
|
|
19372
|
+
const expr = typeof arg.expression === "string" ? arg.expression : "";
|
|
19373
|
+
const compoundConcat = javaConcat && !!arg.variable && expr !== arg.variable && concatOutsideStrings(expr);
|
|
19374
|
+
const candidateUses = arg.variable && !compoundConcat ? usesAtSink.filter((u) => u.variable === arg.variable) : usesAtSink;
|
|
19326
19375
|
{
|
|
19327
19376
|
for (const use of candidateUses) {
|
|
19328
19377
|
if (use.def_id !== null) {
|
|
19329
19378
|
if (allTaintedDefIds.has(use.def_id)) {
|
|
19330
|
-
if (!arg.variable) {
|
|
19379
|
+
if (!arg.variable || compoundConcat) {
|
|
19331
19380
|
if (typeof arg.expression !== "string" || arg.expression.length === 0) continue;
|
|
19332
19381
|
const re = new RegExp(`(?:^|[^A-Za-z0-9_$])${use.variable.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}(?:[^A-Za-z0-9_$]|$)`);
|
|
19333
19382
|
if (!re.test(arg.expression)) continue;
|
|
19334
19383
|
}
|
|
19335
19384
|
const taintInfo = taintByDefId.get(use.def_id);
|
|
19336
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
|
+
}
|
|
19337
19401
|
const isSanitized = checkSanitized(
|
|
19338
19402
|
taintInfo.line,
|
|
19339
19403
|
sink.line,
|
|
19340
19404
|
sink.type,
|
|
19341
|
-
|
|
19405
|
+
sanMap,
|
|
19342
19406
|
{
|
|
19343
19407
|
startDefId: use.def_id,
|
|
19344
19408
|
chainsByToDef: graph.chainsByToDef,
|
|
@@ -19492,6 +19556,86 @@ var KNOWN_SINK_TYPES = /* @__PURE__ */ new Set([
|
|
|
19492
19556
|
"crlf",
|
|
19493
19557
|
"mass_assignment"
|
|
19494
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
|
+
}
|
|
19495
19639
|
function checkSanitized(fromLine, toLine, sinkType, sanitizersByLine, ctx) {
|
|
19496
19640
|
const isKnownSinkType = KNOWN_SINK_TYPES.has(sinkType);
|
|
19497
19641
|
const sanitizersAtTarget = sanitizersByLine.get(toLine);
|
|
@@ -26397,6 +26541,25 @@ var GoPlugin = class extends BaseLanguagePlugin {
|
|
|
26397
26541
|
confidence: 0.9,
|
|
26398
26542
|
returnTainted: true
|
|
26399
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
|
+
},
|
|
26400
26563
|
// Gin framework
|
|
26401
26564
|
// NOTE: `Context.Query` (http_param) and `Context.Param` (http_path) are
|
|
26402
26565
|
// registered identically in DEFAULT_SOURCES; the duplicates here emitted
|
|
@@ -28071,6 +28234,7 @@ var LanguageSourcesPass = class {
|
|
|
28071
28234
|
additionalSources.push(...findGoMultipartUploadSources(code, language));
|
|
28072
28235
|
additionalSources.push(...findGoGrpcRequestSources(code, language));
|
|
28073
28236
|
additionalSources.push(...findGoRequestBodySources(code, language));
|
|
28237
|
+
additionalSources.push(...findGoFormIndexSources(code, language));
|
|
28074
28238
|
const jsDOMSinks = findJavaScriptDOMSinks(code, language);
|
|
28075
28239
|
for (const s of jsDOMSinks) {
|
|
28076
28240
|
const alreadyExists = additionalSinks.some((x) => x.line === s.line && x.cwe === s.cwe);
|
|
@@ -28908,6 +29072,42 @@ function findGoGrpcRequestSources(sourceCode, language) {
|
|
|
28908
29072
|
}
|
|
28909
29073
|
return sources;
|
|
28910
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
|
+
}
|
|
28911
29111
|
function findGoRequestBodySources(sourceCode, language) {
|
|
28912
29112
|
if (language !== "go") return [];
|
|
28913
29113
|
if (!sourceCode.includes(".Body")) return [];
|
|
@@ -36330,7 +36530,7 @@ var SinkFilterPass = class {
|
|
|
36330
36530
|
constProp.synchronizedLines,
|
|
36331
36531
|
language
|
|
36332
36532
|
);
|
|
36333
|
-
filtered = filterSanitizedSinks(filtered, sanitizers, calls);
|
|
36533
|
+
filtered = filterSanitizedSinks(filtered, sanitizers, calls, sources);
|
|
36334
36534
|
if (language === "python") {
|
|
36335
36535
|
const { pyTaintedVars, pySanitizedVars } = langSources;
|
|
36336
36536
|
const sourceLines = ctx.code.split("\n");
|
|
@@ -36391,7 +36591,16 @@ var SinkFilterPass = class {
|
|
|
36391
36591
|
filtered = filtered.filter((sink) => {
|
|
36392
36592
|
if (sink.type !== "xss") return true;
|
|
36393
36593
|
const sinkLineText = sourceLines[sink.line - 1] ?? "";
|
|
36394
|
-
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
|
+
}
|
|
36395
36604
|
const assignmentMatch = sinkLineText.match(/\.(?:innerHTML|outerHTML)\s*=\s*(.+)/);
|
|
36396
36605
|
if (assignmentMatch) {
|
|
36397
36606
|
const rhs = assignmentMatch[1].trim().replace(/;$/, "").trim();
|
|
@@ -37344,6 +37553,20 @@ function evaluateSimpleExpression(expr, symbols) {
|
|
|
37344
37553
|
if (result !== null && !isNaN(result)) return String(Math.floor(result));
|
|
37345
37554
|
return expr;
|
|
37346
37555
|
}
|
|
37556
|
+
var ARGUMENT_IDENT_SKIP = /* @__PURE__ */ new Set(["new", "true", "false", "null", "this", "super"]);
|
|
37557
|
+
function isStringConcat(expression) {
|
|
37558
|
+
return expression.replace(/"(?:\\.|[^"\\])*"|'(?:\\.|[^'\\])*'/g, " ").includes("+");
|
|
37559
|
+
}
|
|
37560
|
+
function argumentIdentifiers(expression) {
|
|
37561
|
+
const stripped = expression.replace(/"(?:\\.|[^"\\])*"|'(?:\\.|[^'\\])*'/g, " ");
|
|
37562
|
+
const found = stripped.match(/[A-Za-z_$][A-Za-z0-9_$]*/g) ?? [];
|
|
37563
|
+
const out2 = [];
|
|
37564
|
+
for (const id of found) {
|
|
37565
|
+
if (ARGUMENT_IDENT_SKIP.has(id)) continue;
|
|
37566
|
+
if (!out2.includes(id)) out2.push(id);
|
|
37567
|
+
}
|
|
37568
|
+
return out2;
|
|
37569
|
+
}
|
|
37347
37570
|
function isStringLiteralExpression(expr) {
|
|
37348
37571
|
const trimmed = expr.trim();
|
|
37349
37572
|
if (trimmed.length < 2) return false;
|
|
@@ -37415,20 +37638,27 @@ function filterCleanVariableSinks(sinks, calls, taintedVars, symbols, dfg, sanit
|
|
|
37415
37638
|
dangerousArgCount++;
|
|
37416
37639
|
if (language === "bash" && arg.expression === call.method_name && !arg.variable && arg.literal == null) continue;
|
|
37417
37640
|
if (arg.variable && !arg.expression?.includes("[")) {
|
|
37418
|
-
const
|
|
37419
|
-
const
|
|
37420
|
-
|
|
37421
|
-
|
|
37422
|
-
|
|
37423
|
-
|
|
37424
|
-
|
|
37425
|
-
|
|
37426
|
-
|
|
37427
|
-
|
|
37641
|
+
const expr = arg.expression ?? "";
|
|
37642
|
+
const concatNames = language === "java" && isStringConcat(expr) ? argumentIdentifiers(expr) : [];
|
|
37643
|
+
const names = concatNames.length > 0 ? concatNames : [arg.variable];
|
|
37644
|
+
let argClean = true;
|
|
37645
|
+
for (const varName of names) {
|
|
37646
|
+
const scopedName = methodName ? `${methodName}:${varName}` : varName;
|
|
37647
|
+
if (fieldNames.has(varName) && !isInSynchronizedBlock) {
|
|
37648
|
+
argClean = false;
|
|
37649
|
+
break;
|
|
37650
|
+
}
|
|
37651
|
+
if (sanitizedVars?.has(scopedName) || sanitizedVars?.has(varName)) continue;
|
|
37652
|
+
if (taintedVars.has(scopedName) || taintedVars.has(varName)) {
|
|
37653
|
+
argClean = false;
|
|
37654
|
+
break;
|
|
37655
|
+
}
|
|
37656
|
+
const symbolValue = symbols.get(scopedName) ?? symbols.get(varName);
|
|
37657
|
+
if (symbolValue && symbolValue.type !== "unknown") continue;
|
|
37658
|
+
argClean = false;
|
|
37659
|
+
break;
|
|
37428
37660
|
}
|
|
37429
|
-
|
|
37430
|
-
if (symbolValue && symbolValue.type !== "unknown") continue;
|
|
37431
|
-
allArgsAreClean = false;
|
|
37661
|
+
if (!argClean) allArgsAreClean = false;
|
|
37432
37662
|
} else {
|
|
37433
37663
|
if (arg.literal != null) continue;
|
|
37434
37664
|
if (arg.expression && !arg.variable && isStringLiteralExpression(arg.expression)) continue;
|
|
@@ -37440,8 +37670,12 @@ function filterCleanVariableSinks(sinks, calls, taintedVars, symbols, dfg, sanit
|
|
|
37440
37670
|
return true;
|
|
37441
37671
|
});
|
|
37442
37672
|
}
|
|
37443
|
-
function filterSanitizedSinks(sinks, sanitizers, calls) {
|
|
37673
|
+
function filterSanitizedSinks(sinks, sanitizers, calls, sources) {
|
|
37444
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
|
+
}
|
|
37445
37679
|
const sanitizersByLine = /* @__PURE__ */ new Map();
|
|
37446
37680
|
for (const san of sanitizers) {
|
|
37447
37681
|
const existing = sanitizersByLine.get(san.line) ?? [];
|
|
@@ -37457,22 +37691,25 @@ function filterSanitizedSinks(sinks, sanitizers, calls) {
|
|
|
37457
37691
|
return sinks.filter((sink) => {
|
|
37458
37692
|
const lineSanitizers = sanitizersByLine.get(sink.line);
|
|
37459
37693
|
if (!lineSanitizers || lineSanitizers.length === 0) return true;
|
|
37460
|
-
|
|
37461
|
-
|
|
37462
|
-
|
|
37463
|
-
|
|
37464
|
-
|
|
37465
|
-
|
|
37466
|
-
|
|
37467
|
-
|
|
37468
|
-
|
|
37469
|
-
|
|
37470
|
-
|
|
37471
|
-
|
|
37472
|
-
|
|
37473
|
-
|
|
37474
|
-
|
|
37475
|
-
|
|
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;
|
|
37476
37713
|
}
|
|
37477
37714
|
}
|
|
37478
37715
|
}
|
|
@@ -38828,9 +39065,13 @@ var TaintPropagationPass = class {
|
|
|
38828
39065
|
const sansAtSink = sanitizersByLine.get(f.sink_line);
|
|
38829
39066
|
if (sansAtSink && sansAtSink.length > 0) {
|
|
38830
39067
|
for (const san of sansAtSink) {
|
|
38831
|
-
if (sanitizerCoversSink(san, f.sink_type))
|
|
38832
|
-
|
|
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;
|
|
38833
39073
|
}
|
|
39074
|
+
return false;
|
|
38834
39075
|
}
|
|
38835
39076
|
}
|
|
38836
39077
|
const usesAtSink = graph.usesByLine.get(f.sink_line);
|
|
@@ -40417,10 +40658,14 @@ var InterproceduralPass = class {
|
|
|
40417
40658
|
}
|
|
40418
40659
|
const sansAtSink = sanitizersByLine.get(f.sink_line);
|
|
40419
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;
|
|
40420
40663
|
for (const san of sansAtSink) {
|
|
40421
|
-
if (sanitizerCoversSink(san, f.sink_type))
|
|
40422
|
-
|
|
40664
|
+
if (!sanitizerCoversSink(san, f.sink_type)) continue;
|
|
40665
|
+
if (sinkVar && identifierOutsideSanitizerCalls(sinkVar, callsAtSink, sansAtSink, f.sink_type)) {
|
|
40666
|
+
continue;
|
|
40423
40667
|
}
|
|
40668
|
+
return false;
|
|
40424
40669
|
}
|
|
40425
40670
|
return true;
|
|
40426
40671
|
});
|
|
@@ -44369,6 +44614,14 @@ var PROVIDER_PATTERNS = [
|
|
|
44369
44614
|
level: "error",
|
|
44370
44615
|
fix: "Revoke the GitHub refresh token and store secrets outside source control."
|
|
44371
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
|
+
},
|
|
44372
44625
|
// GitLab personal / project / group access tokens use the `glpat-` prefix.
|
|
44373
44626
|
// Length is not fixed (legacy bodies are about 20 characters; routable
|
|
44374
44627
|
// tokens are longer), so the floor is 20 and there is no max. (#577)
|
|
@@ -44397,12 +44650,14 @@ var PROVIDER_PATTERNS = [
|
|
|
44397
44650
|
level: "error",
|
|
44398
44651
|
fix: "Rotate the Stripe restricted key in the Stripe Dashboard and load it from a secrets manager."
|
|
44399
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)
|
|
44400
44655
|
{
|
|
44401
44656
|
name: "Stripe live publishable key",
|
|
44402
44657
|
regex: /\bpk_live_[A-Za-z0-9]{24,}\b/,
|
|
44403
|
-
severity: "
|
|
44404
|
-
level: "
|
|
44405
|
-
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."
|
|
44406
44661
|
},
|
|
44407
44662
|
// Test-mode secret and restricted keys still authorize the test account.
|
|
44408
44663
|
// Same length floor as the live forms. Severity is high, not critical (#576).
|
|
@@ -44427,6 +44682,17 @@ var PROVIDER_PATTERNS = [
|
|
|
44427
44682
|
level: "error",
|
|
44428
44683
|
fix: "Revoke the OpenAI key at https://platform.openai.com/api-keys and load from environment."
|
|
44429
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
|
+
},
|
|
44430
44696
|
{
|
|
44431
44697
|
name: "Anthropic API key",
|
|
44432
44698
|
regex: /\bsk-ant-[A-Za-z0-9_-]{90,}\b/,
|
|
@@ -44719,7 +44985,7 @@ var ScanSecretsPass = class {
|
|
|
44719
44985
|
for (const pattern of PROVIDER_PATTERNS) {
|
|
44720
44986
|
const m = pattern.regex.exec(lineText);
|
|
44721
44987
|
if (!m) continue;
|
|
44722
|
-
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])) {
|
|
44723
44989
|
continue;
|
|
44724
44990
|
}
|
|
44725
44991
|
if (pattern.name === "PEM private key" && !pemHasInlineBody(lines, i2)) {
|
|
@@ -45151,8 +45417,6 @@ var PY_SET_COOKIE_RECEIVERS = /* @__PURE__ */ new Set([
|
|
|
45151
45417
|
]);
|
|
45152
45418
|
var PY_SECURE_TRUE_RE = /\bsecure\s*=\s*True\b/;
|
|
45153
45419
|
var PY_HTTPONLY_TRUE_RE = /\bhttponly\s*=\s*True\b/i;
|
|
45154
|
-
var JAVA_SET_SECURE_TRUE_RE = /\.setSecure\s*\(\s*true\s*\)/;
|
|
45155
|
-
var JAVA_SET_HTTPONLY_TRUE_RE = /\.setHttpOnly\s*\(\s*true\s*\)/;
|
|
45156
45420
|
var GO_SECURE_TRUE_RE = /\bSecure\s*:\s*true\b/;
|
|
45157
45421
|
var GO_HTTPONLY_TRUE_RE = /\bHttpOnly\s*:\s*true\b/;
|
|
45158
45422
|
var RUST_SET_COOKIE_MACRO_RE = /(format!|write!|writeln!)\s*\(([^()]*Set-Cookie[^()]*)\)/gis;
|
|
@@ -45181,10 +45445,8 @@ var InsecureCookiePass = class {
|
|
|
45181
45445
|
this.emit(ctx, file, det, "python");
|
|
45182
45446
|
}
|
|
45183
45447
|
} else if (language === "java") {
|
|
45184
|
-
const hasSetSecureTrue = JAVA_SET_SECURE_TRUE_RE.test(code);
|
|
45185
|
-
const hasSetHttpOnlyTrue = JAVA_SET_HTTPONLY_TRUE_RE.test(code);
|
|
45186
45448
|
for (const call of graph.ir.calls) {
|
|
45187
|
-
const det = this.detectJavaCookieCtor(call,
|
|
45449
|
+
const det = this.detectJavaCookieCtor(call, code);
|
|
45188
45450
|
if (!det) continue;
|
|
45189
45451
|
insecureCookies.push(det);
|
|
45190
45452
|
this.emit(ctx, file, det, "java");
|
|
@@ -45263,15 +45525,28 @@ var InsecureCookiePass = class {
|
|
|
45263
45525
|
};
|
|
45264
45526
|
}
|
|
45265
45527
|
// ---------------- Java ----------------
|
|
45266
|
-
|
|
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) {
|
|
45267
45542
|
const method = call.method_name ?? "";
|
|
45268
45543
|
const isCookieCtor = method === "Cookie" || method.endsWith(".Cookie");
|
|
45269
45544
|
if (!isCookieCtor) return null;
|
|
45270
45545
|
const looksLikeCtor = call.is_constructor || !call.receiver && (call.receiver_type === "Cookie" || (call.receiver_type ?? "").endsWith(".Cookie")) || (call.resolution?.target ?? "").endsWith(".<init>");
|
|
45271
45546
|
if (!looksLikeCtor) return null;
|
|
45272
45547
|
if (call.arguments.length < 2) return null;
|
|
45273
|
-
const missingSecure = !
|
|
45274
|
-
const missingHttpOnly = !
|
|
45548
|
+
const missingSecure = !this.javaCookieSetterTrue(code, call, "setSecure");
|
|
45549
|
+
const missingHttpOnly = !this.javaCookieSetterTrue(code, call, "setHttpOnly");
|
|
45275
45550
|
if (!missingSecure && !missingHttpOnly) return null;
|
|
45276
45551
|
return {
|
|
45277
45552
|
line: call.location.line,
|
|
@@ -49885,7 +50160,7 @@ async function analyzeForAPI(code, filePath, language, options = {}) {
|
|
|
49885
50160
|
constPropResult.sanitizedVars,
|
|
49886
50161
|
constPropResult.synchronizedLines
|
|
49887
50162
|
);
|
|
49888
|
-
filteredSinks = filterSanitizedSinks(filteredSinks, taint.sanitizers ?? [], calls);
|
|
50163
|
+
filteredSinks = filterSanitizedSinks(filteredSinks, taint.sanitizers ?? [], calls, taint.sources);
|
|
49889
50164
|
let pythonTaintedVars = /* @__PURE__ */ new Map();
|
|
49890
50165
|
if (language === "python") {
|
|
49891
50166
|
pythonTaintedVars = buildPythonTaintedVars(code);
|