circle-ir 4.9.12 → 4.9.13
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/analysis/passes/blocking-main-thread-pass.d.ts +19 -0
- package/dist/analysis/passes/blocking-main-thread-pass.d.ts.map +1 -1
- package/dist/analysis/passes/blocking-main-thread-pass.js +75 -2
- package/dist/analysis/passes/blocking-main-thread-pass.js.map +1 -1
- package/dist/analysis/passes/language-sources-pass.d.ts.map +1 -1
- package/dist/analysis/passes/language-sources-pass.js +219 -18
- package/dist/analysis/passes/language-sources-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 +94 -0
- package/dist/analysis/passes/sink-filter-pass.js.map +1 -1
- package/dist/analysis/taint-propagation.d.ts.map +1 -1
- package/dist/analysis/taint-propagation.js +17 -2
- package/dist/analysis/taint-propagation.js.map +1 -1
- package/dist/browser/circle-ir.js +208 -17
- package/dist/core/circle-ir-core.cjs +27 -1
- package/dist/core/circle-ir-core.js +27 -1
- package/dist/core/extractors/dfg.js +78 -1
- package/dist/core/extractors/dfg.js.map +1 -1
- package/package.json +1 -1
|
@@ -9363,6 +9363,7 @@ function buildCSharpDFG(tree, cache) {
|
|
|
9363
9363
|
];
|
|
9364
9364
|
for (const method of methods) {
|
|
9365
9365
|
scopeStack.push(/* @__PURE__ */ new Map());
|
|
9366
|
+
const methodDefStart = defs.length;
|
|
9366
9367
|
const params = method.childForFieldName("parameters");
|
|
9367
9368
|
if (params) {
|
|
9368
9369
|
for (const def of extractParameterDefs(params, defIdCounter)) {
|
|
@@ -9392,6 +9393,7 @@ function buildCSharpDFG(tree, cache) {
|
|
|
9392
9393
|
}
|
|
9393
9394
|
}
|
|
9394
9395
|
const bodyUses = extractUses(body2, useIdCounter, scopeStack, false);
|
|
9396
|
+
resolveReassignedUses(defs.slice(methodDefStart), bodyUses.uses);
|
|
9395
9397
|
uses.push(...bodyUses.uses);
|
|
9396
9398
|
useIdCounter = bodyUses.nextId;
|
|
9397
9399
|
}
|
|
@@ -10101,6 +10103,30 @@ function findReachingDef(name2, scopeStack) {
|
|
|
10101
10103
|
}
|
|
10102
10104
|
return null;
|
|
10103
10105
|
}
|
|
10106
|
+
function resolveReassignedUses(methodDefs, methodUses) {
|
|
10107
|
+
const defsByVar = /* @__PURE__ */ new Map();
|
|
10108
|
+
for (const def of methodDefs) {
|
|
10109
|
+
if (def.kind !== "local" && def.kind !== "param") continue;
|
|
10110
|
+
const existing = defsByVar.get(def.variable);
|
|
10111
|
+
if (existing) existing.push(def);
|
|
10112
|
+
else defsByVar.set(def.variable, [def]);
|
|
10113
|
+
}
|
|
10114
|
+
for (const list of defsByVar.values()) {
|
|
10115
|
+
if (list.length > 1) list.sort((a, b) => a.line - b.line || a.id - b.id);
|
|
10116
|
+
}
|
|
10117
|
+
for (const use of methodUses) {
|
|
10118
|
+
const list = defsByVar.get(use.variable);
|
|
10119
|
+
if (!list || list.length < 2) continue;
|
|
10120
|
+
let strictlyBefore;
|
|
10121
|
+
let sameLine;
|
|
10122
|
+
for (const def of list) {
|
|
10123
|
+
if (def.line < use.line) strictlyBefore = def;
|
|
10124
|
+
else if (def.line === use.line) sameLine = def;
|
|
10125
|
+
}
|
|
10126
|
+
const reaching = strictlyBefore ?? sameLine;
|
|
10127
|
+
if (reaching) use.def_id = reaching.id;
|
|
10128
|
+
}
|
|
10129
|
+
}
|
|
10104
10130
|
function computeChains(defs, uses) {
|
|
10105
10131
|
const chains = [];
|
|
10106
10132
|
const defById = /* @__PURE__ */ new Map();
|
|
@@ -18965,7 +18991,7 @@ function checkSanitized(fromLine, toLine, sinkType, sanitizersByLine, ctx) {
|
|
|
18965
18991
|
function buildTaintFlow(source, sink, taintInfo) {
|
|
18966
18992
|
const path = [];
|
|
18967
18993
|
path.push({
|
|
18968
|
-
variable: taintInfo.variable,
|
|
18994
|
+
variable: source.variable ?? taintInfo.variable,
|
|
18969
18995
|
line: source.line,
|
|
18970
18996
|
type: "source",
|
|
18971
18997
|
description: `Tainted data enters via ${source.type}`
|
|
@@ -27163,6 +27189,7 @@ var LanguageSourcesPass = class {
|
|
|
27163
27189
|
additionalSanitizers.push(...findPythonUrlAllowlistRedirectSanitizers(code));
|
|
27164
27190
|
additionalSanitizers.push(...findPythonDefusedXmlSanitizers(code));
|
|
27165
27191
|
additionalSanitizers.push(...findPythonJinjaAutoescapeSanitizers(code));
|
|
27192
|
+
additionalSanitizers.push(...findCrlfWrapperFunctionSanitizers(code, "python"));
|
|
27166
27193
|
const pyMisconfigFindings = findPythonPatternFindings(code, graph.ir.meta.file);
|
|
27167
27194
|
for (const finding of pyMisconfigFindings) {
|
|
27168
27195
|
ctx.addFinding(finding);
|
|
@@ -27321,6 +27348,11 @@ var LanguageSourcesPass = class {
|
|
|
27321
27348
|
ctx.addFinding(finding);
|
|
27322
27349
|
}
|
|
27323
27350
|
}
|
|
27351
|
+
if (language === "csharp") {
|
|
27352
|
+
additionalSanitizers.push(
|
|
27353
|
+
...findCSharpFullPathStartsWithGuardSanitizers(code)
|
|
27354
|
+
);
|
|
27355
|
+
}
|
|
27324
27356
|
if (language === "java") {
|
|
27325
27357
|
additionalSanitizers.push(...findJavaSafeJsonParseSanitizers(code));
|
|
27326
27358
|
additionalSanitizers.push(
|
|
@@ -27329,6 +27361,7 @@ var LanguageSourcesPass = class {
|
|
|
27329
27361
|
additionalSanitizers.push(...findJavaPathGetFileNameSanitizers(code));
|
|
27330
27362
|
additionalSanitizers.push(...findJavaCanonicalPathStartsWithGuardSanitizers(code));
|
|
27331
27363
|
additionalSanitizers.push(...findJavaInlineCrlfStripLogSanitizers(code));
|
|
27364
|
+
additionalSanitizers.push(...findCrlfWrapperFunctionSanitizers(code, "java"));
|
|
27332
27365
|
additionalSanitizers.push(...findJavaArgvFormExecSanitizers(code));
|
|
27333
27366
|
for (const finding of findJavaPatternFindings(code, graph.ir.meta.file)) {
|
|
27334
27367
|
ctx.addFinding(finding);
|
|
@@ -29896,7 +29929,7 @@ function findJsPathResolveStartsWithGuardSanitizers(code) {
|
|
|
29896
29929
|
let guardLine = -1;
|
|
29897
29930
|
for (let l = c.line; l < Math.min(lines.length, c.line + 6); l++) {
|
|
29898
29931
|
if (!guardRe.test(lines[l])) continue;
|
|
29899
|
-
if (
|
|
29932
|
+
if (guardRejects(lines, l, terminatorRe, 2)) {
|
|
29900
29933
|
guardLine = l + 1;
|
|
29901
29934
|
break;
|
|
29902
29935
|
}
|
|
@@ -30005,7 +30038,7 @@ function findJavaPathNormalizeStartsWithGuardSanitizers(code) {
|
|
|
30005
30038
|
let guardLine = -1;
|
|
30006
30039
|
for (let l = c.line; l < Math.min(lines.length, c.line + 6); l++) {
|
|
30007
30040
|
if (!guardRe.test(lines[l])) continue;
|
|
30008
|
-
if (
|
|
30041
|
+
if (guardRejects(lines, l, terminatorRe, 2)) {
|
|
30009
30042
|
guardLine = l + 1;
|
|
30010
30043
|
break;
|
|
30011
30044
|
}
|
|
@@ -30039,17 +30072,7 @@ function findJavaCanonicalPathStartsWithGuardSanitizers(code) {
|
|
|
30039
30072
|
for (let i2 = 0; i2 < lines.length; i2++) {
|
|
30040
30073
|
const m = guardRe.exec(lines[i2]);
|
|
30041
30074
|
if (!m) continue;
|
|
30042
|
-
|
|
30043
|
-
if (!hasTerminator) {
|
|
30044
|
-
for (let j = i2 + 1; j < Math.min(lines.length, i2 + 6); j++) {
|
|
30045
|
-
if (terminatorRe.test(lines[j])) {
|
|
30046
|
-
hasTerminator = true;
|
|
30047
|
-
break;
|
|
30048
|
-
}
|
|
30049
|
-
if (lines[j].includes("}")) break;
|
|
30050
|
-
}
|
|
30051
|
-
}
|
|
30052
|
-
if (!hasTerminator) continue;
|
|
30075
|
+
if (!guardRejects(lines, i2, terminatorRe, 6)) continue;
|
|
30053
30076
|
const guarded = /* @__PURE__ */ new Set([m[1]]);
|
|
30054
30077
|
const mentions = (t) => {
|
|
30055
30078
|
for (const n of guarded) if (new RegExp(`\\b${n}\\b`).test(t)) return true;
|
|
@@ -30074,6 +30097,58 @@ function findJavaCanonicalPathStartsWithGuardSanitizers(code) {
|
|
|
30074
30097
|
}
|
|
30075
30098
|
return sanitizers;
|
|
30076
30099
|
}
|
|
30100
|
+
function guardRejects(lines, i2, terminatorRe, window2) {
|
|
30101
|
+
if (terminatorRe.test(lines[i2])) return true;
|
|
30102
|
+
const opens = (lines[i2].match(/\{/g) ?? []).length;
|
|
30103
|
+
const closes = (lines[i2].match(/\}/g) ?? []).length;
|
|
30104
|
+
if (opens > 0 && closes >= opens) return false;
|
|
30105
|
+
for (let j = i2 + 1; j < Math.min(lines.length, i2 + window2); j++) {
|
|
30106
|
+
if (terminatorRe.test(lines[j])) return true;
|
|
30107
|
+
if (lines[j].includes("}")) return false;
|
|
30108
|
+
}
|
|
30109
|
+
return false;
|
|
30110
|
+
}
|
|
30111
|
+
function findCSharpFullPathStartsWithGuardSanitizers(code) {
|
|
30112
|
+
const sanitizers = [];
|
|
30113
|
+
const lines = code.split("\n");
|
|
30114
|
+
const assignRe = /^\s*(?:(?:var|string)\s+)?([A-Za-z_]\w*)\s*=(?!=)\s*(.*)$/;
|
|
30115
|
+
const fullPathRe = /\bPath\s*\.\s*GetFullPath\s*\(/;
|
|
30116
|
+
const canonicalVars = /* @__PURE__ */ new Set();
|
|
30117
|
+
for (const line of lines) {
|
|
30118
|
+
const a = assignRe.exec(line);
|
|
30119
|
+
if (a && fullPathRe.test(a[2])) canonicalVars.add(a[1]);
|
|
30120
|
+
}
|
|
30121
|
+
if (canonicalVars.size === 0) return sanitizers;
|
|
30122
|
+
const guardRe = /\bif\s*\(\s*!\s*([A-Za-z_]\w*)\s*\.\s*StartsWith\s*\(/;
|
|
30123
|
+
const terminatorRe = /\b(throw|return)\b/;
|
|
30124
|
+
for (let i2 = 0; i2 < lines.length; i2++) {
|
|
30125
|
+
const m = guardRe.exec(lines[i2]);
|
|
30126
|
+
if (!m || !canonicalVars.has(m[1])) continue;
|
|
30127
|
+
if (!guardRejects(lines, i2, terminatorRe, 6)) continue;
|
|
30128
|
+
const guarded = /* @__PURE__ */ new Set([m[1]]);
|
|
30129
|
+
const mentions = (t) => {
|
|
30130
|
+
for (const n of guarded) if (new RegExp(`\\b${n}\\b`).test(t)) return true;
|
|
30131
|
+
return false;
|
|
30132
|
+
};
|
|
30133
|
+
for (let l = 0; l < lines.length; l++) {
|
|
30134
|
+
const lt = lines[l];
|
|
30135
|
+
const a = assignRe.exec(lt);
|
|
30136
|
+
if (a) {
|
|
30137
|
+
if (mentions(a[2])) guarded.add(a[1]);
|
|
30138
|
+
else if (a[1] !== m[1]) guarded.delete(a[1]);
|
|
30139
|
+
}
|
|
30140
|
+
if (mentions(lt)) {
|
|
30141
|
+
sanitizers.push({
|
|
30142
|
+
type: "csharp_fullpath_startswith_guard",
|
|
30143
|
+
method: "GetFullPath",
|
|
30144
|
+
line: l + 1,
|
|
30145
|
+
sanitizes: ["path_traversal", "external_taint_escape"]
|
|
30146
|
+
});
|
|
30147
|
+
}
|
|
30148
|
+
}
|
|
30149
|
+
}
|
|
30150
|
+
return sanitizers;
|
|
30151
|
+
}
|
|
30077
30152
|
function findJavaPathGetFileNameSanitizers(code) {
|
|
30078
30153
|
const sanitizers = [];
|
|
30079
30154
|
const lines = code.split("\n");
|
|
@@ -30112,6 +30187,34 @@ function findJavaPathGetFileNameSanitizers(code) {
|
|
|
30112
30187
|
}
|
|
30113
30188
|
return sanitizers;
|
|
30114
30189
|
}
|
|
30190
|
+
function findCrlfWrapperFunctionSanitizers(code, language) {
|
|
30191
|
+
const sanitizers = [];
|
|
30192
|
+
const lines = code.split("\n");
|
|
30193
|
+
const declRe = language === "java" ? /^\s*(?:public|private|protected)?\s*(?:static\s+)?(?:final\s+)?(?:String|CharSequence)\s+([A-Za-z_]\w*)\s*\(/ : /^\s*def\s+([A-Za-z_]\w*)\s*\(/;
|
|
30194
|
+
const stripRe = language === "java" ? /\.\s*replace(?:All)?\s*\(\s*(?:"[^"]*\\[rnt]|'\\[rnt]')/ : /(?:re\s*\.\s*sub\s*\(\s*r?["'][^"']*\\[rnt]|\.\s*replace\s*\(\s*["']\\[rnt])/;
|
|
30195
|
+
const bodyWindow = 10;
|
|
30196
|
+
const wrappers = /* @__PURE__ */ new Set();
|
|
30197
|
+
for (let i2 = 0; i2 < lines.length; i2++) {
|
|
30198
|
+
const m = declRe.exec(lines[i2]);
|
|
30199
|
+
if (!m) continue;
|
|
30200
|
+
const body2 = lines.slice(i2, Math.min(lines.length, i2 + bodyWindow)).join("\n");
|
|
30201
|
+
if (stripRe.test(body2)) wrappers.add(m[1]);
|
|
30202
|
+
}
|
|
30203
|
+
if (wrappers.size === 0) return sanitizers;
|
|
30204
|
+
for (let i2 = 0; i2 < lines.length; i2++) {
|
|
30205
|
+
for (const name2 of wrappers) {
|
|
30206
|
+
if (declRe.test(lines[i2]) && new RegExp(`\\b${name2}\\s*\\(`).test(lines[i2])) continue;
|
|
30207
|
+
if (!new RegExp(`\\b${name2}\\s*\\(`).test(lines[i2])) continue;
|
|
30208
|
+
sanitizers.push({
|
|
30209
|
+
type: language === "java" ? "java_wrapper_crlf_strip" : "python_wrapper_crlf_strip",
|
|
30210
|
+
method: name2,
|
|
30211
|
+
line: i2 + 1,
|
|
30212
|
+
sanitizes: ["log_injection", "external_taint_escape"]
|
|
30213
|
+
});
|
|
30214
|
+
}
|
|
30215
|
+
}
|
|
30216
|
+
return sanitizers;
|
|
30217
|
+
}
|
|
30115
30218
|
function findJavaInlineCrlfStripLogSanitizers(code) {
|
|
30116
30219
|
const sanitizers = [];
|
|
30117
30220
|
const lines = code.split("\n");
|
|
@@ -33924,10 +34027,18 @@ function jsSsrfHostGuardedLines(code) {
|
|
|
33924
34027
|
}
|
|
33925
34028
|
return covered;
|
|
33926
34029
|
}
|
|
34030
|
+
var CSHARP_ALLOWLIST_STRIP_RE = /\bRegex\s*\.\s*Replace\s*\([^,]*,\s*@?"\[\^(?:[A-Za-z0-9_\- ]|\\w|\\d|\\s)+\]"\s*,\s*@?"[A-Za-z0-9_]?"\s*\)/;
|
|
33927
34031
|
var CSHARP_SANITIZER_RES = [
|
|
33928
34032
|
{ re: /\b(?:HtmlEncode|JavaScriptStringEncode)\s*\(/, type: "xss" },
|
|
33929
34033
|
{ re: /\bHtmlEncoder\s*\.\s*Encode\s*\(/, type: "xss" },
|
|
33930
|
-
{ re: /\bPath\s*\.\s*GetFileName\s*\(/, type: "path_traversal" }
|
|
34034
|
+
{ re: /\bPath\s*\.\s*GetFileName\s*\(/, type: "path_traversal" },
|
|
34035
|
+
// An allowlist strip leaves only inert characters, so it covers every family
|
|
34036
|
+
// whose exploitation needs a metacharacter. Each has a fixture; see
|
|
34037
|
+
// `issue-272-csharp-allowlist-strip.test.ts`.
|
|
34038
|
+
{ re: CSHARP_ALLOWLIST_STRIP_RE, type: "ldap_injection" },
|
|
34039
|
+
{ re: CSHARP_ALLOWLIST_STRIP_RE, type: "xpath_injection" },
|
|
34040
|
+
{ re: CSHARP_ALLOWLIST_STRIP_RE, type: "command_injection" },
|
|
34041
|
+
{ re: CSHARP_ALLOWLIST_STRIP_RE, type: "sql_injection" }
|
|
33931
34042
|
];
|
|
33932
34043
|
function csharpSanitizedVarsByType(code) {
|
|
33933
34044
|
const lines = code.split("\n");
|
|
@@ -34746,6 +34857,36 @@ var SinkFilterPass = class {
|
|
|
34746
34857
|
return true;
|
|
34747
34858
|
});
|
|
34748
34859
|
}
|
|
34860
|
+
if (language === "csharp") {
|
|
34861
|
+
const sourceLines = ctx.code.split("\n");
|
|
34862
|
+
const constBaseClients = /* @__PURE__ */ new Set();
|
|
34863
|
+
const declRe = /\b(?:var|HttpClient)\s+([A-Za-z_]\w*)\s*=\s*new\s+HttpClient\s*\{[^}]*\bBaseAddress\s*=\s*new\s+Uri\s*\(\s*(?:@?"[^"]*"|\$?"[^"{}]*")\s*\)/;
|
|
34864
|
+
for (const line of sourceLines) {
|
|
34865
|
+
const m = declRe.exec(line);
|
|
34866
|
+
if (m) constBaseClients.add(m[1]);
|
|
34867
|
+
}
|
|
34868
|
+
if (constBaseClients.size > 0) {
|
|
34869
|
+
const relativeLiteralArgRe = /\.\s*(?:Get|Post|Put|Patch|Delete|Send|GetString|GetByteArray|GetStream)\w*Async\s*\(\s*(?:@?\$?)"([^"]*)"/;
|
|
34870
|
+
filtered = filtered.filter((sink) => {
|
|
34871
|
+
if (sink.type !== "ssrf") return true;
|
|
34872
|
+
const text = sourceLines[sink.line - 1] ?? "";
|
|
34873
|
+
let onConstClient = false;
|
|
34874
|
+
for (const v of constBaseClients) {
|
|
34875
|
+
if (new RegExp(`(?<![\\w])${escapeRegex(v)}\\s*\\.`).test(text)) {
|
|
34876
|
+
onConstClient = true;
|
|
34877
|
+
break;
|
|
34878
|
+
}
|
|
34879
|
+
}
|
|
34880
|
+
if (!onConstClient) return true;
|
|
34881
|
+
const m = relativeLiteralArgRe.exec(text);
|
|
34882
|
+
if (!m) return true;
|
|
34883
|
+
const lit = m[1];
|
|
34884
|
+
if (/^[A-Za-z][A-Za-z0-9+.-]*:/.test(lit)) return true;
|
|
34885
|
+
if (lit.startsWith("//")) return true;
|
|
34886
|
+
return false;
|
|
34887
|
+
});
|
|
34888
|
+
}
|
|
34889
|
+
}
|
|
34749
34890
|
if (["javascript", "typescript"].includes(language)) {
|
|
34750
34891
|
const sourceLines = ctx.code.split("\n");
|
|
34751
34892
|
filtered = filtered.filter((sink) => {
|
|
@@ -40575,6 +40716,18 @@ var CRYPTO_BLOCKING_METHODS = /* @__PURE__ */ new Set([
|
|
|
40575
40716
|
"deriveKeySync"
|
|
40576
40717
|
]);
|
|
40577
40718
|
var SYNC_SUFFIX_RE2 = /Sync$/;
|
|
40719
|
+
var ROUTER_METHODS = /* @__PURE__ */ new Set([
|
|
40720
|
+
"get",
|
|
40721
|
+
"post",
|
|
40722
|
+
"put",
|
|
40723
|
+
"patch",
|
|
40724
|
+
"delete",
|
|
40725
|
+
"all",
|
|
40726
|
+
"options",
|
|
40727
|
+
"head",
|
|
40728
|
+
"use"
|
|
40729
|
+
]);
|
|
40730
|
+
var FN_ARG_PARAMS_RE = /^(?:async\s+)?(?:function\s*[\w$]*\s*)?\(([^)]*)\)\s*(?:=>|\{)|^(?:async\s+)?([A-Za-z_$][\w$]*)\s*=>/;
|
|
40578
40731
|
var BlockingMainThreadPass = class {
|
|
40579
40732
|
name = "blocking-main-thread";
|
|
40580
40733
|
category = "performance";
|
|
@@ -40596,7 +40749,10 @@ var BlockingMainThreadPass = class {
|
|
|
40596
40749
|
}
|
|
40597
40750
|
}
|
|
40598
40751
|
}
|
|
40599
|
-
|
|
40752
|
+
const inlineHandlers = this.collectInlineHandlerNames(graph.ir.calls);
|
|
40753
|
+
if (handlerRanges.length === 0 && inlineHandlers.size === 0) {
|
|
40754
|
+
return { blockingInHandlers: [] };
|
|
40755
|
+
}
|
|
40600
40756
|
const blockingInHandlers = [];
|
|
40601
40757
|
for (const call of graph.ir.calls) {
|
|
40602
40758
|
const name2 = call.method_name;
|
|
@@ -40604,7 +40760,8 @@ var BlockingMainThreadPass = class {
|
|
|
40604
40760
|
const isSyncSuffix = SYNC_SUFFIX_RE2.test(name2);
|
|
40605
40761
|
if (!isCrypto && !isSyncSuffix) continue;
|
|
40606
40762
|
const line = call.location.line;
|
|
40607
|
-
const
|
|
40763
|
+
const enclosing = call.in_method ?? null;
|
|
40764
|
+
const range = handlerRanges.find((r) => line >= r.start && line <= r.end) ?? (enclosing && inlineHandlers.has(enclosing) ? { start: line, end: line, name: enclosing } : void 0);
|
|
40608
40765
|
if (!range) continue;
|
|
40609
40766
|
const reason = isCrypto ? "crypto" : "sync-suffix";
|
|
40610
40767
|
blockingInHandlers.push({ line, method: name2, handler: range.name, reason });
|
|
@@ -40625,6 +40782,40 @@ var BlockingMainThreadPass = class {
|
|
|
40625
40782
|
}
|
|
40626
40783
|
return { blockingInHandlers };
|
|
40627
40784
|
}
|
|
40785
|
+
/**
|
|
40786
|
+
* Synthetic `<verb>_handler` names that really do belong to an HTTP route.
|
|
40787
|
+
*
|
|
40788
|
+
* The extractor tags *any* function argument of *any* member-expression call
|
|
40789
|
+
* this way, so `items.map(x => …)` becomes `map_handler` and
|
|
40790
|
+
* `cache.get(k, () => …)` becomes `get_handler`. Accepting every
|
|
40791
|
+
* `*_handler` would pull ordinary callbacks into a pass that is explicitly
|
|
40792
|
+
* about the HTTP request path, so two conditions are required: the method is
|
|
40793
|
+
* a router verb, and the callback's own parameters look like a request
|
|
40794
|
+
* handler's — the same `HANDLER_PARAM_NAMES` test used for methods.
|
|
40795
|
+
*
|
|
40796
|
+
* Known limitation: `in_method` carries no receiver, so two callbacks that
|
|
40797
|
+
* share a verb in one file are indistinguishable. A file containing both
|
|
40798
|
+
* `app.get('/x', (req, res) => …)` and `cache.get(k, () => …)` will treat a
|
|
40799
|
+
* blocking call in the second as in-handler. Both conditions still have to
|
|
40800
|
+
* hold for the verb to register at all, which keeps that to files already
|
|
40801
|
+
* mounting a real route of the same verb.
|
|
40802
|
+
*/
|
|
40803
|
+
collectInlineHandlerNames(calls) {
|
|
40804
|
+
const names = /* @__PURE__ */ new Set();
|
|
40805
|
+
for (const call of calls) {
|
|
40806
|
+
if (!ROUTER_METHODS.has(call.method_name)) continue;
|
|
40807
|
+
for (const arg of call.arguments) {
|
|
40808
|
+
const match = FN_ARG_PARAMS_RE.exec((arg.expression ?? "").trim());
|
|
40809
|
+
if (!match) continue;
|
|
40810
|
+
const params = (match[1] ?? match[2] ?? "").split(",").map((p) => p.trim().toLowerCase()).filter((p) => p.length > 0);
|
|
40811
|
+
if (params.some((p) => HANDLER_PARAM_NAMES.has(p))) {
|
|
40812
|
+
names.add(`${call.method_name}_handler`);
|
|
40813
|
+
break;
|
|
40814
|
+
}
|
|
40815
|
+
}
|
|
40816
|
+
}
|
|
40817
|
+
return names;
|
|
40818
|
+
}
|
|
40628
40819
|
isRequestHandler(method) {
|
|
40629
40820
|
if (method.annotations.some((a) => HTTP_DECORATORS.has(a))) return true;
|
|
40630
40821
|
if (HANDLER_METHOD_NAMES.has(method.name)) return true;
|
|
@@ -9409,6 +9409,7 @@ function buildCSharpDFG(tree, cache) {
|
|
|
9409
9409
|
];
|
|
9410
9410
|
for (const method of methods) {
|
|
9411
9411
|
scopeStack.push(/* @__PURE__ */ new Map());
|
|
9412
|
+
const methodDefStart = defs.length;
|
|
9412
9413
|
const params = method.childForFieldName("parameters");
|
|
9413
9414
|
if (params) {
|
|
9414
9415
|
for (const def of extractParameterDefs(params, defIdCounter)) {
|
|
@@ -9438,6 +9439,7 @@ function buildCSharpDFG(tree, cache) {
|
|
|
9438
9439
|
}
|
|
9439
9440
|
}
|
|
9440
9441
|
const bodyUses = extractUses(body2, useIdCounter, scopeStack, false);
|
|
9442
|
+
resolveReassignedUses(defs.slice(methodDefStart), bodyUses.uses);
|
|
9441
9443
|
uses.push(...bodyUses.uses);
|
|
9442
9444
|
useIdCounter = bodyUses.nextId;
|
|
9443
9445
|
}
|
|
@@ -10147,6 +10149,30 @@ function findReachingDef(name2, scopeStack) {
|
|
|
10147
10149
|
}
|
|
10148
10150
|
return null;
|
|
10149
10151
|
}
|
|
10152
|
+
function resolveReassignedUses(methodDefs, methodUses) {
|
|
10153
|
+
const defsByVar = /* @__PURE__ */ new Map();
|
|
10154
|
+
for (const def of methodDefs) {
|
|
10155
|
+
if (def.kind !== "local" && def.kind !== "param") continue;
|
|
10156
|
+
const existing = defsByVar.get(def.variable);
|
|
10157
|
+
if (existing) existing.push(def);
|
|
10158
|
+
else defsByVar.set(def.variable, [def]);
|
|
10159
|
+
}
|
|
10160
|
+
for (const list of defsByVar.values()) {
|
|
10161
|
+
if (list.length > 1) list.sort((a, b) => a.line - b.line || a.id - b.id);
|
|
10162
|
+
}
|
|
10163
|
+
for (const use of methodUses) {
|
|
10164
|
+
const list = defsByVar.get(use.variable);
|
|
10165
|
+
if (!list || list.length < 2) continue;
|
|
10166
|
+
let strictlyBefore;
|
|
10167
|
+
let sameLine;
|
|
10168
|
+
for (const def of list) {
|
|
10169
|
+
if (def.line < use.line) strictlyBefore = def;
|
|
10170
|
+
else if (def.line === use.line) sameLine = def;
|
|
10171
|
+
}
|
|
10172
|
+
const reaching = strictlyBefore ?? sameLine;
|
|
10173
|
+
if (reaching) use.def_id = reaching.id;
|
|
10174
|
+
}
|
|
10175
|
+
}
|
|
10150
10176
|
function computeChains(defs, uses) {
|
|
10151
10177
|
const chains = [];
|
|
10152
10178
|
const defById = /* @__PURE__ */ new Map();
|
|
@@ -16930,7 +16956,7 @@ function checkSanitized(fromLine, toLine, sinkType, sanitizersByLine, ctx) {
|
|
|
16930
16956
|
function buildTaintFlow(source, sink, taintInfo) {
|
|
16931
16957
|
const path = [];
|
|
16932
16958
|
path.push({
|
|
16933
|
-
variable: taintInfo.variable,
|
|
16959
|
+
variable: source.variable ?? taintInfo.variable,
|
|
16934
16960
|
line: source.line,
|
|
16935
16961
|
type: "source",
|
|
16936
16962
|
description: `Tainted data enters via ${source.type}`
|
|
@@ -9343,6 +9343,7 @@ function buildCSharpDFG(tree, cache) {
|
|
|
9343
9343
|
];
|
|
9344
9344
|
for (const method of methods) {
|
|
9345
9345
|
scopeStack.push(/* @__PURE__ */ new Map());
|
|
9346
|
+
const methodDefStart = defs.length;
|
|
9346
9347
|
const params = method.childForFieldName("parameters");
|
|
9347
9348
|
if (params) {
|
|
9348
9349
|
for (const def of extractParameterDefs(params, defIdCounter)) {
|
|
@@ -9372,6 +9373,7 @@ function buildCSharpDFG(tree, cache) {
|
|
|
9372
9373
|
}
|
|
9373
9374
|
}
|
|
9374
9375
|
const bodyUses = extractUses(body2, useIdCounter, scopeStack, false);
|
|
9376
|
+
resolveReassignedUses(defs.slice(methodDefStart), bodyUses.uses);
|
|
9375
9377
|
uses.push(...bodyUses.uses);
|
|
9376
9378
|
useIdCounter = bodyUses.nextId;
|
|
9377
9379
|
}
|
|
@@ -10081,6 +10083,30 @@ function findReachingDef(name2, scopeStack) {
|
|
|
10081
10083
|
}
|
|
10082
10084
|
return null;
|
|
10083
10085
|
}
|
|
10086
|
+
function resolveReassignedUses(methodDefs, methodUses) {
|
|
10087
|
+
const defsByVar = /* @__PURE__ */ new Map();
|
|
10088
|
+
for (const def of methodDefs) {
|
|
10089
|
+
if (def.kind !== "local" && def.kind !== "param") continue;
|
|
10090
|
+
const existing = defsByVar.get(def.variable);
|
|
10091
|
+
if (existing) existing.push(def);
|
|
10092
|
+
else defsByVar.set(def.variable, [def]);
|
|
10093
|
+
}
|
|
10094
|
+
for (const list of defsByVar.values()) {
|
|
10095
|
+
if (list.length > 1) list.sort((a, b) => a.line - b.line || a.id - b.id);
|
|
10096
|
+
}
|
|
10097
|
+
for (const use of methodUses) {
|
|
10098
|
+
const list = defsByVar.get(use.variable);
|
|
10099
|
+
if (!list || list.length < 2) continue;
|
|
10100
|
+
let strictlyBefore;
|
|
10101
|
+
let sameLine;
|
|
10102
|
+
for (const def of list) {
|
|
10103
|
+
if (def.line < use.line) strictlyBefore = def;
|
|
10104
|
+
else if (def.line === use.line) sameLine = def;
|
|
10105
|
+
}
|
|
10106
|
+
const reaching = strictlyBefore ?? sameLine;
|
|
10107
|
+
if (reaching) use.def_id = reaching.id;
|
|
10108
|
+
}
|
|
10109
|
+
}
|
|
10084
10110
|
function computeChains(defs, uses) {
|
|
10085
10111
|
const chains = [];
|
|
10086
10112
|
const defById = /* @__PURE__ */ new Map();
|
|
@@ -16864,7 +16890,7 @@ function checkSanitized(fromLine, toLine, sinkType, sanitizersByLine, ctx) {
|
|
|
16864
16890
|
function buildTaintFlow(source, sink, taintInfo) {
|
|
16865
16891
|
const path = [];
|
|
16866
16892
|
path.push({
|
|
16867
|
-
variable: taintInfo.variable,
|
|
16893
|
+
variable: source.variable ?? taintInfo.variable,
|
|
16868
16894
|
line: source.line,
|
|
16869
16895
|
type: "source",
|
|
16870
16896
|
description: `Tainted data enters via ${source.type}`
|
|
@@ -90,6 +90,9 @@ function buildCSharpDFG(tree, cache) {
|
|
|
90
90
|
];
|
|
91
91
|
for (const method of methods) {
|
|
92
92
|
scopeStack.push(new Map());
|
|
93
|
+
// Where this method's defs begin, so the reaching-def correction below stays
|
|
94
|
+
// inside the method — `defs` accumulates across every method in the file.
|
|
95
|
+
const methodDefStart = defs.length;
|
|
93
96
|
// Parameters (kind: param).
|
|
94
97
|
const params = method.childForFieldName('parameters');
|
|
95
98
|
if (params) {
|
|
@@ -123,6 +126,7 @@ function buildCSharpDFG(tree, cache) {
|
|
|
123
126
|
}
|
|
124
127
|
// Uses: all identifier references, reaching defs resolved from scope.
|
|
125
128
|
const bodyUses = extractUses(body, useIdCounter, scopeStack, false);
|
|
129
|
+
resolveReassignedUses(defs.slice(methodDefStart), bodyUses.uses);
|
|
126
130
|
uses.push(...bodyUses.uses);
|
|
127
131
|
useIdCounter = bodyUses.nextId;
|
|
128
132
|
}
|
|
@@ -986,6 +990,79 @@ function findReachingDef(name, scopeStack) {
|
|
|
986
990
|
}
|
|
987
991
|
return null;
|
|
988
992
|
}
|
|
993
|
+
/**
|
|
994
|
+
* Point uses of a *reassigned* variable at the nearest preceding definition.
|
|
995
|
+
*
|
|
996
|
+
* `DFGUse.def_id` is specified as the reaching definition, but the C# builder
|
|
997
|
+
* collects every def in a method body before it resolves any use, so the scope
|
|
998
|
+
* map holds only the LAST def of each variable by the time uses are resolved.
|
|
999
|
+
* With one def per variable last == reaching and nothing is wrong; a reassigned
|
|
1000
|
+
* variable binds every use to its final def, which breaks taint two ways
|
|
1001
|
+
* (cognium-dev#287):
|
|
1002
|
+
*
|
|
1003
|
+
* var v = input; // def A
|
|
1004
|
+
* v = v + ""; // def B — the right-hand `v` resolved to B, i.e. itself
|
|
1005
|
+
* sink(v); // resolved to B
|
|
1006
|
+
*
|
|
1007
|
+
* `computeChains` skips a use whose def_id is the def it is building, so the
|
|
1008
|
+
* self-reference produced no A -> B chain and taint stopped at A, while the
|
|
1009
|
+
* sink read B. It also emitted a *backwards* B -> A chain, because the use of
|
|
1010
|
+
* `v` on def A's own line resolved to B. Net effect: `v = v.Trim()`,
|
|
1011
|
+
* `v = v.Replace(...)`, `v = v + x` silently cleared taint — a false negative,
|
|
1012
|
+
* which leaves no trace to notice.
|
|
1013
|
+
*
|
|
1014
|
+
* Deliberately narrow. A use is repointed only when its variable has more than
|
|
1015
|
+
* one definition AND a strictly-preceding one exists, so single-def variables
|
|
1016
|
+
* and one-line `var sb = new StringBuilder(); sb.Append(x)` shapes keep exactly
|
|
1017
|
+
* the binding they have today. Same-line ties resolve to the earlier def
|
|
1018
|
+
* because in `v = f(v)` the right-hand side is evaluated before the assignment.
|
|
1019
|
+
*
|
|
1020
|
+
* Scoped to `local` and `param` defs. Fields are excluded on purpose: they are
|
|
1021
|
+
* registered after the method loop and resolving them here would newly enable
|
|
1022
|
+
* cross-method field-carried taint, which is a separate feature (the
|
|
1023
|
+
* `San08FieldStore` half of #287), not this correction.
|
|
1024
|
+
*
|
|
1025
|
+
* Call this per method, with only that method's defs, or a local in one method
|
|
1026
|
+
* will bind to a same-named local in another.
|
|
1027
|
+
*/
|
|
1028
|
+
function resolveReassignedUses(methodDefs, methodUses) {
|
|
1029
|
+
const defsByVar = new Map();
|
|
1030
|
+
for (const def of methodDefs) {
|
|
1031
|
+
if (def.kind !== 'local' && def.kind !== 'param')
|
|
1032
|
+
continue;
|
|
1033
|
+
const existing = defsByVar.get(def.variable);
|
|
1034
|
+
if (existing)
|
|
1035
|
+
existing.push(def);
|
|
1036
|
+
else
|
|
1037
|
+
defsByVar.set(def.variable, [def]);
|
|
1038
|
+
}
|
|
1039
|
+
for (const list of defsByVar.values()) {
|
|
1040
|
+
if (list.length > 1)
|
|
1041
|
+
list.sort((a, b) => a.line - b.line || a.id - b.id);
|
|
1042
|
+
}
|
|
1043
|
+
for (const use of methodUses) {
|
|
1044
|
+
const list = defsByVar.get(use.variable);
|
|
1045
|
+
if (!list || list.length < 2)
|
|
1046
|
+
continue; // not reassigned — leave untouched
|
|
1047
|
+
// Prefer the last def strictly before this use: in `v = f(v)` the
|
|
1048
|
+
// right-hand side is evaluated before the assignment, so the read sees the
|
|
1049
|
+
// previous value. Falling back to a def on the use's own line keeps the
|
|
1050
|
+
// declaration site (`var v = input`) bound to itself instead of to a later
|
|
1051
|
+
// reassignment — that mis-binding is what emitted a *backwards* chain from
|
|
1052
|
+
// the second def to the first.
|
|
1053
|
+
let strictlyBefore;
|
|
1054
|
+
let sameLine;
|
|
1055
|
+
for (const def of list) {
|
|
1056
|
+
if (def.line < use.line)
|
|
1057
|
+
strictlyBefore = def;
|
|
1058
|
+
else if (def.line === use.line)
|
|
1059
|
+
sameLine = def;
|
|
1060
|
+
}
|
|
1061
|
+
const reaching = strictlyBefore ?? sameLine;
|
|
1062
|
+
if (reaching)
|
|
1063
|
+
use.def_id = reaching.id;
|
|
1064
|
+
}
|
|
1065
|
+
}
|
|
989
1066
|
/**
|
|
990
1067
|
* Compute def-use chains.
|
|
991
1068
|
*
|
|
@@ -1023,7 +1100,7 @@ function computeChains(defs, uses) {
|
|
|
1023
1100
|
for (const use of usesOnLine) {
|
|
1024
1101
|
// If this use has a reaching def, create a chain
|
|
1025
1102
|
if (use.def_id !== null && use.def_id !== def.id) {
|
|
1026
|
-
const key = `${use.def_id}
|
|
1103
|
+
const key = `${use.def_id}\0${def.id}\0${use.variable}`;
|
|
1027
1104
|
if (!seenChains.has(key)) {
|
|
1028
1105
|
seenChains.add(key);
|
|
1029
1106
|
chains.push({
|