circle-ir 3.57.0 → 3.58.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/configs/sinks/golang.json +61 -0
- package/configs/sinks/nodejs.json +11 -6
- package/configs/sinks/python.json +24 -0
- package/configs/sinks/rust.json +30 -0
- package/configs/sinks/sql.yaml +53 -0
- package/dist/analysis/config-loader.d.ts.map +1 -1
- package/dist/analysis/config-loader.js +57 -9
- package/dist/analysis/config-loader.js.map +1 -1
- package/dist/analysis/constant-propagation/patterns.d.ts.map +1 -1
- package/dist/analysis/constant-propagation/patterns.js +12 -0
- package/dist/analysis/constant-propagation/patterns.js.map +1 -1
- package/dist/analysis/constant-propagation/propagator.d.ts +62 -0
- package/dist/analysis/constant-propagation/propagator.d.ts.map +1 -1
- package/dist/analysis/constant-propagation/propagator.js +275 -7
- package/dist/analysis/constant-propagation/propagator.js.map +1 -1
- package/dist/analysis/passes/language-sources-pass.d.ts.map +1 -1
- package/dist/analysis/passes/language-sources-pass.js +55 -14
- package/dist/analysis/passes/language-sources-pass.js.map +1 -1
- package/dist/analysis/passes/security-headers-pass.d.ts.map +1 -1
- package/dist/analysis/passes/security-headers-pass.js +93 -0
- package/dist/analysis/passes/security-headers-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 +16 -1
- 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 +153 -9
- 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 +116 -2
- package/dist/analysis/taint-matcher.js.map +1 -1
- package/dist/analysis/taint-propagation.d.ts.map +1 -1
- package/dist/analysis/taint-propagation.js +25 -1
- package/dist/analysis/taint-propagation.js.map +1 -1
- package/dist/browser/circle-ir.js +500 -45
- package/dist/core/circle-ir-core.cjs +368 -21
- package/dist/core/circle-ir-core.js +368 -21
- package/dist/types/config.d.ts +7 -0
- package/dist/types/config.d.ts.map +1 -1
- package/package.json +1 -1
|
@@ -10863,11 +10863,14 @@ var DEFAULT_SOURCES = [
|
|
|
10863
10863
|
// Rocket
|
|
10864
10864
|
{ method: "param", class: "Request", type: "http_param", severity: "high", return_tainted: true },
|
|
10865
10865
|
{ method: "cookies", class: "Request", type: "http_cookie", severity: "high", return_tainted: true },
|
|
10866
|
-
// Axum extractors
|
|
10867
|
-
|
|
10868
|
-
|
|
10869
|
-
|
|
10870
|
-
{ method: "
|
|
10866
|
+
// Axum extractors — Rust-only. The simple names `Json`/`Query`/`Path`/`Form`
|
|
10867
|
+
// collide with stdlib types in other ecosystems (notably Python's
|
|
10868
|
+
// `pathlib.Path` constructor and `flask.Form`), so they MUST be
|
|
10869
|
+
// language-scoped to Rust to avoid spurious source matches.
|
|
10870
|
+
{ method: "Json", type: "http_body", severity: "high", return_tainted: true, languages: ["rust"] },
|
|
10871
|
+
{ method: "Query", type: "http_param", severity: "high", return_tainted: true, languages: ["rust"] },
|
|
10872
|
+
{ method: "Path", type: "http_path", severity: "high", return_tainted: true, languages: ["rust"] },
|
|
10873
|
+
{ method: "Form", type: "http_param", severity: "high", return_tainted: true, languages: ["rust"] },
|
|
10871
10874
|
// Rust std library
|
|
10872
10875
|
{ method: "var", class: "env", type: "env_input", severity: "medium", return_tainted: true },
|
|
10873
10876
|
{ method: "var_os", class: "env", type: "env_input", severity: "medium", return_tainted: true },
|
|
@@ -11070,10 +11073,15 @@ var DEFAULT_SINKS = [
|
|
|
11070
11073
|
{ method: "PathResource", class: "constructor", type: "path_traversal", cwe: "CWE-22", severity: "high", arg_positions: [0] },
|
|
11071
11074
|
// Additional resource/file patterns
|
|
11072
11075
|
{ method: "forFile", type: "path_traversal", cwe: "CWE-22", severity: "high", arg_positions: [0] },
|
|
11073
|
-
|
|
11074
|
-
|
|
11075
|
-
|
|
11076
|
-
|
|
11076
|
+
// Java NIO `Path.resolve(other)` — joining with an untrusted `other` can
|
|
11077
|
+
// escape the parent directory. Language-scoped to Java because the simple
|
|
11078
|
+
// name `resolve` collides with Python `pathlib.Path.resolve()`
|
|
11079
|
+
// (a canonicalization SANITIZER, no argument), JS `Promise.resolve(...)`,
|
|
11080
|
+
// and Rust `Path::canonicalize` variants. Sprint 9 #48.2.
|
|
11081
|
+
{ method: "resolve", class: "Path", type: "path_traversal", cwe: "CWE-22", severity: "high", arg_positions: [0], languages: ["java"] },
|
|
11082
|
+
{ method: "resolve", type: "path_traversal", cwe: "CWE-22", severity: "high", arg_positions: [0], languages: ["java"] },
|
|
11083
|
+
{ method: "resolveSibling", class: "Path", type: "path_traversal", cwe: "CWE-22", severity: "high", arg_positions: [0], languages: ["java"] },
|
|
11084
|
+
{ method: "relativize", class: "Path", type: "path_traversal", cwe: "CWE-22", severity: "medium", arg_positions: [0], languages: ["java"] },
|
|
11077
11085
|
// Static file configuration
|
|
11078
11086
|
{ method: "staticFiles", type: "path_traversal", cwe: "CWE-22", severity: "high", arg_positions: [0] },
|
|
11079
11087
|
{ method: "setRoot", type: "path_traversal", cwe: "CWE-22", severity: "high", arg_positions: [0] },
|
|
@@ -12230,6 +12238,16 @@ var DEFAULT_SANITIZERS = [
|
|
|
12230
12238
|
// Returns just filename, strips path
|
|
12231
12239
|
{ method: "canonicalize", removes: ["path_traversal"] },
|
|
12232
12240
|
// Resolves symlinks and normalizes
|
|
12241
|
+
// Go path sanitizers (#51) — filepath.Base strips directory components
|
|
12242
|
+
// (fully sanitizes), filepath.Clean / path.Clean normalize away ../ segments
|
|
12243
|
+
// (defense-in-depth — mirrors Java getCanonicalPath in this table; the
|
|
12244
|
+
// stricter Clean+HasPrefix guard recognition is tracked separately).
|
|
12245
|
+
// EvalSymlinks is the Go equivalent of Java's Path.toRealPath.
|
|
12246
|
+
{ method: "Base", class: "filepath", removes: ["path_traversal"] },
|
|
12247
|
+
{ method: "Base", class: "path", removes: ["path_traversal"] },
|
|
12248
|
+
{ method: "Clean", class: "filepath", removes: ["path_traversal"] },
|
|
12249
|
+
{ method: "Clean", class: "path", removes: ["path_traversal"] },
|
|
12250
|
+
{ method: "EvalSymlinks", class: "filepath", removes: ["path_traversal"] },
|
|
12233
12251
|
// Log Injection sanitizers
|
|
12234
12252
|
{ method: "replace", removes: ["log_injection"] },
|
|
12235
12253
|
// Used to remove newlines/control chars
|
|
@@ -12324,6 +12342,8 @@ var DEFAULT_SANITIZERS = [
|
|
|
12324
12342
|
{ method: "abspath", class: "os.path", removes: ["path_traversal"] },
|
|
12325
12343
|
{ method: "realpath", class: "path", removes: ["path_traversal"] },
|
|
12326
12344
|
{ method: "abspath", class: "path", removes: ["path_traversal"] },
|
|
12345
|
+
// pathlib.Path.resolve() — canonicalizes path, resolves symlinks (Python 3)
|
|
12346
|
+
{ method: "resolve", class: "Path", removes: ["path_traversal"] },
|
|
12327
12347
|
// Python Type coercion
|
|
12328
12348
|
{ method: "int", removes: ["sql_injection", "command_injection", "xss"] },
|
|
12329
12349
|
{ method: "float", removes: ["sql_injection", "command_injection"] },
|
|
@@ -12356,8 +12376,36 @@ var DEFAULT_SANITIZERS = [
|
|
|
12356
12376
|
{ method: "encode_attribute", class: "html_escape", removes: ["xss"] },
|
|
12357
12377
|
{ method: "escape_html", removes: ["xss"] },
|
|
12358
12378
|
// Rust Type coercion (parsing)
|
|
12359
|
-
{ method: "parse", removes: ["sql_injection", "command_injection", "xss"] }
|
|
12379
|
+
{ method: "parse", removes: ["sql_injection", "command_injection", "xss"] },
|
|
12360
12380
|
// str.parse::<i32>()
|
|
12381
|
+
// =========================================================================
|
|
12382
|
+
// Type-cast taint barriers (#57)
|
|
12383
|
+
// Numeric/UUID casts cannot carry a string-injection payload.
|
|
12384
|
+
// =========================================================================
|
|
12385
|
+
// Java numeric parse — Integer.parseInt, Long.parseLong, etc.
|
|
12386
|
+
{ method: "parseInt", class: "Integer", removes: ["sql_injection", "command_injection", "path_traversal", "code_injection"] },
|
|
12387
|
+
{ method: "parseLong", class: "Long", removes: ["sql_injection", "command_injection", "path_traversal", "code_injection"] },
|
|
12388
|
+
{ method: "parseFloat", class: "Float", removes: ["sql_injection", "command_injection", "path_traversal", "code_injection"] },
|
|
12389
|
+
{ method: "parseDouble", class: "Double", removes: ["sql_injection", "command_injection", "path_traversal", "code_injection"] },
|
|
12390
|
+
{ method: "parseShort", class: "Short", removes: ["sql_injection", "command_injection", "path_traversal", "code_injection"] },
|
|
12391
|
+
{ method: "parseByte", class: "Byte", removes: ["sql_injection", "command_injection", "path_traversal", "code_injection"] },
|
|
12392
|
+
// Java UUID parse — UUID.fromString rejects non-UUID strings
|
|
12393
|
+
{ method: "fromString", class: "UUID", removes: ["sql_injection", "command_injection", "path_traversal", "code_injection"] },
|
|
12394
|
+
// JavaScript numeric coercion (Number/parseInt/parseFloat already covered above; add path_traversal/code_injection)
|
|
12395
|
+
{ method: "BigInt", removes: ["sql_injection", "nosql_injection", "command_injection", "path_traversal", "code_injection"] },
|
|
12396
|
+
// Go numeric parse — strconv.Atoi, ParseInt, ParseFloat, ParseUint, ParseBool
|
|
12397
|
+
{ method: "Atoi", class: "strconv", removes: ["sql_injection", "command_injection", "path_traversal", "code_injection"] },
|
|
12398
|
+
{ method: "ParseInt", class: "strconv", removes: ["sql_injection", "command_injection", "path_traversal", "code_injection"] },
|
|
12399
|
+
{ method: "ParseFloat", class: "strconv", removes: ["sql_injection", "command_injection", "path_traversal", "code_injection"] },
|
|
12400
|
+
{ method: "ParseUint", class: "strconv", removes: ["sql_injection", "command_injection", "path_traversal", "code_injection"] },
|
|
12401
|
+
{ method: "ParseBool", class: "strconv", removes: ["sql_injection", "command_injection", "path_traversal", "code_injection"] },
|
|
12402
|
+
// Go UUID parse
|
|
12403
|
+
{ method: "Parse", class: "uuid", removes: ["sql_injection", "command_injection", "path_traversal", "code_injection"] },
|
|
12404
|
+
{ method: "MustParse", class: "uuid", removes: ["sql_injection", "command_injection", "path_traversal", "code_injection"] },
|
|
12405
|
+
// Python — int/float already covered above; add bool + UUID/Decimal casts
|
|
12406
|
+
{ method: "bool", removes: ["sql_injection", "command_injection", "xss", "code_injection"] },
|
|
12407
|
+
{ method: "UUID", class: "uuid", removes: ["sql_injection", "command_injection", "path_traversal", "code_injection"] },
|
|
12408
|
+
{ method: "Decimal", class: "decimal", removes: ["sql_injection", "command_injection", "path_traversal", "code_injection"] }
|
|
12361
12409
|
];
|
|
12362
12410
|
function getDefaultConfig() {
|
|
12363
12411
|
return {
|
|
@@ -12474,7 +12522,7 @@ function analyzeTaint(calls, types, config = getDefaultConfig(), typeHierarchy,
|
|
|
12474
12522
|
const sourceLines = code !== void 0 ? code.split("\n") : void 0;
|
|
12475
12523
|
const sources = findSources(calls, types, config.sources, sourceLines, language);
|
|
12476
12524
|
const sinks = findSinks(calls, config.sinks, typeHierarchy, language, sourceLines);
|
|
12477
|
-
const sanitizers = findSanitizers(calls, types, config.sanitizers);
|
|
12525
|
+
const sanitizers = findSanitizers(calls, types, config.sanitizers, sourceLines);
|
|
12478
12526
|
return { sources, sinks, sanitizers };
|
|
12479
12527
|
}
|
|
12480
12528
|
function attachSourceLineCode(sources, sinks, code) {
|
|
@@ -12494,6 +12542,9 @@ function findSources(calls, types, patterns, sourceLines, language) {
|
|
|
12494
12542
|
const sources = [];
|
|
12495
12543
|
for (const call of calls) {
|
|
12496
12544
|
for (const pattern of patterns) {
|
|
12545
|
+
if (pattern.languages && pattern.languages.length > 0 && language !== void 0 && !pattern.languages.includes(language)) {
|
|
12546
|
+
continue;
|
|
12547
|
+
}
|
|
12497
12548
|
if (matchesSourcePattern(call, pattern)) {
|
|
12498
12549
|
sources.push({
|
|
12499
12550
|
type: pattern.type,
|
|
@@ -13130,6 +13181,15 @@ function receiverMightBeClass(receiver, className) {
|
|
|
13130
13181
|
if (receiver === className) {
|
|
13131
13182
|
return true;
|
|
13132
13183
|
}
|
|
13184
|
+
if (receiver.endsWith(")")) {
|
|
13185
|
+
const ctorMatch = receiver.match(/^(\w+)\(/);
|
|
13186
|
+
if (ctorMatch) {
|
|
13187
|
+
const ctorName = ctorMatch[1];
|
|
13188
|
+
if (ctorName === className || ctorName.toLowerCase() === className.toLowerCase()) {
|
|
13189
|
+
return true;
|
|
13190
|
+
}
|
|
13191
|
+
}
|
|
13192
|
+
}
|
|
13133
13193
|
if (receiver.includes("::")) {
|
|
13134
13194
|
const scopePrefix = receiver.match(/^(\w+)::/);
|
|
13135
13195
|
if (scopePrefix) {
|
|
@@ -13397,7 +13457,7 @@ function calculateSinkConfidence(call, pattern) {
|
|
|
13397
13457
|
}
|
|
13398
13458
|
return Math.min(confidence, 1);
|
|
13399
13459
|
}
|
|
13400
|
-
function findSanitizers(calls, types, patterns) {
|
|
13460
|
+
function findSanitizers(calls, types, patterns, sourceLines) {
|
|
13401
13461
|
const sanitizers = [];
|
|
13402
13462
|
const sanitizerMethods = /* @__PURE__ */ new Set();
|
|
13403
13463
|
for (const type of types) {
|
|
@@ -13407,6 +13467,66 @@ function findSanitizers(calls, types, patterns) {
|
|
|
13407
13467
|
}
|
|
13408
13468
|
}
|
|
13409
13469
|
}
|
|
13470
|
+
const wrapperSanitizers = /* @__PURE__ */ new Map();
|
|
13471
|
+
for (const type of types) {
|
|
13472
|
+
for (const method of type.methods) {
|
|
13473
|
+
const bodySize = method.end_line - method.start_line;
|
|
13474
|
+
if (bodySize < 0 || bodySize > 2) continue;
|
|
13475
|
+
const paramNames = new Set(method.parameters.map((p) => p.name));
|
|
13476
|
+
if (paramNames.size === 0) continue;
|
|
13477
|
+
const inside = [];
|
|
13478
|
+
for (const c of calls) {
|
|
13479
|
+
if (c.location.line < method.start_line || c.location.line > method.end_line) continue;
|
|
13480
|
+
if (c.method_name === method.name) continue;
|
|
13481
|
+
inside.push(c);
|
|
13482
|
+
}
|
|
13483
|
+
if (inside.length !== 1) continue;
|
|
13484
|
+
const innerCall = inside[0];
|
|
13485
|
+
let matched;
|
|
13486
|
+
for (const pattern of patterns) {
|
|
13487
|
+
if (matchesSanitizerPattern(innerCall, pattern)) {
|
|
13488
|
+
matched = pattern;
|
|
13489
|
+
break;
|
|
13490
|
+
}
|
|
13491
|
+
}
|
|
13492
|
+
if (!matched || !matched.removes || matched.removes.length === 0) continue;
|
|
13493
|
+
let argOk = false;
|
|
13494
|
+
for (const arg of innerCall.arguments) {
|
|
13495
|
+
if (arg.variable && paramNames.has(arg.variable)) {
|
|
13496
|
+
argOk = true;
|
|
13497
|
+
break;
|
|
13498
|
+
}
|
|
13499
|
+
}
|
|
13500
|
+
if (!argOk) continue;
|
|
13501
|
+
if (sourceLines) {
|
|
13502
|
+
const lineText = sourceLines[innerCall.location.line - 1] ?? "";
|
|
13503
|
+
const stripped = lineText.trim();
|
|
13504
|
+
const returnMatch = stripped.match(/^return\s+(?:await\s+)?(.*)$/);
|
|
13505
|
+
if (!returnMatch) continue;
|
|
13506
|
+
const after = returnMatch[1].replace(/;\s*$/, "").trimEnd();
|
|
13507
|
+
const callPrefix = innerCall.receiver ? `${innerCall.receiver}.${innerCall.method_name}(` : `${innerCall.method_name}(`;
|
|
13508
|
+
if (!after.startsWith(callPrefix)) continue;
|
|
13509
|
+
if (!after.endsWith(")")) continue;
|
|
13510
|
+
}
|
|
13511
|
+
const existing = wrapperSanitizers.get(method.name);
|
|
13512
|
+
if (existing) {
|
|
13513
|
+
const set = /* @__PURE__ */ new Set([...existing, ...matched.removes]);
|
|
13514
|
+
wrapperSanitizers.set(method.name, Array.from(set));
|
|
13515
|
+
} else {
|
|
13516
|
+
wrapperSanitizers.set(method.name, [...matched.removes]);
|
|
13517
|
+
}
|
|
13518
|
+
}
|
|
13519
|
+
}
|
|
13520
|
+
for (const call of calls) {
|
|
13521
|
+
const removes = wrapperSanitizers.get(call.method_name);
|
|
13522
|
+
if (!removes) continue;
|
|
13523
|
+
sanitizers.push({
|
|
13524
|
+
type: "derived_wrapper",
|
|
13525
|
+
method: formatSanitizerMethod(call),
|
|
13526
|
+
line: call.location.line,
|
|
13527
|
+
sanitizes: removes
|
|
13528
|
+
});
|
|
13529
|
+
}
|
|
13410
13530
|
for (const call of calls) {
|
|
13411
13531
|
if (sanitizerMethods.has(call.method_name)) {
|
|
13412
13532
|
sanitizers.push({
|
|
@@ -14676,6 +14796,15 @@ var AnalysisPipeline = class {
|
|
|
14676
14796
|
};
|
|
14677
14797
|
|
|
14678
14798
|
// src/analysis/taint-propagation.ts
|
|
14799
|
+
function buildSanitizersByLine(sanitizers) {
|
|
14800
|
+
const out2 = /* @__PURE__ */ new Map();
|
|
14801
|
+
for (const san of sanitizers) {
|
|
14802
|
+
const existing = out2.get(san.line);
|
|
14803
|
+
if (existing) existing.push(san);
|
|
14804
|
+
else out2.set(san.line, [san]);
|
|
14805
|
+
}
|
|
14806
|
+
return out2;
|
|
14807
|
+
}
|
|
14679
14808
|
function propagateTaint(graphOrDfg, callsOrSources, sourcesOrSinks, sinksOrSanitizers, sanitizersArg) {
|
|
14680
14809
|
let graph;
|
|
14681
14810
|
let sources;
|
|
@@ -14711,7 +14840,7 @@ function propagateTaint(graphOrDfg, callsOrSources, sourcesOrSinks, sinksOrSanit
|
|
|
14711
14840
|
const defsByLine = graph.defsByLine;
|
|
14712
14841
|
const usesByLine = graph.usesByLine;
|
|
14713
14842
|
const callsByLine = graph.callsByLine;
|
|
14714
|
-
const sanitizersByLine = graph.sanitizersByLine;
|
|
14843
|
+
const sanitizersByLine = sanitizers.length > 0 ? buildSanitizersByLine(sanitizers) : graph.sanitizersByLine;
|
|
14715
14844
|
const defById = graph.defById;
|
|
14716
14845
|
const rawInitialTaint = findInitialTaint(sources, callsByLine, defsByLine);
|
|
14717
14846
|
const initialTaint = rawInitialTaint.filter((tv) => {
|
|
@@ -15826,7 +15955,32 @@ var SANITIZER_METHODS = /* @__PURE__ */ new Set([
|
|
|
15826
15955
|
"validatePath",
|
|
15827
15956
|
"validateCityName",
|
|
15828
15957
|
"validateInput",
|
|
15829
|
-
"sanitizeInput"
|
|
15958
|
+
"sanitizeInput",
|
|
15959
|
+
// Type-cast barriers (#57) — numeric/boolean casts cannot carry a string
|
|
15960
|
+
// injection payload. Conservative whitelist; ambiguous names like `valueOf`,
|
|
15961
|
+
// `Parse`, `fromString` are intentionally excluded.
|
|
15962
|
+
// Java
|
|
15963
|
+
"parseInt",
|
|
15964
|
+
"parseLong",
|
|
15965
|
+
"parseFloat",
|
|
15966
|
+
"parseDouble",
|
|
15967
|
+
"parseShort",
|
|
15968
|
+
"parseByte",
|
|
15969
|
+
"fromString",
|
|
15970
|
+
// UUID.fromString — parses strict UUID format, rejects injection
|
|
15971
|
+
// JS/TS (parseInt/parseFloat covered above)
|
|
15972
|
+
"Number",
|
|
15973
|
+
"BigInt",
|
|
15974
|
+
// Go
|
|
15975
|
+
"Atoi",
|
|
15976
|
+
"ParseInt",
|
|
15977
|
+
"ParseFloat",
|
|
15978
|
+
"ParseUint",
|
|
15979
|
+
"ParseBool",
|
|
15980
|
+
// Python
|
|
15981
|
+
"int",
|
|
15982
|
+
"float",
|
|
15983
|
+
"bool"
|
|
15830
15984
|
]);
|
|
15831
15985
|
var ANTI_SANITIZER_METHODS = /* @__PURE__ */ new Set([
|
|
15832
15986
|
// URL decoding (reverses URL encoding)
|
|
@@ -15956,6 +16110,10 @@ var ConstantPropagator = class _ConstantPropagator {
|
|
|
15956
16110
|
inConstructor = false;
|
|
15957
16111
|
// Map constructor parameter names to their positions (0-indexed)
|
|
15958
16112
|
constructorParamPositions = /* @__PURE__ */ new Map();
|
|
16113
|
+
// Sprint 9 #58.1 — names of `static final Pattern` fields whose compiled
|
|
16114
|
+
// regex is strict-anchored (provably matches a bounded character set).
|
|
16115
|
+
// Populated lazily on first access via `getSafePatternFields()`.
|
|
16116
|
+
safePatternFieldsCache = null;
|
|
15959
16117
|
/**
|
|
15960
16118
|
* Analyze source code and build constant propagation state.
|
|
15961
16119
|
*/
|
|
@@ -15989,6 +16147,7 @@ var ConstantPropagator = class _ConstantPropagator {
|
|
|
15989
16147
|
this.currentClassName = null;
|
|
15990
16148
|
this.inConstructor = false;
|
|
15991
16149
|
this.constructorParamPositions.clear();
|
|
16150
|
+
this.safePatternFieldsCache = null;
|
|
15992
16151
|
this.collectClassFields(tree.rootNode);
|
|
15993
16152
|
for (const methodName of sanitizerMethods) {
|
|
15994
16153
|
this.methodReturnsSanitized.add(methodName);
|
|
@@ -15998,6 +16157,7 @@ var ConstantPropagator = class _ConstantPropagator {
|
|
|
15998
16157
|
(name2) => this.lookupSymbol(name2)
|
|
15999
16158
|
);
|
|
16000
16159
|
this.analyzeMethodReturns(tree.rootNode);
|
|
16160
|
+
this.seedPythonModuleConstants(tree.rootNode);
|
|
16001
16161
|
this.visit(tree.rootNode);
|
|
16002
16162
|
this.refineTaintFromConstants();
|
|
16003
16163
|
const resultTainted = new Set(this.tainted);
|
|
@@ -16358,6 +16518,162 @@ var ConstantPropagator = class _ConstantPropagator {
|
|
|
16358
16518
|
}
|
|
16359
16519
|
}
|
|
16360
16520
|
}
|
|
16521
|
+
/**
|
|
16522
|
+
* Sprint 9 #55 — seed the symbol table with Python module-level constant
|
|
16523
|
+
* assignments. Walks only direct children of the `module` root and adds
|
|
16524
|
+
* `IDENT = <primitive literal>` to `symbols` so `if IDENT:` guards inside
|
|
16525
|
+
* downstream functions can be folded to dead code.
|
|
16526
|
+
*
|
|
16527
|
+
* Recognized literal RHS kinds: `true`/`false` (booleans), integer/float
|
|
16528
|
+
* literals, string literals. The ExpressionEvaluator already understands
|
|
16529
|
+
* each via the same lookup callback; we just need the symbol present.
|
|
16530
|
+
*/
|
|
16531
|
+
/**
|
|
16532
|
+
* Sprint 9 #55 — gate `field_declaration` folding to primitive literals.
|
|
16533
|
+
*
|
|
16534
|
+
* The deep-nesting regression (cognium-ai#88) constructs a Java
|
|
16535
|
+
* `static final String hyphenData = "a" + "b" + ... (10k segments)` at the
|
|
16536
|
+
* class level. `handleVariableDeclaration` would otherwise dispatch
|
|
16537
|
+
* `evaluateExpression` on the deeply nested binary AST and blow the V8
|
|
16538
|
+
* stack. The dead-code-by-const-guard pattern (`if (DEBUG)`) only requires
|
|
16539
|
+
* `boolean`/`integer`/`string` (single-literal) RHS folding, so restrict
|
|
16540
|
+
* to those node types.
|
|
16541
|
+
*/
|
|
16542
|
+
fieldDeclHasPrimitiveLiteralValue(node) {
|
|
16543
|
+
const primitive = /* @__PURE__ */ new Set([
|
|
16544
|
+
// Java literal node types
|
|
16545
|
+
"true",
|
|
16546
|
+
"false",
|
|
16547
|
+
"null_literal",
|
|
16548
|
+
"decimal_integer_literal",
|
|
16549
|
+
"hex_integer_literal",
|
|
16550
|
+
"octal_integer_literal",
|
|
16551
|
+
"binary_integer_literal",
|
|
16552
|
+
"decimal_floating_point_literal",
|
|
16553
|
+
"hex_floating_point_literal",
|
|
16554
|
+
"character_literal",
|
|
16555
|
+
"string_literal",
|
|
16556
|
+
// JS/TS literal node types (defensive, in case other langs reuse it)
|
|
16557
|
+
"number",
|
|
16558
|
+
"string"
|
|
16559
|
+
]);
|
|
16560
|
+
for (const child of node.children) {
|
|
16561
|
+
if (child.type !== "variable_declarator") continue;
|
|
16562
|
+
const value = child.childForFieldName("value");
|
|
16563
|
+
if (!value) continue;
|
|
16564
|
+
if (!primitive.has(value.type)) return false;
|
|
16565
|
+
}
|
|
16566
|
+
return true;
|
|
16567
|
+
}
|
|
16568
|
+
/**
|
|
16569
|
+
* Sprint 9 #58.1 — collect the set of class-level `Pattern` field names
|
|
16570
|
+
* whose compiled regex is strict-anchored, i.e. provably matches a
|
|
16571
|
+
* bounded character set with no wildcard escape. A subsequent
|
|
16572
|
+
* `if (!FIELD.matcher(var).matches()) throw ...;` guard then proves
|
|
16573
|
+
* `var` is sanitized after the if.
|
|
16574
|
+
*
|
|
16575
|
+
* Recognized initializer shapes (scanned via source-text regex to avoid
|
|
16576
|
+
* threading another AST walk):
|
|
16577
|
+
* `static final Pattern FIELD = Pattern.compile("regex");`
|
|
16578
|
+
*
|
|
16579
|
+
* Strict-anchored regex criteria:
|
|
16580
|
+
* - starts with `^` and ends with `$`
|
|
16581
|
+
* - after stripping `[...]` character classes, must not contain `.` or
|
|
16582
|
+
* `|` (a `.` could match anything; `|` admits an arbitrary alternative)
|
|
16583
|
+
*/
|
|
16584
|
+
getSafePatternFields() {
|
|
16585
|
+
if (this.safePatternFieldsCache !== null) return this.safePatternFieldsCache;
|
|
16586
|
+
const set = /* @__PURE__ */ new Set();
|
|
16587
|
+
const re = /\b(?:public\s+|private\s+|protected\s+)?(?:static\s+final|final\s+static)\s+(?:java\.util\.regex\.)?Pattern\s+(\w+)\s*=\s*(?:java\.util\.regex\.)?Pattern\s*\.\s*compile\s*\(\s*"((?:[^"\\]|\\.)*)"/g;
|
|
16588
|
+
let m;
|
|
16589
|
+
while ((m = re.exec(this.source)) !== null) {
|
|
16590
|
+
const name2 = m[1];
|
|
16591
|
+
const regex = m[2];
|
|
16592
|
+
if (this.isStrictAnchoredRegex(regex)) set.add(name2);
|
|
16593
|
+
}
|
|
16594
|
+
this.safePatternFieldsCache = set;
|
|
16595
|
+
return set;
|
|
16596
|
+
}
|
|
16597
|
+
isStrictAnchoredRegex(re) {
|
|
16598
|
+
if (!re.startsWith("^") || !re.endsWith("$")) return false;
|
|
16599
|
+
const stripped = re.replace(/\[(?:[^\]\\]|\\.)*\]/g, "");
|
|
16600
|
+
const cleaned = stripped.replace(/\\./g, "");
|
|
16601
|
+
if (cleaned.includes(".")) return false;
|
|
16602
|
+
if (cleaned.includes("|")) return false;
|
|
16603
|
+
return true;
|
|
16604
|
+
}
|
|
16605
|
+
/**
|
|
16606
|
+
* Sprint 9 #58.1 — detect the regex-allowlist guard pattern.
|
|
16607
|
+
*
|
|
16608
|
+
* if (!SAFE_NAME.matcher(var).matches()) { throw ...; }
|
|
16609
|
+
*
|
|
16610
|
+
* Returns the guarded variable name if the pattern matches AND
|
|
16611
|
+
* `SAFE_NAME` is a recognized strict-anchored Pattern field, otherwise
|
|
16612
|
+
* null. Caller drops the variable from `tainted` after the if-block.
|
|
16613
|
+
*/
|
|
16614
|
+
detectRegexAllowlistGuard(condition, consequence) {
|
|
16615
|
+
if (!consequence) return null;
|
|
16616
|
+
let condText = getNodeText2(condition, this.source).replace(/\s+/g, "");
|
|
16617
|
+
while (condText.startsWith("(") && condText.endsWith(")")) {
|
|
16618
|
+
const inner = condText.slice(1, -1);
|
|
16619
|
+
let depth = 0;
|
|
16620
|
+
let balanced = true;
|
|
16621
|
+
for (let i2 = 0; i2 < inner.length; i2++) {
|
|
16622
|
+
if (inner[i2] === "(") depth++;
|
|
16623
|
+
else if (inner[i2] === ")") depth--;
|
|
16624
|
+
if (depth < 0) {
|
|
16625
|
+
balanced = false;
|
|
16626
|
+
break;
|
|
16627
|
+
}
|
|
16628
|
+
}
|
|
16629
|
+
if (!balanced || depth !== 0) break;
|
|
16630
|
+
condText = inner;
|
|
16631
|
+
}
|
|
16632
|
+
const m = condText.match(/^!(\w+)\.matcher\((\w+)\)\.matches\(\)$/);
|
|
16633
|
+
if (!m) return null;
|
|
16634
|
+
const patternName = m[1];
|
|
16635
|
+
const varName = m[2];
|
|
16636
|
+
if (!this.getSafePatternFields().has(patternName)) return null;
|
|
16637
|
+
if (!this.consequenceContainsThrow(consequence)) return null;
|
|
16638
|
+
return varName;
|
|
16639
|
+
}
|
|
16640
|
+
consequenceContainsThrow(node) {
|
|
16641
|
+
if (node.type === "throw_statement") return true;
|
|
16642
|
+
const stack = [node];
|
|
16643
|
+
while (stack.length > 0) {
|
|
16644
|
+
const n = stack.pop();
|
|
16645
|
+
if (!n) continue;
|
|
16646
|
+
if (n.type === "throw_statement") return true;
|
|
16647
|
+
if (n.type === "if_statement" || n.type === "switch_statement") continue;
|
|
16648
|
+
for (const c of n.children) stack.push(c);
|
|
16649
|
+
}
|
|
16650
|
+
return false;
|
|
16651
|
+
}
|
|
16652
|
+
seedPythonModuleConstants(root) {
|
|
16653
|
+
if (root.type !== "module") return;
|
|
16654
|
+
for (const child of root.children) {
|
|
16655
|
+
const target = child.type === "assignment" ? child : child.type === "expression_statement" && child.children.length > 0 ? child.children[0] : null;
|
|
16656
|
+
if (!target || target.type !== "assignment") continue;
|
|
16657
|
+
const left = target.childForFieldName("left");
|
|
16658
|
+
const right = target.childForFieldName("right");
|
|
16659
|
+
if (!left || !right) continue;
|
|
16660
|
+
if (left.type !== "identifier") continue;
|
|
16661
|
+
const allowed = /* @__PURE__ */ new Set([
|
|
16662
|
+
"true",
|
|
16663
|
+
"false",
|
|
16664
|
+
"none",
|
|
16665
|
+
"integer",
|
|
16666
|
+
"float",
|
|
16667
|
+
"string"
|
|
16668
|
+
]);
|
|
16669
|
+
if (!allowed.has(right.type)) continue;
|
|
16670
|
+
const name2 = getNodeText2(left, this.source);
|
|
16671
|
+
if (!name2) continue;
|
|
16672
|
+
const value = this.evaluateExpression(right);
|
|
16673
|
+
if (!isKnown(value)) continue;
|
|
16674
|
+
this.symbols.set(name2, value);
|
|
16675
|
+
}
|
|
16676
|
+
}
|
|
16361
16677
|
findAllMethods(node) {
|
|
16362
16678
|
const methods = [];
|
|
16363
16679
|
const stack = [node];
|
|
@@ -16452,6 +16768,11 @@ var ConstantPropagator = class _ConstantPropagator {
|
|
|
16452
16768
|
case "local_variable_declaration":
|
|
16453
16769
|
this.handleVariableDeclaration(node);
|
|
16454
16770
|
return false;
|
|
16771
|
+
case "field_declaration":
|
|
16772
|
+
if (this.fieldDeclHasPrimitiveLiteralValue(node)) {
|
|
16773
|
+
this.handleVariableDeclaration(node);
|
|
16774
|
+
}
|
|
16775
|
+
return false;
|
|
16455
16776
|
case "assignment_expression":
|
|
16456
16777
|
this.handleAssignment(node);
|
|
16457
16778
|
return false;
|
|
@@ -16942,6 +17263,16 @@ var ConstantPropagator = class _ConstantPropagator {
|
|
|
16942
17263
|
}
|
|
16943
17264
|
this.inConditionalBranch = wasInConditional;
|
|
16944
17265
|
this.tainted = /* @__PURE__ */ new Set([...taintedBefore, ...taintedAfterThen, ...taintedAfterElse]);
|
|
17266
|
+
const guardedVar = this.detectRegexAllowlistGuard(condition, consequence);
|
|
17267
|
+
if (guardedVar) {
|
|
17268
|
+
this.tainted.delete(guardedVar);
|
|
17269
|
+
this.sanitizedVars.add(guardedVar);
|
|
17270
|
+
const scoped = this.getScopedName(guardedVar);
|
|
17271
|
+
if (scoped !== guardedVar) {
|
|
17272
|
+
this.tainted.delete(scoped);
|
|
17273
|
+
this.sanitizedVars.add(scoped);
|
|
17274
|
+
}
|
|
17275
|
+
}
|
|
16945
17276
|
}
|
|
16946
17277
|
}
|
|
16947
17278
|
/**
|
|
@@ -17132,17 +17463,33 @@ var ConstantPropagator = class _ConstantPropagator {
|
|
|
17132
17463
|
/**
|
|
17133
17464
|
* Check if an expression is a call to a sanitizer method.
|
|
17134
17465
|
* This includes both built-in sanitizers and @sanitizer annotated methods.
|
|
17466
|
+
* Handles Java (`method_invocation`), Go/JS/TS (`call_expression`), and
|
|
17467
|
+
* Python (`call`) AST shapes.
|
|
17135
17468
|
*/
|
|
17136
17469
|
isSanitizerMethodCall(node) {
|
|
17137
|
-
|
|
17138
|
-
|
|
17470
|
+
const methodName = this.extractCallName(node);
|
|
17471
|
+
if (!methodName) return false;
|
|
17472
|
+
return SANITIZER_METHODS.has(methodName) || this.methodReturnsSanitized.has(methodName);
|
|
17473
|
+
}
|
|
17474
|
+
/**
|
|
17475
|
+
* Extract the trailing method/function name from any call node shape:
|
|
17476
|
+
* Java `method_invocation` — name field
|
|
17477
|
+
* Go/JS `call_expression` — function field (identifier or selector/member)
|
|
17478
|
+
* Python `call` — function field (identifier or attribute)
|
|
17479
|
+
*/
|
|
17480
|
+
extractCallName(node) {
|
|
17481
|
+
let fnNode = null;
|
|
17482
|
+
if (node.type === "method_invocation") {
|
|
17483
|
+
fnNode = node.childForFieldName("name");
|
|
17484
|
+
} else if (node.type === "call_expression" || node.type === "call") {
|
|
17485
|
+
fnNode = node.childForFieldName("function");
|
|
17139
17486
|
}
|
|
17140
|
-
|
|
17141
|
-
if (
|
|
17142
|
-
|
|
17487
|
+
if (!fnNode) return null;
|
|
17488
|
+
if (fnNode.type === "selector_expression" || fnNode.type === "member_expression" || fnNode.type === "attribute") {
|
|
17489
|
+
const tail = fnNode.childForFieldName("field") || fnNode.childForFieldName("property") || fnNode.childForFieldName("attribute");
|
|
17490
|
+
if (tail) return getNodeText2(tail, this.source);
|
|
17143
17491
|
}
|
|
17144
|
-
|
|
17145
|
-
return SANITIZER_METHODS.has(methodName) || this.methodReturnsSanitized.has(methodName);
|
|
17492
|
+
return getNodeText2(fnNode, this.source);
|
|
17146
17493
|
}
|
|
17147
17494
|
/**
|
|
17148
17495
|
* Check if an expression is a call to an anti-sanitizer method.
|
|
@@ -22110,25 +22457,42 @@ function findBashTaintSources(sourceCode, dfg) {
|
|
|
22110
22457
|
const sources = [];
|
|
22111
22458
|
const lines = sourceCode.split("\n");
|
|
22112
22459
|
const definedVars = new Set(dfg.defs.filter((d) => d.kind === "local").map((d) => d.variable));
|
|
22460
|
+
const fnHeaderRe = /^\s*(?:function\s+)?[A-Za-z_][\w-]*\s*\(\s*\)\s*\{?\s*$|^\s*function\s+[A-Za-z_][\w-]*\s*\{?\s*$/;
|
|
22461
|
+
let braceDepth = 0;
|
|
22113
22462
|
for (let i2 = 0; i2 < lines.length; i2++) {
|
|
22114
22463
|
const line = lines[i2];
|
|
22115
22464
|
const trimmed = line.trim();
|
|
22116
22465
|
const lineNumber = i2 + 1;
|
|
22117
22466
|
if (trimmed.startsWith("#")) continue;
|
|
22118
|
-
const
|
|
22119
|
-
|
|
22120
|
-
|
|
22121
|
-
|
|
22122
|
-
|
|
22123
|
-
|
|
22124
|
-
sources.
|
|
22125
|
-
|
|
22126
|
-
|
|
22127
|
-
|
|
22128
|
-
|
|
22129
|
-
|
|
22130
|
-
|
|
22131
|
-
|
|
22467
|
+
const insideFunction = braceDepth > 0;
|
|
22468
|
+
if (!insideFunction) {
|
|
22469
|
+
const positionalRe = /\$([1-9@*])|\$\{([1-9@*])\}/g;
|
|
22470
|
+
let m;
|
|
22471
|
+
while ((m = positionalRe.exec(line)) !== null) {
|
|
22472
|
+
const param = m[1] ?? m[2];
|
|
22473
|
+
const alreadyExists = sources.some((s) => s.line === lineNumber && s.variable === param);
|
|
22474
|
+
if (!alreadyExists) {
|
|
22475
|
+
sources.push({
|
|
22476
|
+
type: "io_input",
|
|
22477
|
+
location: `positional parameter $${param}`,
|
|
22478
|
+
severity: "high",
|
|
22479
|
+
line: lineNumber,
|
|
22480
|
+
confidence: 1,
|
|
22481
|
+
variable: param
|
|
22482
|
+
});
|
|
22483
|
+
}
|
|
22484
|
+
}
|
|
22485
|
+
}
|
|
22486
|
+
if (fnHeaderRe.test(line) || /^\s*[A-Za-z_][\w-]*\s*\(\s*\)\s*\{/.test(line)) {
|
|
22487
|
+
const openBracesOnLine = (line.match(/\{/g) ?? []).length;
|
|
22488
|
+
const closeBracesOnLine = (line.match(/\}/g) ?? []).length;
|
|
22489
|
+
braceDepth += openBracesOnLine - closeBracesOnLine;
|
|
22490
|
+
} else {
|
|
22491
|
+
if (braceDepth > 0) {
|
|
22492
|
+
const openBracesOnLine = (line.match(/\{/g) ?? []).length;
|
|
22493
|
+
const closeBracesOnLine = (line.match(/\}/g) ?? []).length;
|
|
22494
|
+
braceDepth += openBracesOnLine - closeBracesOnLine;
|
|
22495
|
+
if (braceDepth < 0) braceDepth = 0;
|
|
22132
22496
|
}
|
|
22133
22497
|
}
|
|
22134
22498
|
const cmdSubAssign = trimmed.match(/^(\w+)=\$\((\w+)\s/);
|
|
@@ -22550,10 +22914,14 @@ function filterCleanVariableSinks(sinks, calls, taintedVars, symbols, dfg, sanit
|
|
|
22550
22914
|
const callsAtSink = callsByLine.get(sink.line) ?? [];
|
|
22551
22915
|
const isInSynchronizedBlock = synchronizedLines?.has(sink.line) ?? false;
|
|
22552
22916
|
const relevantCalls = sink.method ? callsAtSink.filter((c) => c.method_name === sink.method) : callsAtSink;
|
|
22917
|
+
const trustArgPositions = language !== "bash" && language !== "shell";
|
|
22553
22918
|
for (const call of relevantCalls) {
|
|
22554
22919
|
let allArgsAreClean = true;
|
|
22920
|
+
let dangerousArgCount = 0;
|
|
22555
22921
|
const methodName = call.in_method;
|
|
22556
22922
|
for (const arg of call.arguments) {
|
|
22923
|
+
if (trustArgPositions && sink.argPositions && sink.argPositions.length > 0 && !sink.argPositions.includes(arg.position)) continue;
|
|
22924
|
+
dangerousArgCount++;
|
|
22557
22925
|
if (language === "bash" && arg.expression === call.method_name && !arg.variable && arg.literal == null) continue;
|
|
22558
22926
|
if (arg.variable && !arg.expression?.includes("[")) {
|
|
22559
22927
|
const varName = arg.variable;
|
|
@@ -22576,7 +22944,7 @@ function filterCleanVariableSinks(sinks, calls, taintedVars, symbols, dfg, sanit
|
|
|
22576
22944
|
allArgsAreClean = false;
|
|
22577
22945
|
}
|
|
22578
22946
|
}
|
|
22579
|
-
if (allArgsAreClean &&
|
|
22947
|
+
if (allArgsAreClean && dangerousArgCount > 0) return false;
|
|
22580
22948
|
}
|
|
22581
22949
|
return true;
|
|
22582
22950
|
});
|
|
@@ -22668,7 +23036,7 @@ var TaintPropagationPass = class {
|
|
|
22668
23036
|
flows.push(f);
|
|
22669
23037
|
}
|
|
22670
23038
|
}
|
|
22671
|
-
const collectionFlows = detectCollectionFlows(calls, sources, sinks, constProp.tainted, constProp.unreachableLines) ?? [];
|
|
23039
|
+
const collectionFlows = detectCollectionFlows(calls, sources, sinks, constProp.tainted, constProp.unreachableLines, ctx.code) ?? [];
|
|
22672
23040
|
for (const f of collectionFlows) {
|
|
22673
23041
|
if (flows.some((x) => x.source_line === f.source_line && x.sink_line === f.sink_line)) continue;
|
|
22674
23042
|
const flowForCheck = {
|
|
@@ -22687,13 +23055,13 @@ var TaintPropagationPass = class {
|
|
|
22687
23055
|
if (isFP) continue;
|
|
22688
23056
|
flows.push(f);
|
|
22689
23057
|
}
|
|
22690
|
-
const paramFlows = detectParameterSinkFlows(types, calls, sources, sinks, constProp.unreachableLines) ?? [];
|
|
23058
|
+
const paramFlows = detectParameterSinkFlows(types, calls, sources, sinks, constProp.unreachableLines, constProp.tainted, ctx.code) ?? [];
|
|
22691
23059
|
for (const f of paramFlows) {
|
|
22692
23060
|
if (!flows.some((x) => x.source_line === f.source_line && x.sink_line === f.sink_line)) {
|
|
22693
23061
|
flows.push(f);
|
|
22694
23062
|
}
|
|
22695
23063
|
}
|
|
22696
|
-
const exprScanFlows = detectExpressionScanFlows(calls, sources, sinks, sanitizers, constProp.unreachableLines, ctx.code, ctx.language) ?? [];
|
|
23064
|
+
const exprScanFlows = detectExpressionScanFlows(calls, sources, sinks, sanitizers, constProp.unreachableLines, constProp.tainted, ctx.code, ctx.language) ?? [];
|
|
22697
23065
|
for (const f of exprScanFlows) {
|
|
22698
23066
|
if (flows.some(
|
|
22699
23067
|
(x) => x.source_line === f.source_line && x.sink_line === f.sink_line && x.sink_type === f.sink_type
|
|
@@ -22714,10 +23082,21 @@ var TaintPropagationPass = class {
|
|
|
22714
23082
|
if (isFP) continue;
|
|
22715
23083
|
flows.push(f);
|
|
22716
23084
|
}
|
|
22717
|
-
|
|
23085
|
+
const sanitizedNames = constProp.sanitizedVars;
|
|
23086
|
+
const finalFlows = sanitizedNames.size === 0 ? flows : flows.filter((f) => {
|
|
23087
|
+
if (f.path.length === 0) return true;
|
|
23088
|
+
const sourceVar = f.path[0].variable;
|
|
23089
|
+
if (!sourceVar) return true;
|
|
23090
|
+
if (sanitizedNames.has(sourceVar)) return false;
|
|
23091
|
+
for (const s of sanitizedNames) {
|
|
23092
|
+
if (s.endsWith(`:${sourceVar}`)) return false;
|
|
23093
|
+
}
|
|
23094
|
+
return true;
|
|
23095
|
+
});
|
|
23096
|
+
return { flows: finalFlows };
|
|
22718
23097
|
}
|
|
22719
23098
|
};
|
|
22720
|
-
function detectCollectionFlows(calls, sources, sinks, taintedVars, unreachableLines) {
|
|
23099
|
+
function detectCollectionFlows(calls, sources, sinks, taintedVars, unreachableLines, code) {
|
|
22721
23100
|
const flows = [];
|
|
22722
23101
|
const callsByLine = /* @__PURE__ */ new Map();
|
|
22723
23102
|
for (const call of calls) {
|
|
@@ -22739,6 +23118,9 @@ function detectCollectionFlows(calls, sources, sinks, taintedVars, unreachableLi
|
|
|
22739
23118
|
if (taintedVars.has(varName) || taintedVars.has(scopedName)) {
|
|
22740
23119
|
const source = sources[0];
|
|
22741
23120
|
if (source) {
|
|
23121
|
+
if (typeof code === "string" && isReassignedToLiteralBetween(code, varName, source.line, sink.line)) {
|
|
23122
|
+
continue;
|
|
23123
|
+
}
|
|
22742
23124
|
flows.push({
|
|
22743
23125
|
source_line: source.line,
|
|
22744
23126
|
sink_line: sink.line,
|
|
@@ -22773,6 +23155,9 @@ function detectCollectionFlows(calls, sources, sinks, taintedVars, unreachableLi
|
|
|
22773
23155
|
if (taintedVars.has(collectionVar) || taintedVars.has(scopedCollection)) {
|
|
22774
23156
|
const source = sources[0];
|
|
22775
23157
|
if (source) {
|
|
23158
|
+
if (typeof code === "string" && isReassignedToLiteralBetween(code, collectionVar, source.line, sink.line)) {
|
|
23159
|
+
continue;
|
|
23160
|
+
}
|
|
22776
23161
|
flows.push({
|
|
22777
23162
|
source_line: source.line,
|
|
22778
23163
|
sink_line: sink.line,
|
|
@@ -22842,7 +23227,7 @@ function detectArrayElementFlows(calls, sources, sinks, taintedArrayElements, un
|
|
|
22842
23227
|
}
|
|
22843
23228
|
return flows;
|
|
22844
23229
|
}
|
|
22845
|
-
function detectParameterSinkFlows(types, calls, sources, sinks, unreachableLines) {
|
|
23230
|
+
function detectParameterSinkFlows(types, calls, sources, sinks, unreachableLines, tainted, code) {
|
|
22846
23231
|
const flows = [];
|
|
22847
23232
|
const paramSourcesByMethod = /* @__PURE__ */ new Map();
|
|
22848
23233
|
for (const source of sources) {
|
|
@@ -22884,6 +23269,9 @@ function detectParameterSinkFlows(types, calls, sources, sinks, unreachableLines
|
|
|
22884
23269
|
if (paramSource) {
|
|
22885
23270
|
const exists = flows.some((f) => f.source_line === paramSource.line && f.sink_line === sink.line);
|
|
22886
23271
|
if (!exists) {
|
|
23272
|
+
if (typeof code === "string" && isReassignedToLiteralBetween(code, arg.variable, paramSource.line, sink.line)) {
|
|
23273
|
+
continue;
|
|
23274
|
+
}
|
|
22887
23275
|
flows.push({
|
|
22888
23276
|
source_line: paramSource.line,
|
|
22889
23277
|
sink_line: sink.line,
|
|
@@ -22905,7 +23293,27 @@ function detectParameterSinkFlows(types, calls, sources, sinks, unreachableLines
|
|
|
22905
23293
|
void types;
|
|
22906
23294
|
return flows;
|
|
22907
23295
|
}
|
|
22908
|
-
function
|
|
23296
|
+
function isReassignedToLiteralBetween(code, variable, srcLine, sinkLine) {
|
|
23297
|
+
if (!variable || sinkLine - srcLine < 2) return false;
|
|
23298
|
+
if (!/^[A-Za-z_][\w]*$/.test(variable)) return false;
|
|
23299
|
+
const lines = code.split("\n");
|
|
23300
|
+
const lo = Math.max(0, srcLine);
|
|
23301
|
+
const hi = Math.min(lines.length, sinkLine - 1);
|
|
23302
|
+
const strLit = `(?:"[^"\\\\]*(?:\\\\.[^"\\\\]*)*"|'[^'\\\\]*(?:\\\\.[^'\\\\]*)*'|\`[^\`\\\\]*(?:\\\\.[^\`\\\\]*)*\`)`;
|
|
23303
|
+
const reNaked = new RegExp(
|
|
23304
|
+
`^\\s*${variable}\\s*(?::?=)\\s*${strLit}\\s*;?\\s*$`
|
|
23305
|
+
);
|
|
23306
|
+
const reGuarded = new RegExp(
|
|
23307
|
+
`^\\s*if\\b.*\\b${variable}\\s*=\\s*${strLit}\\s*;?\\s*$`
|
|
23308
|
+
);
|
|
23309
|
+
for (let i2 = lo; i2 < hi; i2++) {
|
|
23310
|
+
const line = lines[i2];
|
|
23311
|
+
if (!line) continue;
|
|
23312
|
+
if (reNaked.test(line) || reGuarded.test(line)) return true;
|
|
23313
|
+
}
|
|
23314
|
+
return false;
|
|
23315
|
+
}
|
|
23316
|
+
function detectExpressionScanFlows(calls, sources, sinks, sanitizers, unreachableLines, tainted, code, language) {
|
|
22909
23317
|
const flows = [];
|
|
22910
23318
|
const sourcesWithVar = sources.filter(
|
|
22911
23319
|
(s) => typeof s.variable === "string" && s.variable.length > 0
|
|
@@ -22957,9 +23365,9 @@ function detectExpressionScanFlows(calls, sources, sinks, sanitizers, unreachabl
|
|
|
22957
23365
|
if (!rhsMatch) continue;
|
|
22958
23366
|
const rhs = rhsMatch[1];
|
|
22959
23367
|
for (const san of lineSans) {
|
|
22960
|
-
const sanMatch = san.method.match(
|
|
23368
|
+
const sanMatch = san.method.match(/(\w+)\(\)$/);
|
|
22961
23369
|
if (!sanMatch) continue;
|
|
22962
|
-
const sanName = sanMatch[1]
|
|
23370
|
+
const sanName = sanMatch[1];
|
|
22963
23371
|
if (!rhs.includes(`${sanName}(`)) continue;
|
|
22964
23372
|
let set = aliasSanitizedFor.get(varName);
|
|
22965
23373
|
if (!set) {
|
|
@@ -23023,6 +23431,9 @@ function detectExpressionScanFlows(calls, sources, sinks, sanitizers, unreachabl
|
|
|
23023
23431
|
if (aliasSanitizedFor.get(source.variable)?.has(sink.type)) {
|
|
23024
23432
|
break;
|
|
23025
23433
|
}
|
|
23434
|
+
if (typeof code === "string" && isReassignedToLiteralBetween(code, source.variable, source.line, sink.line)) {
|
|
23435
|
+
break;
|
|
23436
|
+
}
|
|
23026
23437
|
flows.push({
|
|
23027
23438
|
source_line: source.line,
|
|
23028
23439
|
sink_line: sink.line,
|
|
@@ -23053,6 +23464,9 @@ function detectExpressionScanFlows(calls, sources, sinks, sanitizers, unreachabl
|
|
|
23053
23464
|
if (!colocSources || colocSources.length === 0) continue;
|
|
23054
23465
|
for (const source of colocSources) {
|
|
23055
23466
|
if (!canSourceReachSink(source.type, sink.type)) continue;
|
|
23467
|
+
if (source.type === "file_input" && sink.type === "path_traversal" && sink.method && source.location.includes(`${sink.method}(`)) {
|
|
23468
|
+
continue;
|
|
23469
|
+
}
|
|
23056
23470
|
if (flows.some(
|
|
23057
23471
|
(f) => f.source_line === source.line && f.sink_line === sink.line && f.sink_type === sink.type
|
|
23058
23472
|
)) continue;
|
|
@@ -26530,6 +26944,28 @@ var JS_ROUTE_METHODS = /* @__PURE__ */ new Set([
|
|
|
26530
26944
|
"head",
|
|
26531
26945
|
"options"
|
|
26532
26946
|
]);
|
|
26947
|
+
var SECURITY_MIDDLEWARE_METHODS = /* @__PURE__ */ new Set([
|
|
26948
|
+
// Node helmet (and sub-modules)
|
|
26949
|
+
"helmet",
|
|
26950
|
+
"frameguard",
|
|
26951
|
+
"contentSecurityPolicy",
|
|
26952
|
+
"hsts",
|
|
26953
|
+
"noSniff",
|
|
26954
|
+
"xssFilter",
|
|
26955
|
+
"referrerPolicy",
|
|
26956
|
+
"permittedCrossDomainPolicies",
|
|
26957
|
+
"dnsPrefetchControl",
|
|
26958
|
+
// Spring HttpSecurity builder chain
|
|
26959
|
+
"frameOptions",
|
|
26960
|
+
"headers",
|
|
26961
|
+
"httpStrictTransportSecurity",
|
|
26962
|
+
"contentTypeOptions",
|
|
26963
|
+
"xssProtection",
|
|
26964
|
+
// Flask / Python
|
|
26965
|
+
"Talisman",
|
|
26966
|
+
"Secure"
|
|
26967
|
+
]);
|
|
26968
|
+
var SECURITY_MIDDLEWARE_ANNOTATIONS_RE = /\b(EnableWebSecurity|SecurityFilterChain|after_request|before_request)\b/;
|
|
26533
26969
|
var SecurityHeadersPass = class {
|
|
26534
26970
|
name = "security-headers";
|
|
26535
26971
|
category = "security";
|
|
@@ -26556,12 +26992,14 @@ var SecurityHeadersPass = class {
|
|
|
26556
26992
|
list.push(call);
|
|
26557
26993
|
}
|
|
26558
26994
|
const hasHandler = detectHandler(graph, calls);
|
|
26995
|
+
const hasGlobalMiddleware = detectGlobalSecurityMiddleware(graph, calls);
|
|
26559
26996
|
for (const rule of this.rules) {
|
|
26560
26997
|
const headerKey = rule.header.toLowerCase();
|
|
26561
26998
|
const writes = writtenHeaders.get(headerKey) ?? [];
|
|
26562
26999
|
if (rule.kind === "missing") {
|
|
26563
27000
|
if (writes.length > 0) continue;
|
|
26564
27001
|
if (rule.requiresHandler !== false && !hasHandler) continue;
|
|
27002
|
+
if (hasGlobalMiddleware) continue;
|
|
26565
27003
|
ctx.addFinding({
|
|
26566
27004
|
id: `${rule.rule_id}-${file}`,
|
|
26567
27005
|
pass: this.name,
|
|
@@ -26715,6 +27153,23 @@ function detectHandler(graph, calls) {
|
|
|
26715
27153
|
}
|
|
26716
27154
|
return false;
|
|
26717
27155
|
}
|
|
27156
|
+
function detectGlobalSecurityMiddleware(graph, calls) {
|
|
27157
|
+
for (const call of calls) {
|
|
27158
|
+
if (SECURITY_MIDDLEWARE_METHODS.has(call.method_name)) return true;
|
|
27159
|
+
if (call.method_name === "use" && call.arguments.length > 0) {
|
|
27160
|
+
const firstArg = call.arguments[0].expression ?? "";
|
|
27161
|
+
if (/\b(helmet|Talisman|secure)\b/.test(firstArg)) return true;
|
|
27162
|
+
}
|
|
27163
|
+
}
|
|
27164
|
+
for (const type of graph.ir.types) {
|
|
27165
|
+
if (type.annotations.some((a) => SECURITY_MIDDLEWARE_ANNOTATIONS_RE.test(a))) return true;
|
|
27166
|
+
for (const method of type.methods) {
|
|
27167
|
+
if (method.annotations.some((a) => SECURITY_MIDDLEWARE_ANNOTATIONS_RE.test(a))) return true;
|
|
27168
|
+
if (/^security[A-Za-z]*FilterChain$/i.test(method.name)) return true;
|
|
27169
|
+
}
|
|
27170
|
+
}
|
|
27171
|
+
return false;
|
|
27172
|
+
}
|
|
26718
27173
|
|
|
26719
27174
|
// src/analysis/passes/scan-secrets-pass.ts
|
|
26720
27175
|
var TEST_PATH_RE3 = /(?:^|[\\/])(?:test|tests|spec|specs|__tests?__|__mocks?__|fixtures?|testdata)(?:[\\/]|$)/i;
|