circle-ir 3.57.0 → 3.59.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 +226 -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 +610 -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.
|
|
@@ -21698,6 +22045,7 @@ var LanguageSourcesPass = class {
|
|
|
21698
22045
|
const additionalSources = [];
|
|
21699
22046
|
const additionalSinks = [];
|
|
21700
22047
|
additionalSources.push(...findGetterSources(types, constProp.instanceFieldTaint, code));
|
|
22048
|
+
additionalSources.push(...findOopFieldReadSources(types, code, language));
|
|
21701
22049
|
additionalSources.push(...findJavaScriptAssignmentSources(code, language));
|
|
21702
22050
|
const jsDOMSinks = findJavaScriptDOMSinks(code, language);
|
|
21703
22051
|
for (const s of jsDOMSinks) {
|
|
@@ -21802,6 +22150,115 @@ function findGetterSources(types, instanceFieldTaint, _sourceCode) {
|
|
|
21802
22150
|
}
|
|
21803
22151
|
return sources;
|
|
21804
22152
|
}
|
|
22153
|
+
function findOopFieldReadSources(types, sourceCode, language) {
|
|
22154
|
+
if (language !== "java" && language !== "python") return [];
|
|
22155
|
+
const sources = [];
|
|
22156
|
+
const lines = sourceCode.split("\n");
|
|
22157
|
+
const isPython = language === "python";
|
|
22158
|
+
const SELF = isPython ? "self" : "this";
|
|
22159
|
+
const javaHttpPattern = /\b(?:req|request|httpRequest|servletRequest|httpServletRequest)\.(?:getParameter|getParameterValues|getParameterMap|getHeader|getHeaders|getCookies|getQueryString|getPathInfo|getRequestURI|getRequestURL|getInputStream|getReader)\b/;
|
|
22160
|
+
const fieldAssignRe = new RegExp(`^\\s*${SELF}\\.([A-Za-z_]\\w*)\\s*=\\s*(.+?)(?:;\\s*)?$`);
|
|
22161
|
+
const commentPrefix = isPython ? "#" : "//";
|
|
22162
|
+
for (const type of types) {
|
|
22163
|
+
if (type.kind !== "class") continue;
|
|
22164
|
+
if (type.name === "<module>") continue;
|
|
22165
|
+
let ctor;
|
|
22166
|
+
for (const m of type.methods) {
|
|
22167
|
+
if (isPython) {
|
|
22168
|
+
if (m.name === "__init__") {
|
|
22169
|
+
ctor = m;
|
|
22170
|
+
break;
|
|
22171
|
+
}
|
|
22172
|
+
} else {
|
|
22173
|
+
if (m.name === type.name) {
|
|
22174
|
+
ctor = m;
|
|
22175
|
+
break;
|
|
22176
|
+
}
|
|
22177
|
+
}
|
|
22178
|
+
}
|
|
22179
|
+
if (!ctor) continue;
|
|
22180
|
+
const paramNames = /* @__PURE__ */ new Set();
|
|
22181
|
+
for (const p of ctor.parameters) {
|
|
22182
|
+
if (p.name === "self" || p.name === "this") continue;
|
|
22183
|
+
paramNames.add(p.name);
|
|
22184
|
+
}
|
|
22185
|
+
const fieldTaint = /* @__PURE__ */ new Map();
|
|
22186
|
+
const ctorStart = ctor.start_line;
|
|
22187
|
+
const ctorEnd = ctor.end_line;
|
|
22188
|
+
for (let i2 = ctorStart - 1; i2 < Math.min(ctorEnd, lines.length); i2++) {
|
|
22189
|
+
const line = lines[i2] ?? "";
|
|
22190
|
+
if (line.trim().startsWith(commentPrefix)) continue;
|
|
22191
|
+
const m = line.match(fieldAssignRe);
|
|
22192
|
+
if (!m) continue;
|
|
22193
|
+
const fieldName = m[1];
|
|
22194
|
+
const rhs = m[2].trim().replace(/;\s*$/, "");
|
|
22195
|
+
let sourceType = null;
|
|
22196
|
+
if (paramNames.has(rhs)) {
|
|
22197
|
+
sourceType = "interprocedural_param";
|
|
22198
|
+
} else if (!isPython && javaHttpPattern.test(rhs)) {
|
|
22199
|
+
sourceType = "http_param";
|
|
22200
|
+
} else if (isPython) {
|
|
22201
|
+
for (const { pattern, type: type2 } of PYTHON_TAINTED_PATTERNS2) {
|
|
22202
|
+
if (pattern.test(rhs)) {
|
|
22203
|
+
sourceType = type2;
|
|
22204
|
+
break;
|
|
22205
|
+
}
|
|
22206
|
+
}
|
|
22207
|
+
}
|
|
22208
|
+
if (sourceType) {
|
|
22209
|
+
fieldTaint.set(fieldName, { line: i2 + 1, type: sourceType });
|
|
22210
|
+
}
|
|
22211
|
+
}
|
|
22212
|
+
if (fieldTaint.size === 0) continue;
|
|
22213
|
+
for (const [fieldName, info2] of fieldTaint) {
|
|
22214
|
+
sources.push({
|
|
22215
|
+
type: info2.type,
|
|
22216
|
+
location: `${type.name}.${SELF}.${fieldName} (constructor-injected field, #78)`,
|
|
22217
|
+
severity: "high",
|
|
22218
|
+
line: info2.line,
|
|
22219
|
+
confidence: 0.85,
|
|
22220
|
+
variable: `${SELF}.${fieldName}`
|
|
22221
|
+
});
|
|
22222
|
+
}
|
|
22223
|
+
for (const m of type.methods) {
|
|
22224
|
+
if (m === ctor) continue;
|
|
22225
|
+
const nonSelfParams = m.parameters.filter((p) => p.name !== "self" && p.name !== "this");
|
|
22226
|
+
if (nonSelfParams.length !== 0) continue;
|
|
22227
|
+
const mStart = m.start_line;
|
|
22228
|
+
const mEnd = m.end_line;
|
|
22229
|
+
let returnedField = null;
|
|
22230
|
+
let returnStatementCount = 0;
|
|
22231
|
+
const returnRe = new RegExp(`\\breturn\\s+${SELF}\\.([A-Za-z_]\\w*)\\s*[;}]?`);
|
|
22232
|
+
for (let i2 = mStart - 1; i2 < Math.min(mEnd, lines.length); i2++) {
|
|
22233
|
+
const raw = lines[i2] ?? "";
|
|
22234
|
+
const trimmed = raw.trim();
|
|
22235
|
+
if (!trimmed) continue;
|
|
22236
|
+
if (trimmed.startsWith(commentPrefix)) continue;
|
|
22237
|
+
const rm = trimmed.match(returnRe);
|
|
22238
|
+
if (rm) {
|
|
22239
|
+
returnedField = rm[1];
|
|
22240
|
+
returnStatementCount++;
|
|
22241
|
+
} else if (/\breturn\b/.test(trimmed)) {
|
|
22242
|
+
returnStatementCount = 99;
|
|
22243
|
+
break;
|
|
22244
|
+
}
|
|
22245
|
+
}
|
|
22246
|
+
if (returnStatementCount === 1 && returnedField && fieldTaint.has(returnedField)) {
|
|
22247
|
+
const fieldInfo = fieldTaint.get(returnedField);
|
|
22248
|
+
const getterVar = isPython ? `${SELF}.${m.name}` : m.name;
|
|
22249
|
+
sources.push({
|
|
22250
|
+
type: fieldInfo.type,
|
|
22251
|
+
location: `${type.name}.${m.name} returns tainted field '${returnedField}' (#78)`,
|
|
22252
|
+
severity: "high",
|
|
22253
|
+
line: m.start_line,
|
|
22254
|
+
confidence: 0.85,
|
|
22255
|
+
variable: getterVar
|
|
22256
|
+
});
|
|
22257
|
+
}
|
|
22258
|
+
}
|
|
22259
|
+
}
|
|
22260
|
+
return sources;
|
|
22261
|
+
}
|
|
21805
22262
|
function findJavaScriptAssignmentSources(sourceCode, language) {
|
|
21806
22263
|
if (!["javascript", "typescript"].includes(language)) return [];
|
|
21807
22264
|
const sources = [];
|
|
@@ -22110,25 +22567,42 @@ function findBashTaintSources(sourceCode, dfg) {
|
|
|
22110
22567
|
const sources = [];
|
|
22111
22568
|
const lines = sourceCode.split("\n");
|
|
22112
22569
|
const definedVars = new Set(dfg.defs.filter((d) => d.kind === "local").map((d) => d.variable));
|
|
22570
|
+
const fnHeaderRe = /^\s*(?:function\s+)?[A-Za-z_][\w-]*\s*\(\s*\)\s*\{?\s*$|^\s*function\s+[A-Za-z_][\w-]*\s*\{?\s*$/;
|
|
22571
|
+
let braceDepth = 0;
|
|
22113
22572
|
for (let i2 = 0; i2 < lines.length; i2++) {
|
|
22114
22573
|
const line = lines[i2];
|
|
22115
22574
|
const trimmed = line.trim();
|
|
22116
22575
|
const lineNumber = i2 + 1;
|
|
22117
22576
|
if (trimmed.startsWith("#")) continue;
|
|
22118
|
-
const
|
|
22119
|
-
|
|
22120
|
-
|
|
22121
|
-
|
|
22122
|
-
|
|
22123
|
-
|
|
22124
|
-
sources.
|
|
22125
|
-
|
|
22126
|
-
|
|
22127
|
-
|
|
22128
|
-
|
|
22129
|
-
|
|
22130
|
-
|
|
22131
|
-
|
|
22577
|
+
const insideFunction = braceDepth > 0;
|
|
22578
|
+
if (!insideFunction) {
|
|
22579
|
+
const positionalRe = /\$([1-9@*])|\$\{([1-9@*])\}/g;
|
|
22580
|
+
let m;
|
|
22581
|
+
while ((m = positionalRe.exec(line)) !== null) {
|
|
22582
|
+
const param = m[1] ?? m[2];
|
|
22583
|
+
const alreadyExists = sources.some((s) => s.line === lineNumber && s.variable === param);
|
|
22584
|
+
if (!alreadyExists) {
|
|
22585
|
+
sources.push({
|
|
22586
|
+
type: "io_input",
|
|
22587
|
+
location: `positional parameter $${param}`,
|
|
22588
|
+
severity: "high",
|
|
22589
|
+
line: lineNumber,
|
|
22590
|
+
confidence: 1,
|
|
22591
|
+
variable: param
|
|
22592
|
+
});
|
|
22593
|
+
}
|
|
22594
|
+
}
|
|
22595
|
+
}
|
|
22596
|
+
if (fnHeaderRe.test(line) || /^\s*[A-Za-z_][\w-]*\s*\(\s*\)\s*\{/.test(line)) {
|
|
22597
|
+
const openBracesOnLine = (line.match(/\{/g) ?? []).length;
|
|
22598
|
+
const closeBracesOnLine = (line.match(/\}/g) ?? []).length;
|
|
22599
|
+
braceDepth += openBracesOnLine - closeBracesOnLine;
|
|
22600
|
+
} else {
|
|
22601
|
+
if (braceDepth > 0) {
|
|
22602
|
+
const openBracesOnLine = (line.match(/\{/g) ?? []).length;
|
|
22603
|
+
const closeBracesOnLine = (line.match(/\}/g) ?? []).length;
|
|
22604
|
+
braceDepth += openBracesOnLine - closeBracesOnLine;
|
|
22605
|
+
if (braceDepth < 0) braceDepth = 0;
|
|
22132
22606
|
}
|
|
22133
22607
|
}
|
|
22134
22608
|
const cmdSubAssign = trimmed.match(/^(\w+)=\$\((\w+)\s/);
|
|
@@ -22550,10 +23024,14 @@ function filterCleanVariableSinks(sinks, calls, taintedVars, symbols, dfg, sanit
|
|
|
22550
23024
|
const callsAtSink = callsByLine.get(sink.line) ?? [];
|
|
22551
23025
|
const isInSynchronizedBlock = synchronizedLines?.has(sink.line) ?? false;
|
|
22552
23026
|
const relevantCalls = sink.method ? callsAtSink.filter((c) => c.method_name === sink.method) : callsAtSink;
|
|
23027
|
+
const trustArgPositions = language !== "bash" && language !== "shell";
|
|
22553
23028
|
for (const call of relevantCalls) {
|
|
22554
23029
|
let allArgsAreClean = true;
|
|
23030
|
+
let dangerousArgCount = 0;
|
|
22555
23031
|
const methodName = call.in_method;
|
|
22556
23032
|
for (const arg of call.arguments) {
|
|
23033
|
+
if (trustArgPositions && sink.argPositions && sink.argPositions.length > 0 && !sink.argPositions.includes(arg.position)) continue;
|
|
23034
|
+
dangerousArgCount++;
|
|
22557
23035
|
if (language === "bash" && arg.expression === call.method_name && !arg.variable && arg.literal == null) continue;
|
|
22558
23036
|
if (arg.variable && !arg.expression?.includes("[")) {
|
|
22559
23037
|
const varName = arg.variable;
|
|
@@ -22576,7 +23054,7 @@ function filterCleanVariableSinks(sinks, calls, taintedVars, symbols, dfg, sanit
|
|
|
22576
23054
|
allArgsAreClean = false;
|
|
22577
23055
|
}
|
|
22578
23056
|
}
|
|
22579
|
-
if (allArgsAreClean &&
|
|
23057
|
+
if (allArgsAreClean && dangerousArgCount > 0) return false;
|
|
22580
23058
|
}
|
|
22581
23059
|
return true;
|
|
22582
23060
|
});
|
|
@@ -22668,7 +23146,7 @@ var TaintPropagationPass = class {
|
|
|
22668
23146
|
flows.push(f);
|
|
22669
23147
|
}
|
|
22670
23148
|
}
|
|
22671
|
-
const collectionFlows = detectCollectionFlows(calls, sources, sinks, constProp.tainted, constProp.unreachableLines) ?? [];
|
|
23149
|
+
const collectionFlows = detectCollectionFlows(calls, sources, sinks, constProp.tainted, constProp.unreachableLines, ctx.code) ?? [];
|
|
22672
23150
|
for (const f of collectionFlows) {
|
|
22673
23151
|
if (flows.some((x) => x.source_line === f.source_line && x.sink_line === f.sink_line)) continue;
|
|
22674
23152
|
const flowForCheck = {
|
|
@@ -22687,13 +23165,13 @@ var TaintPropagationPass = class {
|
|
|
22687
23165
|
if (isFP) continue;
|
|
22688
23166
|
flows.push(f);
|
|
22689
23167
|
}
|
|
22690
|
-
const paramFlows = detectParameterSinkFlows(types, calls, sources, sinks, constProp.unreachableLines) ?? [];
|
|
23168
|
+
const paramFlows = detectParameterSinkFlows(types, calls, sources, sinks, constProp.unreachableLines, constProp.tainted, ctx.code) ?? [];
|
|
22691
23169
|
for (const f of paramFlows) {
|
|
22692
23170
|
if (!flows.some((x) => x.source_line === f.source_line && x.sink_line === f.sink_line)) {
|
|
22693
23171
|
flows.push(f);
|
|
22694
23172
|
}
|
|
22695
23173
|
}
|
|
22696
|
-
const exprScanFlows = detectExpressionScanFlows(calls, sources, sinks, sanitizers, constProp.unreachableLines, ctx.code, ctx.language) ?? [];
|
|
23174
|
+
const exprScanFlows = detectExpressionScanFlows(calls, sources, sinks, sanitizers, constProp.unreachableLines, constProp.tainted, ctx.code, ctx.language) ?? [];
|
|
22697
23175
|
for (const f of exprScanFlows) {
|
|
22698
23176
|
if (flows.some(
|
|
22699
23177
|
(x) => x.source_line === f.source_line && x.sink_line === f.sink_line && x.sink_type === f.sink_type
|
|
@@ -22714,10 +23192,21 @@ var TaintPropagationPass = class {
|
|
|
22714
23192
|
if (isFP) continue;
|
|
22715
23193
|
flows.push(f);
|
|
22716
23194
|
}
|
|
22717
|
-
|
|
23195
|
+
const sanitizedNames = constProp.sanitizedVars;
|
|
23196
|
+
const finalFlows = sanitizedNames.size === 0 ? flows : flows.filter((f) => {
|
|
23197
|
+
if (f.path.length === 0) return true;
|
|
23198
|
+
const sourceVar = f.path[0].variable;
|
|
23199
|
+
if (!sourceVar) return true;
|
|
23200
|
+
if (sanitizedNames.has(sourceVar)) return false;
|
|
23201
|
+
for (const s of sanitizedNames) {
|
|
23202
|
+
if (s.endsWith(`:${sourceVar}`)) return false;
|
|
23203
|
+
}
|
|
23204
|
+
return true;
|
|
23205
|
+
});
|
|
23206
|
+
return { flows: finalFlows };
|
|
22718
23207
|
}
|
|
22719
23208
|
};
|
|
22720
|
-
function detectCollectionFlows(calls, sources, sinks, taintedVars, unreachableLines) {
|
|
23209
|
+
function detectCollectionFlows(calls, sources, sinks, taintedVars, unreachableLines, code) {
|
|
22721
23210
|
const flows = [];
|
|
22722
23211
|
const callsByLine = /* @__PURE__ */ new Map();
|
|
22723
23212
|
for (const call of calls) {
|
|
@@ -22739,6 +23228,9 @@ function detectCollectionFlows(calls, sources, sinks, taintedVars, unreachableLi
|
|
|
22739
23228
|
if (taintedVars.has(varName) || taintedVars.has(scopedName)) {
|
|
22740
23229
|
const source = sources[0];
|
|
22741
23230
|
if (source) {
|
|
23231
|
+
if (typeof code === "string" && isReassignedToLiteralBetween(code, varName, source.line, sink.line)) {
|
|
23232
|
+
continue;
|
|
23233
|
+
}
|
|
22742
23234
|
flows.push({
|
|
22743
23235
|
source_line: source.line,
|
|
22744
23236
|
sink_line: sink.line,
|
|
@@ -22773,6 +23265,9 @@ function detectCollectionFlows(calls, sources, sinks, taintedVars, unreachableLi
|
|
|
22773
23265
|
if (taintedVars.has(collectionVar) || taintedVars.has(scopedCollection)) {
|
|
22774
23266
|
const source = sources[0];
|
|
22775
23267
|
if (source) {
|
|
23268
|
+
if (typeof code === "string" && isReassignedToLiteralBetween(code, collectionVar, source.line, sink.line)) {
|
|
23269
|
+
continue;
|
|
23270
|
+
}
|
|
22776
23271
|
flows.push({
|
|
22777
23272
|
source_line: source.line,
|
|
22778
23273
|
sink_line: sink.line,
|
|
@@ -22842,7 +23337,7 @@ function detectArrayElementFlows(calls, sources, sinks, taintedArrayElements, un
|
|
|
22842
23337
|
}
|
|
22843
23338
|
return flows;
|
|
22844
23339
|
}
|
|
22845
|
-
function detectParameterSinkFlows(types, calls, sources, sinks, unreachableLines) {
|
|
23340
|
+
function detectParameterSinkFlows(types, calls, sources, sinks, unreachableLines, tainted, code) {
|
|
22846
23341
|
const flows = [];
|
|
22847
23342
|
const paramSourcesByMethod = /* @__PURE__ */ new Map();
|
|
22848
23343
|
for (const source of sources) {
|
|
@@ -22884,6 +23379,9 @@ function detectParameterSinkFlows(types, calls, sources, sinks, unreachableLines
|
|
|
22884
23379
|
if (paramSource) {
|
|
22885
23380
|
const exists = flows.some((f) => f.source_line === paramSource.line && f.sink_line === sink.line);
|
|
22886
23381
|
if (!exists) {
|
|
23382
|
+
if (typeof code === "string" && isReassignedToLiteralBetween(code, arg.variable, paramSource.line, sink.line)) {
|
|
23383
|
+
continue;
|
|
23384
|
+
}
|
|
22887
23385
|
flows.push({
|
|
22888
23386
|
source_line: paramSource.line,
|
|
22889
23387
|
sink_line: sink.line,
|
|
@@ -22905,7 +23403,27 @@ function detectParameterSinkFlows(types, calls, sources, sinks, unreachableLines
|
|
|
22905
23403
|
void types;
|
|
22906
23404
|
return flows;
|
|
22907
23405
|
}
|
|
22908
|
-
function
|
|
23406
|
+
function isReassignedToLiteralBetween(code, variable, srcLine, sinkLine) {
|
|
23407
|
+
if (!variable || sinkLine - srcLine < 2) return false;
|
|
23408
|
+
if (!/^[A-Za-z_][\w]*$/.test(variable)) return false;
|
|
23409
|
+
const lines = code.split("\n");
|
|
23410
|
+
const lo = Math.max(0, srcLine);
|
|
23411
|
+
const hi = Math.min(lines.length, sinkLine - 1);
|
|
23412
|
+
const strLit = `(?:"[^"\\\\]*(?:\\\\.[^"\\\\]*)*"|'[^'\\\\]*(?:\\\\.[^'\\\\]*)*'|\`[^\`\\\\]*(?:\\\\.[^\`\\\\]*)*\`)`;
|
|
23413
|
+
const reNaked = new RegExp(
|
|
23414
|
+
`^\\s*${variable}\\s*(?::?=)\\s*${strLit}\\s*;?\\s*$`
|
|
23415
|
+
);
|
|
23416
|
+
const reGuarded = new RegExp(
|
|
23417
|
+
`^\\s*if\\b.*\\b${variable}\\s*=\\s*${strLit}\\s*;?\\s*$`
|
|
23418
|
+
);
|
|
23419
|
+
for (let i2 = lo; i2 < hi; i2++) {
|
|
23420
|
+
const line = lines[i2];
|
|
23421
|
+
if (!line) continue;
|
|
23422
|
+
if (reNaked.test(line) || reGuarded.test(line)) return true;
|
|
23423
|
+
}
|
|
23424
|
+
return false;
|
|
23425
|
+
}
|
|
23426
|
+
function detectExpressionScanFlows(calls, sources, sinks, sanitizers, unreachableLines, tainted, code, language) {
|
|
22909
23427
|
const flows = [];
|
|
22910
23428
|
const sourcesWithVar = sources.filter(
|
|
22911
23429
|
(s) => typeof s.variable === "string" && s.variable.length > 0
|
|
@@ -22957,9 +23475,9 @@ function detectExpressionScanFlows(calls, sources, sinks, sanitizers, unreachabl
|
|
|
22957
23475
|
if (!rhsMatch) continue;
|
|
22958
23476
|
const rhs = rhsMatch[1];
|
|
22959
23477
|
for (const san of lineSans) {
|
|
22960
|
-
const sanMatch = san.method.match(
|
|
23478
|
+
const sanMatch = san.method.match(/(\w+)\(\)$/);
|
|
22961
23479
|
if (!sanMatch) continue;
|
|
22962
|
-
const sanName = sanMatch[1]
|
|
23480
|
+
const sanName = sanMatch[1];
|
|
22963
23481
|
if (!rhs.includes(`${sanName}(`)) continue;
|
|
22964
23482
|
let set = aliasSanitizedFor.get(varName);
|
|
22965
23483
|
if (!set) {
|
|
@@ -23023,6 +23541,9 @@ function detectExpressionScanFlows(calls, sources, sinks, sanitizers, unreachabl
|
|
|
23023
23541
|
if (aliasSanitizedFor.get(source.variable)?.has(sink.type)) {
|
|
23024
23542
|
break;
|
|
23025
23543
|
}
|
|
23544
|
+
if (typeof code === "string" && isReassignedToLiteralBetween(code, source.variable, source.line, sink.line)) {
|
|
23545
|
+
break;
|
|
23546
|
+
}
|
|
23026
23547
|
flows.push({
|
|
23027
23548
|
source_line: source.line,
|
|
23028
23549
|
sink_line: sink.line,
|
|
@@ -23053,6 +23574,9 @@ function detectExpressionScanFlows(calls, sources, sinks, sanitizers, unreachabl
|
|
|
23053
23574
|
if (!colocSources || colocSources.length === 0) continue;
|
|
23054
23575
|
for (const source of colocSources) {
|
|
23055
23576
|
if (!canSourceReachSink(source.type, sink.type)) continue;
|
|
23577
|
+
if (source.type === "file_input" && sink.type === "path_traversal" && sink.method && source.location.includes(`${sink.method}(`)) {
|
|
23578
|
+
continue;
|
|
23579
|
+
}
|
|
23056
23580
|
if (flows.some(
|
|
23057
23581
|
(f) => f.source_line === source.line && f.sink_line === sink.line && f.sink_type === sink.type
|
|
23058
23582
|
)) continue;
|
|
@@ -26530,6 +27054,28 @@ var JS_ROUTE_METHODS = /* @__PURE__ */ new Set([
|
|
|
26530
27054
|
"head",
|
|
26531
27055
|
"options"
|
|
26532
27056
|
]);
|
|
27057
|
+
var SECURITY_MIDDLEWARE_METHODS = /* @__PURE__ */ new Set([
|
|
27058
|
+
// Node helmet (and sub-modules)
|
|
27059
|
+
"helmet",
|
|
27060
|
+
"frameguard",
|
|
27061
|
+
"contentSecurityPolicy",
|
|
27062
|
+
"hsts",
|
|
27063
|
+
"noSniff",
|
|
27064
|
+
"xssFilter",
|
|
27065
|
+
"referrerPolicy",
|
|
27066
|
+
"permittedCrossDomainPolicies",
|
|
27067
|
+
"dnsPrefetchControl",
|
|
27068
|
+
// Spring HttpSecurity builder chain
|
|
27069
|
+
"frameOptions",
|
|
27070
|
+
"headers",
|
|
27071
|
+
"httpStrictTransportSecurity",
|
|
27072
|
+
"contentTypeOptions",
|
|
27073
|
+
"xssProtection",
|
|
27074
|
+
// Flask / Python
|
|
27075
|
+
"Talisman",
|
|
27076
|
+
"Secure"
|
|
27077
|
+
]);
|
|
27078
|
+
var SECURITY_MIDDLEWARE_ANNOTATIONS_RE = /\b(EnableWebSecurity|SecurityFilterChain|after_request|before_request)\b/;
|
|
26533
27079
|
var SecurityHeadersPass = class {
|
|
26534
27080
|
name = "security-headers";
|
|
26535
27081
|
category = "security";
|
|
@@ -26556,12 +27102,14 @@ var SecurityHeadersPass = class {
|
|
|
26556
27102
|
list.push(call);
|
|
26557
27103
|
}
|
|
26558
27104
|
const hasHandler = detectHandler(graph, calls);
|
|
27105
|
+
const hasGlobalMiddleware = detectGlobalSecurityMiddleware(graph, calls);
|
|
26559
27106
|
for (const rule of this.rules) {
|
|
26560
27107
|
const headerKey = rule.header.toLowerCase();
|
|
26561
27108
|
const writes = writtenHeaders.get(headerKey) ?? [];
|
|
26562
27109
|
if (rule.kind === "missing") {
|
|
26563
27110
|
if (writes.length > 0) continue;
|
|
26564
27111
|
if (rule.requiresHandler !== false && !hasHandler) continue;
|
|
27112
|
+
if (hasGlobalMiddleware) continue;
|
|
26565
27113
|
ctx.addFinding({
|
|
26566
27114
|
id: `${rule.rule_id}-${file}`,
|
|
26567
27115
|
pass: this.name,
|
|
@@ -26715,6 +27263,23 @@ function detectHandler(graph, calls) {
|
|
|
26715
27263
|
}
|
|
26716
27264
|
return false;
|
|
26717
27265
|
}
|
|
27266
|
+
function detectGlobalSecurityMiddleware(graph, calls) {
|
|
27267
|
+
for (const call of calls) {
|
|
27268
|
+
if (SECURITY_MIDDLEWARE_METHODS.has(call.method_name)) return true;
|
|
27269
|
+
if (call.method_name === "use" && call.arguments.length > 0) {
|
|
27270
|
+
const firstArg = call.arguments[0].expression ?? "";
|
|
27271
|
+
if (/\b(helmet|Talisman|secure)\b/.test(firstArg)) return true;
|
|
27272
|
+
}
|
|
27273
|
+
}
|
|
27274
|
+
for (const type of graph.ir.types) {
|
|
27275
|
+
if (type.annotations.some((a) => SECURITY_MIDDLEWARE_ANNOTATIONS_RE.test(a))) return true;
|
|
27276
|
+
for (const method of type.methods) {
|
|
27277
|
+
if (method.annotations.some((a) => SECURITY_MIDDLEWARE_ANNOTATIONS_RE.test(a))) return true;
|
|
27278
|
+
if (/^security[A-Za-z]*FilterChain$/i.test(method.name)) return true;
|
|
27279
|
+
}
|
|
27280
|
+
}
|
|
27281
|
+
return false;
|
|
27282
|
+
}
|
|
26718
27283
|
|
|
26719
27284
|
// src/analysis/passes/scan-secrets-pass.ts
|
|
26720
27285
|
var TEST_PATH_RE3 = /(?:^|[\\/])(?:test|tests|spec|specs|__tests?__|__mocks?__|fixtures?|testdata)(?:[\\/]|$)/i;
|