cognium-dev 4.12.0 → 4.13.1
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/README.md +14 -21
- package/dist/cli.js +919 -84
- package/package.json +2 -2
package/dist/cli.js
CHANGED
|
@@ -9055,6 +9055,7 @@ function buildCSharpDFG(tree, cache) {
|
|
|
9055
9055
|
currentScope(scopeStack).set(name2, def.id);
|
|
9056
9056
|
}
|
|
9057
9057
|
}
|
|
9058
|
+
const plainTargets = [];
|
|
9058
9059
|
for (const asn of findNodes(body2, "assignment_expression")) {
|
|
9059
9060
|
const left = asn.childForFieldName("left");
|
|
9060
9061
|
if (left?.type === "identifier") {
|
|
@@ -9062,9 +9063,17 @@ function buildCSharpDFG(tree, cache) {
|
|
|
9062
9063
|
const def = { id: defIdCounter++, variable: name2, line: asn.startPosition.row + 1, kind: "local" };
|
|
9063
9064
|
defs.push(def);
|
|
9064
9065
|
currentScope(scopeStack).set(name2, def.id);
|
|
9066
|
+
if (asn.children.some((c) => c?.type === "=")) {
|
|
9067
|
+
plainTargets.push({ name: name2, line: left.startPosition.row + 1 });
|
|
9068
|
+
}
|
|
9065
9069
|
}
|
|
9066
9070
|
}
|
|
9067
9071
|
const bodyUses = extractUses(body2, useIdCounter, scopeStack, false);
|
|
9072
|
+
for (const target of plainTargets) {
|
|
9073
|
+
const idx = bodyUses.uses.findIndex((u) => u.variable === target.name && u.line === target.line);
|
|
9074
|
+
if (idx !== -1)
|
|
9075
|
+
bodyUses.uses.splice(idx, 1);
|
|
9076
|
+
}
|
|
9068
9077
|
resolveReassignedUses(defs.slice(methodDefStart), bodyUses.uses);
|
|
9069
9078
|
uses.push(...bodyUses.uses);
|
|
9070
9079
|
useIdCounter = bodyUses.nextId;
|
|
@@ -20030,7 +20039,13 @@ class ConstantPropagator {
|
|
|
20030
20039
|
}
|
|
20031
20040
|
return;
|
|
20032
20041
|
}
|
|
20033
|
-
|
|
20042
|
+
let condValue = this.evaluateExpression(condition);
|
|
20043
|
+
if (!(isKnown(condValue) && condValue.type === "bool")) {
|
|
20044
|
+
const literal = this.literalCondition(condition);
|
|
20045
|
+
if (literal !== null) {
|
|
20046
|
+
condValue = { type: "bool", value: literal, sourceLine: getNodeLine(condition) };
|
|
20047
|
+
}
|
|
20048
|
+
}
|
|
20034
20049
|
if (isKnown(condValue) && condValue.type === "bool") {
|
|
20035
20050
|
if (condValue.value === true) {
|
|
20036
20051
|
if (alternative) {
|
|
@@ -20165,19 +20180,41 @@ class ConstantPropagator {
|
|
|
20165
20180
|
break;
|
|
20166
20181
|
}
|
|
20167
20182
|
}
|
|
20168
|
-
|
|
20183
|
+
if (!switchValue) {
|
|
20184
|
+
const valueNode = node.childForFieldName("value");
|
|
20185
|
+
if (valueNode) {
|
|
20186
|
+
const literal = this.literalValue(valueNode);
|
|
20187
|
+
switchValue = literal !== null ? { type: typeof literal === "number" ? "int" : "string", value: literal, sourceLine: getNodeLine(valueNode) } : this.evaluateExpression(valueNode);
|
|
20188
|
+
}
|
|
20189
|
+
}
|
|
20190
|
+
const switchBlock = node.children.find((c) => c.type === "switch_block" || c.type === "switch_body");
|
|
20169
20191
|
if (!switchBlock) {
|
|
20170
20192
|
for (const child of node.children) {
|
|
20171
20193
|
this.visit(child);
|
|
20172
20194
|
}
|
|
20173
20195
|
return;
|
|
20174
20196
|
}
|
|
20175
|
-
const caseGroups = switchBlock.children.filter((c) => c.type === "switch_block_statement_group" || c.type === "switch_rule");
|
|
20197
|
+
const caseGroups = switchBlock.children.filter((c) => c.type === "switch_block_statement_group" || c.type === "switch_rule" || c.type === "switch_section" || c.type === "switch_case" || c.type === "switch_default");
|
|
20198
|
+
if (caseGroups.length === 0) {
|
|
20199
|
+
this.visit(switchBlock);
|
|
20200
|
+
return;
|
|
20201
|
+
}
|
|
20176
20202
|
if (switchValue && isKnown(switchValue)) {
|
|
20177
20203
|
let matchingIdx = -1;
|
|
20178
20204
|
let defaultIdx = -1;
|
|
20179
20205
|
for (let i2 = 0;i2 < caseGroups.length; i2++) {
|
|
20180
20206
|
const caseGroup = caseGroups[i2];
|
|
20207
|
+
if (caseGroup.type === "switch_default") {
|
|
20208
|
+
defaultIdx = i2;
|
|
20209
|
+
continue;
|
|
20210
|
+
}
|
|
20211
|
+
if (caseGroup.type === "switch_case") {
|
|
20212
|
+
const label = caseGroup.childForFieldName("value");
|
|
20213
|
+
const caseValue = label ? this.literalValue(label) : null;
|
|
20214
|
+
if (caseValue !== null && caseValue === switchValue.value)
|
|
20215
|
+
matchingIdx = i2;
|
|
20216
|
+
continue;
|
|
20217
|
+
}
|
|
20181
20218
|
for (const child of caseGroup.children) {
|
|
20182
20219
|
if (child.type === "switch_label") {
|
|
20183
20220
|
const labelText = getNodeText2(child, this.source);
|
|
@@ -20189,6 +20226,16 @@ class ConstantPropagator {
|
|
|
20189
20226
|
matchingIdx = i2;
|
|
20190
20227
|
}
|
|
20191
20228
|
}
|
|
20229
|
+
} else if (caseGroup.type === "switch_section") {
|
|
20230
|
+
if (child.type === "default") {
|
|
20231
|
+
defaultIdx = i2;
|
|
20232
|
+
} else if (child.type === "constant_pattern") {
|
|
20233
|
+
const inner = child.children.find((c) => c.isNamed);
|
|
20234
|
+
const caseValue = inner ? this.literalValue(inner) : null;
|
|
20235
|
+
if (caseValue !== null && caseValue === switchValue.value) {
|
|
20236
|
+
matchingIdx = i2;
|
|
20237
|
+
}
|
|
20238
|
+
}
|
|
20192
20239
|
}
|
|
20193
20240
|
}
|
|
20194
20241
|
}
|
|
@@ -20271,6 +20318,41 @@ class ConstantPropagator {
|
|
|
20271
20318
|
}
|
|
20272
20319
|
return false;
|
|
20273
20320
|
}
|
|
20321
|
+
literalCondition(node) {
|
|
20322
|
+
if (node.type === "parenthesized_expression") {
|
|
20323
|
+
const inner = node.children.find((c) => c.isNamed);
|
|
20324
|
+
return inner ? this.literalCondition(inner) : null;
|
|
20325
|
+
}
|
|
20326
|
+
if (node.type === "boolean_literal") {
|
|
20327
|
+
const text = getNodeText2(node, this.source);
|
|
20328
|
+
return text === "true" ? true : text === "false" ? false : null;
|
|
20329
|
+
}
|
|
20330
|
+
if (node.type === "binary_expression") {
|
|
20331
|
+
const left = node.childForFieldName("left");
|
|
20332
|
+
const right = node.childForFieldName("right");
|
|
20333
|
+
const op = node.childForFieldName("operator");
|
|
20334
|
+
const opText = op ? getNodeText2(op, this.source) : "";
|
|
20335
|
+
if (!left || !right || opText !== "==" && opText !== "!=")
|
|
20336
|
+
return null;
|
|
20337
|
+
const l = this.literalValue(left);
|
|
20338
|
+
const r = this.literalValue(right);
|
|
20339
|
+
if (l === null || r === null)
|
|
20340
|
+
return null;
|
|
20341
|
+
return opText === "==" ? l === r : l !== r;
|
|
20342
|
+
}
|
|
20343
|
+
return null;
|
|
20344
|
+
}
|
|
20345
|
+
literalValue(node) {
|
|
20346
|
+
const text = getNodeText2(node, this.source);
|
|
20347
|
+
if (node.type === "integer_literal" || node.type === "decimal_integer_literal") {
|
|
20348
|
+
const n = Number(text.replace(/_/g, "").replace(/[uUlL]+$/, ""));
|
|
20349
|
+
return Number.isInteger(n) ? n : null;
|
|
20350
|
+
}
|
|
20351
|
+
if (node.type === "character_literal" || node.type === "string_literal") {
|
|
20352
|
+
return text.slice(1, -1);
|
|
20353
|
+
}
|
|
20354
|
+
return null;
|
|
20355
|
+
}
|
|
20274
20356
|
extractCaseValue(labelNode) {
|
|
20275
20357
|
for (const child of labelNode.children) {
|
|
20276
20358
|
if (child.type === "decimal_integer_literal") {
|
|
@@ -34062,6 +34144,486 @@ class MyBatisAnnotationSqlSinkPass {
|
|
|
34062
34144
|
}
|
|
34063
34145
|
}
|
|
34064
34146
|
|
|
34147
|
+
// ../circle-ir/dist/analysis/passes/csharp-sanitizer-credit.js
|
|
34148
|
+
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*\)/;
|
|
34149
|
+
var CSHARP_ALLOWLIST_FILTER_RE = /\bnew\s+string\s*\(\s*[A-Za-z_]\w*\s*\.\s*Where\s*\(\s*char\s*\.\s*Is(?:LetterOrDigit|Digit|Letter)\s*\)\s*\.\s*ToArray\s*\(\s*\)\s*\)/;
|
|
34150
|
+
var CSHARP_SANITIZER_RES = [
|
|
34151
|
+
{ re: /\b(?:HtmlEncode|JavaScriptStringEncode)\s*\(/, type: "xss" },
|
|
34152
|
+
{ re: /\bHtmlEncoder\s*\.\s*(?:Default\s*\.\s*)?Encode\s*\(/, type: "xss" },
|
|
34153
|
+
{ re: /\bPath\s*\.\s*GetFileName\s*\(/, type: "path_traversal" },
|
|
34154
|
+
{ re: /\bSecurityElement\s*\.\s*Escape\s*\(/, type: "xpath_injection" },
|
|
34155
|
+
{ re: CSHARP_ALLOWLIST_STRIP_RE, type: "ldap_injection" },
|
|
34156
|
+
{ re: CSHARP_ALLOWLIST_STRIP_RE, type: "xpath_injection" },
|
|
34157
|
+
{ re: CSHARP_ALLOWLIST_STRIP_RE, type: "command_injection" },
|
|
34158
|
+
{ re: CSHARP_ALLOWLIST_STRIP_RE, type: "sql_injection" },
|
|
34159
|
+
{ re: CSHARP_ALLOWLIST_STRIP_RE, type: "crlf" },
|
|
34160
|
+
{ re: CSHARP_ALLOWLIST_FILTER_RE, type: "ldap_injection" },
|
|
34161
|
+
{ re: CSHARP_ALLOWLIST_FILTER_RE, type: "xpath_injection" },
|
|
34162
|
+
{ re: CSHARP_ALLOWLIST_FILTER_RE, type: "command_injection" },
|
|
34163
|
+
{ re: CSHARP_ALLOWLIST_FILTER_RE, type: "sql_injection" },
|
|
34164
|
+
{ re: CSHARP_ALLOWLIST_FILTER_RE, type: "crlf" }
|
|
34165
|
+
];
|
|
34166
|
+
var INERT_WHEN_ALPHANUMERIC = new Set([
|
|
34167
|
+
"ldap_injection",
|
|
34168
|
+
"xpath_injection",
|
|
34169
|
+
"command_injection",
|
|
34170
|
+
"sql_injection",
|
|
34171
|
+
"crlf",
|
|
34172
|
+
"xss",
|
|
34173
|
+
"path_traversal",
|
|
34174
|
+
"code_injection"
|
|
34175
|
+
]);
|
|
34176
|
+
var IDENT = "[A-Za-z_]\\w*";
|
|
34177
|
+
var ASSIGN_RE = new RegExp(`^\\s*(?:var\\s+|[A-Za-z_][\\w.<>\\[\\]?]*\\s+)?(${IDENT})\\s*=(?!=)\\s*(.+?);?\\s*$`);
|
|
34178
|
+
var ENDS_STATEMENT_RE = /[;{}]\s*(?:\/\/.*)?$/;
|
|
34179
|
+
function escapeRegex(s) {
|
|
34180
|
+
return s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
34181
|
+
}
|
|
34182
|
+
function withoutStringLiterals(text) {
|
|
34183
|
+
return text.replace(/@"(?:[^"]|"")*"|"(?:[^"\\]|\\.)*"/g, '""');
|
|
34184
|
+
}
|
|
34185
|
+
function mentions(text, name2) {
|
|
34186
|
+
return new RegExp(`(?<![\\w])${escapeRegex(name2)}(?![\\w])`).test(text);
|
|
34187
|
+
}
|
|
34188
|
+
function insideParens(text, open) {
|
|
34189
|
+
let depth = 0;
|
|
34190
|
+
for (let i2 = open;i2 < text.length; i2++) {
|
|
34191
|
+
if (text[i2] === "(")
|
|
34192
|
+
depth++;
|
|
34193
|
+
else if (text[i2] === ")" && --depth === 0)
|
|
34194
|
+
return text.slice(open + 1, i2);
|
|
34195
|
+
}
|
|
34196
|
+
return null;
|
|
34197
|
+
}
|
|
34198
|
+
function lastCallArguments(text) {
|
|
34199
|
+
const close = text.lastIndexOf(")");
|
|
34200
|
+
if (close < 0)
|
|
34201
|
+
return null;
|
|
34202
|
+
let depth = 0;
|
|
34203
|
+
for (let i2 = close;i2 >= 0; i2--) {
|
|
34204
|
+
if (text[i2] === ")")
|
|
34205
|
+
depth++;
|
|
34206
|
+
else if (text[i2] === "(" && --depth === 0)
|
|
34207
|
+
return text.slice(i2 + 1, close);
|
|
34208
|
+
}
|
|
34209
|
+
return null;
|
|
34210
|
+
}
|
|
34211
|
+
function leadingIf(text) {
|
|
34212
|
+
const m = /^\s*(?:\}\s*)?(?:else\s+)?if\s*\(/.exec(text);
|
|
34213
|
+
if (!m)
|
|
34214
|
+
return null;
|
|
34215
|
+
const open = m[0].length - 1;
|
|
34216
|
+
const cond = insideParens(text, open);
|
|
34217
|
+
if (cond === null)
|
|
34218
|
+
return null;
|
|
34219
|
+
return { cond: cond.trim(), rest: text.slice(open + cond.length + 2).trim() };
|
|
34220
|
+
}
|
|
34221
|
+
var EXITS_RE = /^(?:\{\s*)?(?:return\b|throw\b|continue\b|break\b)/;
|
|
34222
|
+
|
|
34223
|
+
class CSharpCredit {
|
|
34224
|
+
methods;
|
|
34225
|
+
lines;
|
|
34226
|
+
helperCache = new Map;
|
|
34227
|
+
guardHelperCache = new Map;
|
|
34228
|
+
constructor(code, methods) {
|
|
34229
|
+
this.methods = methods;
|
|
34230
|
+
this.lines = code.split(/\r?\n/);
|
|
34231
|
+
}
|
|
34232
|
+
methodAt(line) {
|
|
34233
|
+
let best = null;
|
|
34234
|
+
for (const m of this.methods) {
|
|
34235
|
+
if (line < m.start_line || line > m.end_line)
|
|
34236
|
+
continue;
|
|
34237
|
+
if (!best || m.end_line - m.start_line < best.end_line - best.start_line)
|
|
34238
|
+
best = m;
|
|
34239
|
+
}
|
|
34240
|
+
return best;
|
|
34241
|
+
}
|
|
34242
|
+
statements(from, to) {
|
|
34243
|
+
const out2 = [];
|
|
34244
|
+
for (let i2 = Math.max(from, 1);i2 < to; i2++) {
|
|
34245
|
+
let text = (this.lines[i2 - 1] ?? "").trimEnd();
|
|
34246
|
+
const first = i2;
|
|
34247
|
+
if (/^\s*(?:[\w.<>[\]?]+\s+)?[A-Za-z_]\w*\s*=(?!=)/.test(text) && !ENDS_STATEMENT_RE.test(text)) {
|
|
34248
|
+
while (i2 < to - 1 && i2 < first + 12) {
|
|
34249
|
+
i2++;
|
|
34250
|
+
text += " " + (this.lines[i2 - 1] ?? "").trim();
|
|
34251
|
+
if (ENDS_STATEMENT_RE.test(this.lines[i2 - 1] ?? ""))
|
|
34252
|
+
break;
|
|
34253
|
+
}
|
|
34254
|
+
}
|
|
34255
|
+
out2.push({ line: first, text });
|
|
34256
|
+
}
|
|
34257
|
+
return out2;
|
|
34258
|
+
}
|
|
34259
|
+
builtinSanitizes(type, text) {
|
|
34260
|
+
for (const { re, type: t } of CSHARP_SANITIZER_RES) {
|
|
34261
|
+
if (t === type && re.test(text))
|
|
34262
|
+
return true;
|
|
34263
|
+
}
|
|
34264
|
+
if (type === "ldap_injection") {
|
|
34265
|
+
if (/\.\s*Replace\s*\(\s*"\("\s*,/.test(text) && /\.\s*Replace\s*\(\s*"\)"\s*,/.test(text))
|
|
34266
|
+
return true;
|
|
34267
|
+
}
|
|
34268
|
+
if (type === "path_traversal") {
|
|
34269
|
+
if (/\b([A-Za-z_]\w*)\s*\.\s*StartsWith\s*\([^)]*\)\s*\?\s*\1\s*:\s*null\b/.test(text))
|
|
34270
|
+
return true;
|
|
34271
|
+
}
|
|
34272
|
+
return false;
|
|
34273
|
+
}
|
|
34274
|
+
helpers(type) {
|
|
34275
|
+
const cached = this.helperCache.get(type);
|
|
34276
|
+
if (cached)
|
|
34277
|
+
return cached;
|
|
34278
|
+
const set = new Set;
|
|
34279
|
+
this.helperCache.set(type, set);
|
|
34280
|
+
for (const m of this.methods) {
|
|
34281
|
+
const returns = this.returnExprs(m);
|
|
34282
|
+
if (returns.length === 0)
|
|
34283
|
+
continue;
|
|
34284
|
+
const ok = returns.every((r) => {
|
|
34285
|
+
if (this.builtinSanitizes(type, r.expr))
|
|
34286
|
+
return true;
|
|
34287
|
+
const v = /^([A-Za-z_]\w*)$/.exec(r.expr.trim());
|
|
34288
|
+
return !!v && this.sanitizedVarsIn(type, m.start_line, r.line, false).has(v[1]);
|
|
34289
|
+
});
|
|
34290
|
+
if (ok)
|
|
34291
|
+
set.add(m.name);
|
|
34292
|
+
}
|
|
34293
|
+
return set;
|
|
34294
|
+
}
|
|
34295
|
+
returnExprs(m) {
|
|
34296
|
+
const out2 = [];
|
|
34297
|
+
for (const st of this.statements(m.start_line, m.end_line + 1)) {
|
|
34298
|
+
for (const r of st.text.matchAll(/\breturn\s+([^;]+);/g))
|
|
34299
|
+
out2.push({ line: st.line, expr: r[1] });
|
|
34300
|
+
if (st.line === m.start_line) {
|
|
34301
|
+
const body2 = /\)\s*=>\s*(.+?);\s*$/.exec(st.text);
|
|
34302
|
+
if (body2)
|
|
34303
|
+
out2.push({ line: st.line, expr: body2[1] });
|
|
34304
|
+
}
|
|
34305
|
+
}
|
|
34306
|
+
return out2;
|
|
34307
|
+
}
|
|
34308
|
+
sanitizes(type, text) {
|
|
34309
|
+
if (this.builtinSanitizes(type, text))
|
|
34310
|
+
return true;
|
|
34311
|
+
for (const name2 of this.helpers(type)) {
|
|
34312
|
+
if (new RegExp(`(?<![\\w.])${escapeRegex(name2)}\\s*\\(`).test(text))
|
|
34313
|
+
return true;
|
|
34314
|
+
}
|
|
34315
|
+
return false;
|
|
34316
|
+
}
|
|
34317
|
+
sanitizedVarsIn(type, from, to, withHelpers) {
|
|
34318
|
+
const sanitized = new Set;
|
|
34319
|
+
for (const st of this.statements(from, to)) {
|
|
34320
|
+
const m = ASSIGN_RE.exec(st.text);
|
|
34321
|
+
if (!m)
|
|
34322
|
+
continue;
|
|
34323
|
+
const [, lhs, rhs] = m;
|
|
34324
|
+
const credited = (withHelpers ? this.sanitizes(type, rhs) : this.builtinSanitizes(type, rhs)) || [...sanitized].some((v) => mentions(rhs, v));
|
|
34325
|
+
if (credited)
|
|
34326
|
+
sanitized.add(lhs);
|
|
34327
|
+
else
|
|
34328
|
+
sanitized.delete(lhs);
|
|
34329
|
+
}
|
|
34330
|
+
return sanitized;
|
|
34331
|
+
}
|
|
34332
|
+
sanitizedVarsAt(type, sinkLine) {
|
|
34333
|
+
return this.sanitizedVarsIn(type, this.methodAt(sinkLine)?.start_line ?? 1, sinkLine, true);
|
|
34334
|
+
}
|
|
34335
|
+
rawTaintReaches(type, sinkLine, credited, seeds) {
|
|
34336
|
+
if (seeds.length === 0)
|
|
34337
|
+
return false;
|
|
34338
|
+
const method = this.methodAt(sinkLine);
|
|
34339
|
+
const from = method?.start_line ?? 1;
|
|
34340
|
+
const tainted = new Set;
|
|
34341
|
+
const stmts = this.statements(from, sinkLine);
|
|
34342
|
+
for (let i2 = 0;i2 < stmts.length; i2++) {
|
|
34343
|
+
const st = stmts[i2];
|
|
34344
|
+
const end = i2 + 1 < stmts.length ? stmts[i2 + 1].line : sinkLine;
|
|
34345
|
+
const m = ASSIGN_RE.exec(st.text);
|
|
34346
|
+
if (m) {
|
|
34347
|
+
const rhs = withoutStringLiterals(m[2]);
|
|
34348
|
+
if ([...tainted].some((v) => mentions(rhs, v)))
|
|
34349
|
+
tainted.add(m[1]);
|
|
34350
|
+
else
|
|
34351
|
+
tainted.delete(m[1]);
|
|
34352
|
+
}
|
|
34353
|
+
for (const s of seeds)
|
|
34354
|
+
if (s.line >= st.line && s.line < end)
|
|
34355
|
+
tainted.add(s.variable);
|
|
34356
|
+
}
|
|
34357
|
+
if (tainted.size === 0)
|
|
34358
|
+
return false;
|
|
34359
|
+
const call = withoutStringLiterals(this.sinkText(sinkLine));
|
|
34360
|
+
const rest = this.withoutSanitizerCalls(type, lastCallArguments(call) ?? call);
|
|
34361
|
+
if (this.builtinSanitizes(type, rest))
|
|
34362
|
+
return false;
|
|
34363
|
+
for (const v of tainted)
|
|
34364
|
+
if (!credited.has(v) && mentions(rest, v))
|
|
34365
|
+
return true;
|
|
34366
|
+
return false;
|
|
34367
|
+
}
|
|
34368
|
+
withoutSanitizerCalls(type, text) {
|
|
34369
|
+
const res = [];
|
|
34370
|
+
for (const { re, type: t } of CSHARP_SANITIZER_RES)
|
|
34371
|
+
if (t === type)
|
|
34372
|
+
res.push(re);
|
|
34373
|
+
for (const name2 of this.helpers(type))
|
|
34374
|
+
res.push(new RegExp(`(?<![\\w.])${escapeRegex(name2)}\\s*\\(`));
|
|
34375
|
+
let out2 = text;
|
|
34376
|
+
for (const re of res) {
|
|
34377
|
+
for (let guard = 0;guard < 16; guard++) {
|
|
34378
|
+
const m = re.exec(out2);
|
|
34379
|
+
if (!m)
|
|
34380
|
+
break;
|
|
34381
|
+
if (!m[0].endsWith("(")) {
|
|
34382
|
+
out2 = out2.slice(0, m.index) + out2.slice(m.index + m[0].length);
|
|
34383
|
+
continue;
|
|
34384
|
+
}
|
|
34385
|
+
const open = m.index + m[0].length - 1;
|
|
34386
|
+
const inner = insideParens(out2, open);
|
|
34387
|
+
if (inner === null)
|
|
34388
|
+
break;
|
|
34389
|
+
out2 = out2.slice(0, m.index) + out2.slice(open + inner.length + 2);
|
|
34390
|
+
}
|
|
34391
|
+
}
|
|
34392
|
+
return out2;
|
|
34393
|
+
}
|
|
34394
|
+
sinkText(line) {
|
|
34395
|
+
const text = this.lines[line - 1] ?? "";
|
|
34396
|
+
const guarded = leadingIf(text);
|
|
34397
|
+
return guarded && guarded.rest !== "" ? guarded.rest : text;
|
|
34398
|
+
}
|
|
34399
|
+
allowlistVars(cond) {
|
|
34400
|
+
const out2 = [];
|
|
34401
|
+
const anchored = new RegExp(`\\bRegex\\s*\\.\\s*IsMatch\\s*\\(\\s*(${IDENT})\\s*,\\s*@?"\\^\\[(?:[A-Za-z0-9_\\- ]|\\\\w|\\\\d)+\\](?:[+*]|\\{\\d+(?:,\\d*)?\\})\\$"`, "g");
|
|
34402
|
+
for (const m of cond.matchAll(anchored))
|
|
34403
|
+
out2.push(m[1]);
|
|
34404
|
+
const methodGroup = new RegExp(`(${IDENT})\\s*\\.\\s*All\\s*\\(\\s*char\\s*\\.\\s*Is(?:LetterOrDigit|Digit|Letter)\\s*\\)`, "g");
|
|
34405
|
+
for (const m of cond.matchAll(methodGroup))
|
|
34406
|
+
out2.push(m[1]);
|
|
34407
|
+
const lambda = new RegExp(`(${IDENT})\\s*\\.\\s*All\\s*\\(\\s*(${IDENT})\\s*=>`, "g");
|
|
34408
|
+
for (const m of cond.matchAll(lambda)) {
|
|
34409
|
+
const open = cond.indexOf("(", m.index + m[1].length);
|
|
34410
|
+
const body2 = insideParens(cond, open)?.replace(new RegExp(`^\\s*${m[2]}\\s*=>`), "") ?? "";
|
|
34411
|
+
const c = escapeRegex(m[2]);
|
|
34412
|
+
const residue = body2.replace(new RegExp(`char\\s*\\.\\s*Is(?:LetterOrDigit|Digit|Letter)\\s*\\(\\s*${c}\\s*\\)`, "g"), "").replace(new RegExp(`${c}\\s*==\\s*'[_\\-]'`, "g"), "").replace(/\|\||\s/g, "");
|
|
34413
|
+
if (residue === "")
|
|
34414
|
+
out2.push(m[1]);
|
|
34415
|
+
}
|
|
34416
|
+
return out2;
|
|
34417
|
+
}
|
|
34418
|
+
numericVars(cond, parsedFrom) {
|
|
34419
|
+
const out2 = [];
|
|
34420
|
+
const tryParse = new RegExp(`\\b(?:int|long|short|byte|uint|ulong|decimal|double|float|Int32|Int64|Guid)\\s*\\.\\s*TryParse\\s*\\(\\s*(${IDENT})\\s*,`, "g");
|
|
34421
|
+
for (const m of cond.matchAll(tryParse))
|
|
34422
|
+
out2.push(m[1]);
|
|
34423
|
+
for (const [parsed, from] of parsedFrom) {
|
|
34424
|
+
if (new RegExp(`(?<![\\w])${escapeRegex(parsed)}\\s*(?:!=\\s*null|\\.\\s*HasValue)\\b`).test(cond))
|
|
34425
|
+
out2.push(from);
|
|
34426
|
+
}
|
|
34427
|
+
return out2;
|
|
34428
|
+
}
|
|
34429
|
+
hostVars(cond, hostOf) {
|
|
34430
|
+
const out2 = [];
|
|
34431
|
+
const compares = (expr) => {
|
|
34432
|
+
const e = escapeRegex(expr).replace(/\\ /g, "\\s*");
|
|
34433
|
+
return new RegExp(`${e}\\s*==\\s*@?"[^"]+"`).test(cond) || new RegExp(`@?"[^"]+"\\s*==\\s*${e}`).test(cond) || new RegExp(`${e}\\s*\\.\\s*Equals\\s*\\(`).test(cond) || new RegExp(`\\.\\s*Contains\\s*\\(\\s*${e}\\s*[),]`).test(cond) || new RegExp(`\\bIndexOf\\s*\\([^()]*,\\s*${e}\\s*\\)\\s*>=\\s*0`).test(cond);
|
|
34434
|
+
};
|
|
34435
|
+
const direct = new RegExp(`new\\s+(?:System\\s*\\.\\s*)?Uri\\s*\\(\\s*(${IDENT})\\s*\\)\\s*\\.\\s*Host\\b`, "g");
|
|
34436
|
+
for (const m of cond.matchAll(direct)) {
|
|
34437
|
+
if (compares(m[0]))
|
|
34438
|
+
out2.push(m[1]);
|
|
34439
|
+
}
|
|
34440
|
+
for (const [hostVar, from] of hostOf) {
|
|
34441
|
+
if (mentions(cond, hostVar) && compares(hostVar))
|
|
34442
|
+
out2.push(from);
|
|
34443
|
+
}
|
|
34444
|
+
return out2;
|
|
34445
|
+
}
|
|
34446
|
+
guardHelpers(kind) {
|
|
34447
|
+
const cached = this.guardHelperCache.get(kind);
|
|
34448
|
+
if (cached)
|
|
34449
|
+
return cached;
|
|
34450
|
+
const map = new Map;
|
|
34451
|
+
this.guardHelperCache.set(kind, map);
|
|
34452
|
+
for (const m of this.methods) {
|
|
34453
|
+
const signature = this.lines[m.start_line - 1] ?? "";
|
|
34454
|
+
const open = signature.indexOf("(", signature.indexOf(m.name));
|
|
34455
|
+
const params = open === -1 ? null : insideParens(signature, open);
|
|
34456
|
+
if (!params)
|
|
34457
|
+
continue;
|
|
34458
|
+
const names = params.split(",").map((p) => p.trim().split(/\s+/).pop() ?? "");
|
|
34459
|
+
const returns = this.returnExprs(m);
|
|
34460
|
+
if (returns.length !== 1)
|
|
34461
|
+
continue;
|
|
34462
|
+
const proven = this.provenBy(kind, returns[0].expr, new Map, new Map, false);
|
|
34463
|
+
const pos = names.findIndex((n) => proven.includes(n));
|
|
34464
|
+
if (pos !== -1)
|
|
34465
|
+
map.set(m.name, pos);
|
|
34466
|
+
}
|
|
34467
|
+
return map;
|
|
34468
|
+
}
|
|
34469
|
+
provenBy(kind, cond, parsedFrom, hostOf, withHelpers) {
|
|
34470
|
+
const out2 = kind === "allowlist" ? this.allowlistVars(cond) : kind === "numeric" ? this.numericVars(cond, parsedFrom) : this.hostVars(cond, hostOf);
|
|
34471
|
+
if (withHelpers) {
|
|
34472
|
+
for (const [name2, pos] of this.guardHelpers(kind)) {
|
|
34473
|
+
for (const call of cond.matchAll(new RegExp(`(?<![\\w.])${escapeRegex(name2)}\\s*\\(`, "g"))) {
|
|
34474
|
+
const args2 = insideParens(cond, call.index + call[0].length - 1)?.split(",") ?? [];
|
|
34475
|
+
const arg = args2[pos]?.trim();
|
|
34476
|
+
if (arg && new RegExp(`^${IDENT}$`).test(arg))
|
|
34477
|
+
out2.push(arg);
|
|
34478
|
+
}
|
|
34479
|
+
}
|
|
34480
|
+
}
|
|
34481
|
+
return out2;
|
|
34482
|
+
}
|
|
34483
|
+
guardedVarsAt(type, sinkLine) {
|
|
34484
|
+
const kinds = [];
|
|
34485
|
+
if (INERT_WHEN_ALPHANUMERIC.has(type))
|
|
34486
|
+
kinds.push("allowlist", "numeric");
|
|
34487
|
+
if (type === "ssrf")
|
|
34488
|
+
kinds.push("host");
|
|
34489
|
+
const proven = new Set;
|
|
34490
|
+
if (kinds.length === 0)
|
|
34491
|
+
return proven;
|
|
34492
|
+
const scopeStart = this.methodAt(sinkLine)?.start_line ?? 1;
|
|
34493
|
+
const before = this.statements(scopeStart, sinkLine);
|
|
34494
|
+
const boolOf = new Map;
|
|
34495
|
+
const parsedFrom = new Map;
|
|
34496
|
+
const hostOf = new Map;
|
|
34497
|
+
const parseAnywhere = new RegExp(`(${IDENT})\\s*=\\s*(?:\\([^)]*\\)\\s*)?(?:int|long|short|byte|uint|ulong|decimal|double|float|Int32|Int64)\\s*\\.\\s*Parse\\s*\\(\\s*(${IDENT})\\s*[,)]`, "g");
|
|
34498
|
+
for (const st of before) {
|
|
34499
|
+
for (const p of st.text.matchAll(parseAnywhere))
|
|
34500
|
+
parsedFrom.set(p[1], p[2]);
|
|
34501
|
+
const m = ASSIGN_RE.exec(st.text);
|
|
34502
|
+
if (!m)
|
|
34503
|
+
continue;
|
|
34504
|
+
const [, lhs, rhs] = m;
|
|
34505
|
+
const parse2 = new RegExp(`^(?:\\([^)]*\\)\\s*)?(?:int|long|short|byte|uint|ulong|decimal|double|float|Int32|Int64)\\s*\\.\\s*Parse\\s*\\(\\s*(${IDENT})\\s*[,)]`).exec(rhs);
|
|
34506
|
+
if (parse2)
|
|
34507
|
+
parsedFrom.set(lhs, parse2[1]);
|
|
34508
|
+
const host = new RegExp(`^new\\s+(?:System\\s*\\.\\s*)?Uri\\s*\\(\\s*(${IDENT})\\s*\\)\\s*\\.\\s*Host$`).exec(rhs.trim());
|
|
34509
|
+
if (host)
|
|
34510
|
+
hostOf.set(lhs, host[1]);
|
|
34511
|
+
boolOf.set(lhs, rhs);
|
|
34512
|
+
}
|
|
34513
|
+
const credit = (cond) => {
|
|
34514
|
+
const resolved = new RegExp(`^${IDENT}$`).test(cond) ? boolOf.get(cond) ?? cond : cond;
|
|
34515
|
+
for (const kind of kinds) {
|
|
34516
|
+
for (const v of this.provenBy(kind, resolved, parsedFrom, hostOf, true))
|
|
34517
|
+
proven.add(v);
|
|
34518
|
+
}
|
|
34519
|
+
};
|
|
34520
|
+
const own = leadingIf(this.lines[sinkLine - 1] ?? "");
|
|
34521
|
+
if (own && !own.cond.startsWith("!"))
|
|
34522
|
+
credit(own.cond);
|
|
34523
|
+
let depth = 0;
|
|
34524
|
+
for (let i2 = sinkLine - 1;i2 >= scopeStart; i2--) {
|
|
34525
|
+
const text = this.lines[i2 - 1] ?? "";
|
|
34526
|
+
for (let c = text.length - 1;c >= 0; c--) {
|
|
34527
|
+
if (text[c] === "}")
|
|
34528
|
+
depth++;
|
|
34529
|
+
else if (text[c] === "{") {
|
|
34530
|
+
if (depth > 0) {
|
|
34531
|
+
depth--;
|
|
34532
|
+
continue;
|
|
34533
|
+
}
|
|
34534
|
+
let header = text.slice(0, c);
|
|
34535
|
+
for (let h = i2 - 1;header.trim() === "" && h >= scopeStart; h--)
|
|
34536
|
+
header = this.lines[h - 1] ?? "";
|
|
34537
|
+
const enclosing = leadingIf(header);
|
|
34538
|
+
if (enclosing && !enclosing.cond.startsWith("!"))
|
|
34539
|
+
credit(enclosing.cond);
|
|
34540
|
+
}
|
|
34541
|
+
}
|
|
34542
|
+
}
|
|
34543
|
+
for (let i2 = scopeStart;i2 < sinkLine; i2++) {
|
|
34544
|
+
const guard = leadingIf(this.lines[i2 - 1] ?? "");
|
|
34545
|
+
if (!guard)
|
|
34546
|
+
continue;
|
|
34547
|
+
let rest = guard.rest;
|
|
34548
|
+
for (let n = i2 + 1;rest === "" && n < sinkLine; n++)
|
|
34549
|
+
rest = (this.lines[n - 1] ?? "").trim();
|
|
34550
|
+
if (!EXITS_RE.test(rest))
|
|
34551
|
+
continue;
|
|
34552
|
+
if (guard.cond.startsWith("!")) {
|
|
34553
|
+
const inner = guard.cond.slice(1).trim();
|
|
34554
|
+
const wrapped = inner.startsWith("(") && insideParens(inner, 0)?.length === inner.length - 2;
|
|
34555
|
+
credit(wrapped ? inner.slice(1, -1) : inner);
|
|
34556
|
+
} else if (/!=/.test(guard.cond) && !/[|&]/.test(guard.cond))
|
|
34557
|
+
credit(guard.cond.replace("!=", "=="));
|
|
34558
|
+
}
|
|
34559
|
+
return proven;
|
|
34560
|
+
}
|
|
34561
|
+
safeBuilders(credited, sinkLine) {
|
|
34562
|
+
const scopeStart = this.methodAt(sinkLine)?.start_line ?? 1;
|
|
34563
|
+
const onlySafe = (expr) => {
|
|
34564
|
+
const idents = expr.replace(/\$?@?"(?:[^"\\]|\\.)*"/g, " ").replace(/'(?:[^'\\]|\\.)'/g, " ").replace(/\.\s*[A-Za-z_]\w*\s*\(\s*\)/g, " ").match(/[A-Za-z_]\w*/g) ?? [];
|
|
34565
|
+
return idents.every((id) => credited.has(id));
|
|
34566
|
+
};
|
|
34567
|
+
const state = new Map;
|
|
34568
|
+
const declRe = new RegExp(`\\b(?:StringBuilder|var)\\s+(${IDENT})\\s*=\\s*new\\s+(?:System\\s*\\.\\s*Text\\s*\\.\\s*)?StringBuilder\\s*\\(`);
|
|
34569
|
+
const writeRe = new RegExp(`^\\s*(${IDENT})\\s*\\.\\s*(?:Append|AppendLine|AppendFormat|Insert)\\s*\\(`);
|
|
34570
|
+
for (const st of this.statements(scopeStart, sinkLine)) {
|
|
34571
|
+
const decl = declRe.exec(st.text);
|
|
34572
|
+
if (decl) {
|
|
34573
|
+
const args2 = insideParens(st.text, decl.index + decl[0].length - 1) ?? "";
|
|
34574
|
+
state.set(decl[1], onlySafe(args2));
|
|
34575
|
+
continue;
|
|
34576
|
+
}
|
|
34577
|
+
const write = writeRe.exec(st.text);
|
|
34578
|
+
if (write && state.has(write[1])) {
|
|
34579
|
+
const args2 = insideParens(st.text, write[0].length - 1);
|
|
34580
|
+
if (args2 === null || !onlySafe(args2))
|
|
34581
|
+
state.set(write[1], false);
|
|
34582
|
+
}
|
|
34583
|
+
}
|
|
34584
|
+
return new Set([...state].filter(([, safe]) => safe).map(([name2]) => name2));
|
|
34585
|
+
}
|
|
34586
|
+
creditedOnLine(line, seeds = []) {
|
|
34587
|
+
const text = this.sinkText(line);
|
|
34588
|
+
const types = new Set(CSHARP_SANITIZER_RES.map((r) => r.type));
|
|
34589
|
+
for (const t of INERT_WHEN_ALPHANUMERIC)
|
|
34590
|
+
types.add(t);
|
|
34591
|
+
types.add("ssrf");
|
|
34592
|
+
for (const type of types) {
|
|
34593
|
+
const credited = new Set([...this.sanitizedVarsAt(type, line), ...this.guardedVarsAt(type, line)]);
|
|
34594
|
+
for (const v of this.safeBuilders(credited, line))
|
|
34595
|
+
credited.add(v);
|
|
34596
|
+
const covered = this.sanitizes(type, text) || [...credited].some((v) => mentions(text, v));
|
|
34597
|
+
if (covered && !this.rawTaintReaches(type, line, credited, seeds))
|
|
34598
|
+
return true;
|
|
34599
|
+
}
|
|
34600
|
+
return this.fixedHostRequest(line);
|
|
34601
|
+
}
|
|
34602
|
+
fixedHostRequest(sinkLine) {
|
|
34603
|
+
const fixedLiteral = '\\$?@?"https?:\\/\\/[A-Za-z0-9.\\-]+(?::\\d+)?[\\/?#][^"]*"';
|
|
34604
|
+
const text = this.lines[sinkLine - 1] ?? "";
|
|
34605
|
+
const argStart = `\\(\\s*(?:new\\s+(?:System\\s*\\.\\s*)?Uri\\s*\\(\\s*)?`;
|
|
34606
|
+
if (new RegExp(argStart + fixedLiteral).test(text))
|
|
34607
|
+
return true;
|
|
34608
|
+
const scopeStart = this.methodAt(sinkLine)?.start_line ?? 1;
|
|
34609
|
+
const fixed = new Set;
|
|
34610
|
+
for (const st of this.statements(scopeStart, sinkLine)) {
|
|
34611
|
+
const m = ASSIGN_RE.exec(st.text);
|
|
34612
|
+
if (!m)
|
|
34613
|
+
continue;
|
|
34614
|
+
if (new RegExp("^(?:new\\s+(?:System\\s*\\.\\s*)?Uri\\s*\\(\\s*)?" + fixedLiteral).test(m[2].trim()))
|
|
34615
|
+
fixed.add(m[1]);
|
|
34616
|
+
else
|
|
34617
|
+
fixed.delete(m[1]);
|
|
34618
|
+
}
|
|
34619
|
+
for (const v of fixed) {
|
|
34620
|
+
if (new RegExp(argStart + escapeRegex(v) + "\\s*[,)]").test(text))
|
|
34621
|
+
return true;
|
|
34622
|
+
}
|
|
34623
|
+
return false;
|
|
34624
|
+
}
|
|
34625
|
+
}
|
|
34626
|
+
|
|
34065
34627
|
// ../circle-ir/dist/analysis/passes/sink-filter-pass.js
|
|
34066
34628
|
function escapeReSf(s) {
|
|
34067
34629
|
return s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
@@ -34364,7 +34926,6 @@ function jsSsrfHostGuardedLines(code) {
|
|
|
34364
34926
|
}
|
|
34365
34927
|
return covered;
|
|
34366
34928
|
}
|
|
34367
|
-
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*\)/;
|
|
34368
34929
|
function isUnwrittenLocalMapKeyRead(code, source) {
|
|
34369
34930
|
if (source.type !== "plugin_param")
|
|
34370
34931
|
return false;
|
|
@@ -34391,15 +34952,6 @@ function isUnwrittenLocalMapKeyRead(code, source) {
|
|
|
34391
34952
|
return false;
|
|
34392
34953
|
return true;
|
|
34393
34954
|
}
|
|
34394
|
-
var CSHARP_SANITIZER_RES = [
|
|
34395
|
-
{ re: /\b(?:HtmlEncode|JavaScriptStringEncode)\s*\(/, type: "xss" },
|
|
34396
|
-
{ re: /\bHtmlEncoder\s*\.\s*Encode\s*\(/, type: "xss" },
|
|
34397
|
-
{ re: /\bPath\s*\.\s*GetFileName\s*\(/, type: "path_traversal" },
|
|
34398
|
-
{ re: CSHARP_ALLOWLIST_STRIP_RE, type: "ldap_injection" },
|
|
34399
|
-
{ re: CSHARP_ALLOWLIST_STRIP_RE, type: "xpath_injection" },
|
|
34400
|
-
{ re: CSHARP_ALLOWLIST_STRIP_RE, type: "command_injection" },
|
|
34401
|
-
{ re: CSHARP_ALLOWLIST_STRIP_RE, type: "sql_injection" }
|
|
34402
|
-
];
|
|
34403
34955
|
function csharpSanitizedVarsByType(code) {
|
|
34404
34956
|
const lines = code.split(`
|
|
34405
34957
|
`);
|
|
@@ -34432,7 +34984,7 @@ function csharpSanitizedVarsByType(code) {
|
|
|
34432
34984
|
for (const [type, set] of byType) {
|
|
34433
34985
|
if (set.has(lhs))
|
|
34434
34986
|
continue;
|
|
34435
|
-
const refsSanitized = [...set].some((v) => new RegExp(`(?<![\\w])${
|
|
34987
|
+
const refsSanitized = [...set].some((v) => new RegExp(`(?<![\\w])${escapeRegex2(v)}(?![\\w])`).test(rhs));
|
|
34436
34988
|
if (refsSanitized) {
|
|
34437
34989
|
set.add(lhs);
|
|
34438
34990
|
changed = true;
|
|
@@ -35328,30 +35880,39 @@ class SinkFilterPass {
|
|
|
35328
35880
|
const sourceLines = ctx.code.split(`
|
|
35329
35881
|
`);
|
|
35330
35882
|
const sanitizedByType = csharpSanitizedVarsByType(ctx.code);
|
|
35883
|
+
const csFields = new Set(ctx.graph.ir.types.flatMap((t) => (t.fields ?? []).map((f) => f.name)));
|
|
35884
|
+
const credit = new CSharpCredit(ctx.code, ctx.graph.ir.types.flatMap((t) => t.methods));
|
|
35885
|
+
const csSeeds = mergedSources.flatMap((s) => s.variable ? [{ variable: s.variable, line: s.line }] : []);
|
|
35331
35886
|
const xxeHardened = /\bXmlResolver\s*=\s*null\b/.test(ctx.code) || /\bDtdProcessing\s*=\s*(?:DtdProcessing\s*\.\s*)?(?:Prohibit|Ignore)\b/.test(ctx.code);
|
|
35332
35887
|
filtered = filtered.filter((sink) => {
|
|
35333
35888
|
if (sink.type === "xxe" && xxeHardened)
|
|
35334
35889
|
return false;
|
|
35335
35890
|
const sinkLineText = sourceLines[sink.line - 1] ?? "";
|
|
35336
|
-
|
|
35337
|
-
|
|
35338
|
-
|
|
35339
|
-
|
|
35340
|
-
const
|
|
35341
|
-
|
|
35342
|
-
|
|
35343
|
-
|
|
35344
|
-
|
|
35345
|
-
|
|
35346
|
-
}
|
|
35347
|
-
|
|
35891
|
+
const inlineSanitized = credit.sanitizes(sink.type, sinkLineText);
|
|
35892
|
+
if (sink.type === "ssrf" && credit.fixedHostRequest(sink.line))
|
|
35893
|
+
return false;
|
|
35894
|
+
const scoped = credit.sanitizedVarsAt(sink.type, sink.line);
|
|
35895
|
+
const fileWide = sanitizedByType.get(sink.type);
|
|
35896
|
+
const sanitizedVars = new Set(scoped);
|
|
35897
|
+
if (fileWide) {
|
|
35898
|
+
for (const v of fileWide)
|
|
35899
|
+
if (csFields.has(v))
|
|
35900
|
+
sanitizedVars.add(v);
|
|
35901
|
+
}
|
|
35902
|
+
for (const v of credit.guardedVarsAt(sink.type, sink.line))
|
|
35903
|
+
sanitizedVars.add(v);
|
|
35904
|
+
for (const v of credit.safeBuilders(sanitizedVars, sink.line))
|
|
35905
|
+
sanitizedVars.add(v);
|
|
35906
|
+
const received = credit.sinkText(sink.line);
|
|
35907
|
+
const covered = inlineSanitized || [...sanitizedVars].some((v) => new RegExp(`(?<![\\w])${escapeRegex2(v)}(?![\\w])`).test(received));
|
|
35908
|
+
return !covered || credit.rawTaintReaches(sink.type, sink.line, sanitizedVars, csSeeds);
|
|
35348
35909
|
});
|
|
35349
35910
|
}
|
|
35350
35911
|
if (language === "csharp") {
|
|
35351
35912
|
const sourceLines = ctx.code.split(`
|
|
35352
35913
|
`);
|
|
35353
35914
|
const constBaseClients = new Set;
|
|
35354
|
-
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*\)/;
|
|
35915
|
+
const declRe = /\b(?:var|HttpClient)\s+([A-Za-z_]\w*)\s*=\s*new\s+HttpClient\s*(?:\([^)]*\)\s*)?\{[^}]*\bBaseAddress\s*=\s*new\s+(?:System\s*\.\s*)?Uri\s*\(\s*(?:@?"[^"]*"|\$?"[^"{}]*")\s*\)/;
|
|
35355
35916
|
for (const line of sourceLines) {
|
|
35356
35917
|
const m = declRe.exec(line);
|
|
35357
35918
|
if (m)
|
|
@@ -35365,14 +35926,22 @@ class SinkFilterPass {
|
|
|
35365
35926
|
const text = sourceLines[sink.line - 1] ?? "";
|
|
35366
35927
|
let onConstClient = false;
|
|
35367
35928
|
for (const v of constBaseClients) {
|
|
35368
|
-
if (new RegExp(`(?<![\\w])${
|
|
35929
|
+
if (new RegExp(`(?<![\\w])${escapeRegex2(v)}\\s*\\.`).test(text)) {
|
|
35369
35930
|
onConstClient = true;
|
|
35370
35931
|
break;
|
|
35371
35932
|
}
|
|
35372
35933
|
}
|
|
35373
35934
|
if (!onConstClient)
|
|
35374
35935
|
return true;
|
|
35375
|
-
|
|
35936
|
+
let m = relativeLiteralArgRe.exec(text);
|
|
35937
|
+
if (!m) {
|
|
35938
|
+
const arg = /Async\s*\(\s*([A-Za-z_]\w*)\s*[,)]/.exec(text)?.[1];
|
|
35939
|
+
if (arg) {
|
|
35940
|
+
const assigned = new RegExp(`\\b${escapeRegex2(arg)}\\s*=\\s*(?:@?\\$?)"([^"]*)"`);
|
|
35941
|
+
for (let i2 = sink.line - 2;i2 >= 0 && !m; i2--)
|
|
35942
|
+
m = assigned.exec(sourceLines[i2] ?? "");
|
|
35943
|
+
}
|
|
35944
|
+
}
|
|
35376
35945
|
if (!m)
|
|
35377
35946
|
return true;
|
|
35378
35947
|
const lit = m[1];
|
|
@@ -35408,7 +35977,7 @@ class SinkFilterPass {
|
|
|
35408
35977
|
const wrappers = findPythonLdapStripWrappers(sourceLines);
|
|
35409
35978
|
const allLdapSanitizers = [...PY_BUILTIN_LDAP_SANITIZERS, ...wrappers];
|
|
35410
35979
|
if (allLdapSanitizers.length > 0) {
|
|
35411
|
-
const ldapPatterns = allLdapSanitizers.map((name2) => new RegExp(`\\b${
|
|
35980
|
+
const ldapPatterns = allLdapSanitizers.map((name2) => new RegExp(`\\b${escapeRegex2(name2)}\\s*\\(`));
|
|
35412
35981
|
filtered = filtered.filter((sink) => {
|
|
35413
35982
|
if (sink.type !== "ldap_injection")
|
|
35414
35983
|
return true;
|
|
@@ -35494,7 +36063,7 @@ class SinkFilterPass {
|
|
|
35494
36063
|
return { sources, sinks: filtered, sanitizers };
|
|
35495
36064
|
}
|
|
35496
36065
|
}
|
|
35497
|
-
function
|
|
36066
|
+
function escapeRegex2(s) {
|
|
35498
36067
|
return s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
35499
36068
|
}
|
|
35500
36069
|
var RESERVED_KEYWORDS = new Set([
|
|
@@ -35576,7 +36145,7 @@ function extractSourceIdentifiers(text) {
|
|
|
35576
36145
|
return out2;
|
|
35577
36146
|
}
|
|
35578
36147
|
function isAssignedFromSanitizerPattern(sourceLines, sinkLine, varName, sanitizerPatterns, lookback = 30) {
|
|
35579
|
-
const escapedVar =
|
|
36148
|
+
const escapedVar = escapeRegex2(varName);
|
|
35580
36149
|
const assignRe = new RegExp(`(?:\\b(?:const|let|var|final|String)\\s+)?\\b${escapedVar}\\s*=\\s*(.+)`);
|
|
35581
36150
|
const fnBoundaryRe = /^\s*(?:def\s+\w+\s*\(|function\s+\w+\s*\(|\w+\s*[:=]\s*(?:async\s+)?\(?[^)]*\)?\s*=>)/;
|
|
35582
36151
|
const lo = Math.max(0, sinkLine - 1 - lookback);
|
|
@@ -37145,7 +37714,7 @@ class LibraryProfileXssGatePass {
|
|
|
37145
37714
|
|
|
37146
37715
|
// ../circle-ir/dist/analysis/passes/taint-propagation-pass.js
|
|
37147
37716
|
var CONTAINER_READ_SOURCES = new Set(["plugin_param", "config_param"]);
|
|
37148
|
-
function makeConstPropVeto(constProp, graph, sources, code) {
|
|
37717
|
+
function makeConstPropVeto(constProp, graph, sources, code, language) {
|
|
37149
37718
|
const lines = typeof code === "string" ? code.split(`
|
|
37150
37719
|
`) : [];
|
|
37151
37720
|
const selfReassign = new Map;
|
|
@@ -37182,6 +37751,8 @@ function makeConstPropVeto(constProp, graph, sources, code) {
|
|
|
37182
37751
|
return false;
|
|
37183
37752
|
if (r.reason !== "variable_not_tainted")
|
|
37184
37753
|
return true;
|
|
37754
|
+
if (language === "csharp")
|
|
37755
|
+
return false;
|
|
37185
37756
|
const lo = Math.min(sourceLine, sinkLine);
|
|
37186
37757
|
let d = defAt(step.variable, step.line);
|
|
37187
37758
|
let selfHops = 0;
|
|
@@ -37220,7 +37791,7 @@ class TaintPropagationPass {
|
|
|
37220
37791
|
return { flows: [] };
|
|
37221
37792
|
}
|
|
37222
37793
|
const propagationResult = propagateTaint(graph, sources, sinks, sanitizers);
|
|
37223
|
-
const isVetoed = makeConstPropVeto(constProp, graph, sources, ctx.code);
|
|
37794
|
+
const isVetoed = makeConstPropVeto(constProp, graph, sources, ctx.code, ctx.language);
|
|
37224
37795
|
const verifiedFlows = propagationResult.flows.filter((flow) => {
|
|
37225
37796
|
if (constProp.unreachableLines.has(flow.sink.line))
|
|
37226
37797
|
return false;
|
|
@@ -37790,7 +38361,7 @@ function detectCollectionFlows(calls, sources, sinks, taintedVars, unreachableLi
|
|
|
37790
38361
|
if (source.variable && source.variable !== varName && source.in_method && call.in_method && source.in_method !== call.in_method) {
|
|
37791
38362
|
continue;
|
|
37792
38363
|
}
|
|
37793
|
-
if (typeof code === "string" && isReassignedToLiteralBetween(code, varName, source.line, sink.line)) {
|
|
38364
|
+
if (typeof code === "string" && isReassignedToLiteralBetween(code, varName, source.line, sink.line, unreachableLines)) {
|
|
37794
38365
|
continue;
|
|
37795
38366
|
}
|
|
37796
38367
|
flows.push({
|
|
@@ -37830,7 +38401,7 @@ function detectCollectionFlows(calls, sources, sinks, taintedVars, unreachableLi
|
|
|
37830
38401
|
if (source.variable && source.variable !== collectionVar && source.in_method && call.in_method && source.in_method !== call.in_method) {
|
|
37831
38402
|
continue;
|
|
37832
38403
|
}
|
|
37833
|
-
if (typeof code === "string" && isReassignedToLiteralBetween(code, collectionVar, source.line, sink.line)) {
|
|
38404
|
+
if (typeof code === "string" && isReassignedToLiteralBetween(code, collectionVar, source.line, sink.line, unreachableLines)) {
|
|
37834
38405
|
continue;
|
|
37835
38406
|
}
|
|
37836
38407
|
flows.push({
|
|
@@ -37949,7 +38520,7 @@ function detectParameterSinkFlows(types, calls, sources, sinks, unreachableLines
|
|
|
37949
38520
|
if (paramSource) {
|
|
37950
38521
|
const exists = flows.some((f) => f.source_line === paramSource.line && f.sink_line === sink.line && f.sink_type === sink.type);
|
|
37951
38522
|
if (!exists) {
|
|
37952
|
-
if (typeof code === "string" && isReassignedToLiteralBetween(code, arg.variable, paramSource.line, sink.line)) {
|
|
38523
|
+
if (typeof code === "string" && isReassignedToLiteralBetween(code, arg.variable, paramSource.line, sink.line, unreachableLines)) {
|
|
37953
38524
|
continue;
|
|
37954
38525
|
}
|
|
37955
38526
|
flows.push({
|
|
@@ -37972,6 +38543,96 @@ function detectParameterSinkFlows(types, calls, sources, sinks, unreachableLines
|
|
|
37972
38543
|
}
|
|
37973
38544
|
return flows;
|
|
37974
38545
|
}
|
|
38546
|
+
function csharpMethodScopedAliases(code, seeds, methods, fieldNames) {
|
|
38547
|
+
const lines = code.split(`
|
|
38548
|
+
`);
|
|
38549
|
+
const methodOf = (line) => {
|
|
38550
|
+
let best = -1;
|
|
38551
|
+
let span = Infinity;
|
|
38552
|
+
methods.forEach((m, i2) => {
|
|
38553
|
+
if (line >= m.start_line && line <= m.end_line && m.end_line - m.start_line < span) {
|
|
38554
|
+
best = i2;
|
|
38555
|
+
span = m.end_line - m.start_line;
|
|
38556
|
+
}
|
|
38557
|
+
});
|
|
38558
|
+
return best;
|
|
38559
|
+
};
|
|
38560
|
+
const declRe = /^\s*(?:public|private|protected|internal|static|readonly|const|\s)*\s*(?:[A-Za-z_][\w.]*(?:\s*<[^>]*>)?(?:\s*\[\s*\])*\??)\s+([A-Za-z_]\w*)\s*=\s*(.+?);\s*$/;
|
|
38561
|
+
const assignRe = /^\s*(?:this\s*\.\s*)?([A-Za-z_]\w*)\s*=\s*(.+?);\s*$/;
|
|
38562
|
+
const returnRe = /^\s*return\s+(.+?);\s*$/;
|
|
38563
|
+
const KEYWORDS = new Set(["return", "throw", "new", "await", "yield", "else", "case", "using", "var"]);
|
|
38564
|
+
const codeOf = (rhs) => rhs.replace(/\$@?"(?:[^"\\]|\\.)*"/g, (lit) => " " + [...lit.matchAll(/\{([^}]*)\}/g)].map((x) => x[1]).join(" ") + " ").replace(/@?"(?:[^"\\]|\\.)*"/g, " ").replace(/'(?:[^'\\]|\\.)'/g, " ");
|
|
38565
|
+
const local = new Map;
|
|
38566
|
+
const taintedFields = new Set;
|
|
38567
|
+
const returnsTainted = new Set;
|
|
38568
|
+
const localsOf = (m) => {
|
|
38569
|
+
let set = local.get(m);
|
|
38570
|
+
if (!set) {
|
|
38571
|
+
set = new Set;
|
|
38572
|
+
local.set(m, set);
|
|
38573
|
+
}
|
|
38574
|
+
return set;
|
|
38575
|
+
};
|
|
38576
|
+
for (const seed of seeds) {
|
|
38577
|
+
const base = seed.variable.replace(/^this\./, "");
|
|
38578
|
+
if (fieldNames.has(base))
|
|
38579
|
+
taintedFields.add(base);
|
|
38580
|
+
else
|
|
38581
|
+
localsOf(methodOf(seed.line)).add(seed.variable);
|
|
38582
|
+
}
|
|
38583
|
+
const out2 = [];
|
|
38584
|
+
let changed = true;
|
|
38585
|
+
for (let guard = 0;changed && guard < lines.length + 2; guard++) {
|
|
38586
|
+
changed = false;
|
|
38587
|
+
for (let i2 = 0;i2 < lines.length; i2++) {
|
|
38588
|
+
let text = lines[i2];
|
|
38589
|
+
const trimmed = text.trimStart();
|
|
38590
|
+
if (trimmed.startsWith("//") || trimmed.startsWith("*"))
|
|
38591
|
+
continue;
|
|
38592
|
+
if (/^\s*(?:[\w.<>\[\]?]+\s+)?[A-Za-z_]\w*\s*=(?!=)/.test(text) && !/;\s*(?:\/\/.*)?$/.test(text)) {
|
|
38593
|
+
for (let j = i2 + 1;j < lines.length && j <= i2 + 12; j++) {
|
|
38594
|
+
text = text.trimEnd() + " " + lines[j].trim();
|
|
38595
|
+
if (/;\s*(?:\/\/.*)?$/.test(lines[j]))
|
|
38596
|
+
break;
|
|
38597
|
+
}
|
|
38598
|
+
}
|
|
38599
|
+
const m = methodOf(i2 + 1);
|
|
38600
|
+
const here = localsOf(m);
|
|
38601
|
+
const ret = returnRe.exec(text);
|
|
38602
|
+
if (ret && m !== -1 && !returnsTainted.has(methods[m].name)) {
|
|
38603
|
+
const idents2 = codeOf(ret[1]).match(/[\p{L}\p{N}_]+/gu) ?? [];
|
|
38604
|
+
if (idents2.some((t) => here.has(t) || taintedFields.has(t))) {
|
|
38605
|
+
returnsTainted.add(methods[m].name);
|
|
38606
|
+
changed = true;
|
|
38607
|
+
}
|
|
38608
|
+
continue;
|
|
38609
|
+
}
|
|
38610
|
+
const d = declRe.exec(text) ?? assignRe.exec(text);
|
|
38611
|
+
if (!d)
|
|
38612
|
+
continue;
|
|
38613
|
+
const [, lhs, rhs] = d;
|
|
38614
|
+
if (KEYWORDS.has(lhs))
|
|
38615
|
+
continue;
|
|
38616
|
+
const isField = fieldNames.has(lhs) && !declRe.test(text);
|
|
38617
|
+
if (isField ? taintedFields.has(lhs) : here.has(lhs))
|
|
38618
|
+
continue;
|
|
38619
|
+
const rhsCode = codeOf(rhs);
|
|
38620
|
+
const idents = rhsCode.match(/[\p{L}\p{N}_]+/gu) ?? [];
|
|
38621
|
+
const viaLocal = idents.some((t) => here.has(t));
|
|
38622
|
+
const viaField = idents.some((t) => taintedFields.has(t) && !here.has(t));
|
|
38623
|
+
const viaCall = [...rhsCode.matchAll(/([\p{L}_][\p{L}\p{N}_]*)\s*\(/gu)].some((c) => returnsTainted.has(c[1]));
|
|
38624
|
+
if (!viaLocal && !viaField && !viaCall)
|
|
38625
|
+
continue;
|
|
38626
|
+
if (isField)
|
|
38627
|
+
taintedFields.add(lhs);
|
|
38628
|
+
else
|
|
38629
|
+
here.add(lhs);
|
|
38630
|
+
out2.push({ variable: lhs, line: i2 + 1, carried: !viaLocal });
|
|
38631
|
+
changed = true;
|
|
38632
|
+
}
|
|
38633
|
+
}
|
|
38634
|
+
return out2;
|
|
38635
|
+
}
|
|
37975
38636
|
var _reassignSplitCode;
|
|
37976
38637
|
var _reassignSplitLines;
|
|
37977
38638
|
function splitLinesCached(code) {
|
|
@@ -37982,7 +38643,7 @@ function splitLinesCached(code) {
|
|
|
37982
38643
|
`);
|
|
37983
38644
|
return _reassignSplitLines;
|
|
37984
38645
|
}
|
|
37985
|
-
function isReassignedToLiteralBetween(code, variable, srcLine, sinkLine) {
|
|
38646
|
+
function isReassignedToLiteralBetween(code, variable, srcLine, sinkLine, unreachableLines) {
|
|
37986
38647
|
if (!variable || sinkLine - srcLine < 2)
|
|
37987
38648
|
return false;
|
|
37988
38649
|
if (!/^[A-Za-z_][\w]*$/.test(variable))
|
|
@@ -38002,49 +38663,117 @@ function isReassignedToLiteralBetween(code, variable, srcLine, sinkLine) {
|
|
|
38002
38663
|
return true;
|
|
38003
38664
|
if (!reNaked.test(line) && !reSwitchCase.test(line))
|
|
38004
38665
|
continue;
|
|
38005
|
-
if (
|
|
38666
|
+
if (siblingBranchAssignsNonLiteral(lines, i2, variable, strLit, unreachableLines))
|
|
38006
38667
|
continue;
|
|
38007
38668
|
return true;
|
|
38008
38669
|
}
|
|
38009
38670
|
return false;
|
|
38010
38671
|
}
|
|
38011
|
-
function
|
|
38672
|
+
function enclosingBrace(lines, line, col) {
|
|
38012
38673
|
let depth = 0;
|
|
38013
|
-
let
|
|
38014
|
-
|
|
38015
|
-
|
|
38016
|
-
|
|
38017
|
-
if (line[c] === "}")
|
|
38674
|
+
for (let i2 = line;i2 >= 0; i2--) {
|
|
38675
|
+
const text = lines[i2];
|
|
38676
|
+
for (let c = i2 === line ? Math.min(col, text.length) - 1 : text.length - 1;c >= 0; c--) {
|
|
38677
|
+
if (text[c] === "}")
|
|
38018
38678
|
depth++;
|
|
38019
|
-
else if (
|
|
38679
|
+
else if (text[c] === "{") {
|
|
38680
|
+
if (depth === 0) {
|
|
38681
|
+
let header = text.slice(0, c).trim();
|
|
38682
|
+
let headerLine = i2;
|
|
38683
|
+
for (let h = i2 - 1;header === "" && h >= 0; h--) {
|
|
38684
|
+
header = lines[h].trim();
|
|
38685
|
+
headerLine = h;
|
|
38686
|
+
}
|
|
38687
|
+
return { line: i2, col: c, header, headerLine };
|
|
38688
|
+
}
|
|
38020
38689
|
depth--;
|
|
38690
|
+
}
|
|
38021
38691
|
}
|
|
38022
|
-
|
|
38023
|
-
|
|
38024
|
-
|
|
38025
|
-
|
|
38692
|
+
}
|
|
38693
|
+
return null;
|
|
38694
|
+
}
|
|
38695
|
+
function matchingClose(lines, line, col) {
|
|
38696
|
+
let depth = 0;
|
|
38697
|
+
for (let i2 = line;i2 < lines.length; i2++) {
|
|
38698
|
+
const text = lines[i2];
|
|
38699
|
+
for (let c = i2 === line ? col : 0;c < text.length; c++) {
|
|
38700
|
+
if (text[c] === "{")
|
|
38701
|
+
depth++;
|
|
38702
|
+
else if (text[c] === "}" && --depth === 0)
|
|
38703
|
+
return { line: i2, col: c };
|
|
38026
38704
|
}
|
|
38027
38705
|
}
|
|
38028
|
-
|
|
38706
|
+
return null;
|
|
38707
|
+
}
|
|
38708
|
+
function siblingBranchAssignsNonLiteral(lines, idx, variable, strLit, unreachableLines) {
|
|
38709
|
+
let block = enclosingBrace(lines, idx, 0);
|
|
38710
|
+
if (block && /^(?:case\b.*|default\s*):$/.test(block.header)) {
|
|
38711
|
+
block = enclosingBrace(lines, block.line, block.col);
|
|
38712
|
+
}
|
|
38713
|
+
if (!block)
|
|
38714
|
+
return false;
|
|
38715
|
+
let first = block.headerLine;
|
|
38716
|
+
let last;
|
|
38717
|
+
const end = matchingClose(lines, block.line, block.col);
|
|
38718
|
+
if (!end)
|
|
38719
|
+
return false;
|
|
38720
|
+
last = end.line;
|
|
38721
|
+
if (/\bswitch\b/.test(block.header)) {} else if (/^(?:\}\s*)?else\b/.test(block.header) || /^(?:\}\s*)?(?:else\s+)?if\s*\(/.test(block.header)) {
|
|
38722
|
+
let cur = block;
|
|
38723
|
+
for (let guard = 0;guard < 64 && /^(?:\}\s*)?else\b/.test(cur.header); guard++) {
|
|
38724
|
+
let closeLine = cur.headerLine;
|
|
38725
|
+
let closeCol = lines[closeLine].indexOf("}");
|
|
38726
|
+
if (closeCol === -1 || closeCol > lines[closeLine].search(/\belse\b/)) {
|
|
38727
|
+
closeLine--;
|
|
38728
|
+
while (closeLine >= 0 && lines[closeLine].trim() === "")
|
|
38729
|
+
closeLine--;
|
|
38730
|
+
if (closeLine < 0 || !lines[closeLine].trimEnd().endsWith("}"))
|
|
38731
|
+
return false;
|
|
38732
|
+
closeCol = lines[closeLine].lastIndexOf("}");
|
|
38733
|
+
}
|
|
38734
|
+
const prev = enclosingBrace(lines, closeLine, closeCol);
|
|
38735
|
+
if (!prev)
|
|
38736
|
+
return false;
|
|
38737
|
+
cur = prev;
|
|
38738
|
+
first = prev.headerLine;
|
|
38739
|
+
}
|
|
38740
|
+
let close = end;
|
|
38741
|
+
for (let guard = 0;guard < 64; guard++) {
|
|
38742
|
+
let l = close.line;
|
|
38743
|
+
let rest = lines[l].slice(close.col + 1).trim();
|
|
38744
|
+
while (rest === "" && l + 1 < lines.length) {
|
|
38745
|
+
l++;
|
|
38746
|
+
rest = lines[l].trim();
|
|
38747
|
+
}
|
|
38748
|
+
if (!/^else\b/.test(rest))
|
|
38749
|
+
break;
|
|
38750
|
+
let openLine = l;
|
|
38751
|
+
let openCol = lines[l].indexOf("{", l === close.line ? close.col + 1 : 0);
|
|
38752
|
+
if (openCol === -1) {
|
|
38753
|
+
openLine = l + 1;
|
|
38754
|
+
while (openLine < lines.length && lines[openLine].trim() === "")
|
|
38755
|
+
openLine++;
|
|
38756
|
+
if (openLine >= lines.length || !lines[openLine].trimStart().startsWith("{"))
|
|
38757
|
+
break;
|
|
38758
|
+
openCol = lines[openLine].indexOf("{");
|
|
38759
|
+
}
|
|
38760
|
+
const next = matchingClose(lines, openLine, openCol);
|
|
38761
|
+
if (!next)
|
|
38762
|
+
break;
|
|
38763
|
+
close = next;
|
|
38764
|
+
last = next.line;
|
|
38765
|
+
}
|
|
38766
|
+
} else {
|
|
38029
38767
|
return false;
|
|
38768
|
+
}
|
|
38030
38769
|
const assign = new RegExp(`(?:^|[^\\w.$])${variable}\\s*(?::?=)(?!=)\\s*(.*?)\\s*;?\\s*(?:break\\s*;?)?\\s*$`);
|
|
38031
38770
|
const literalOnly = new RegExp(`^${strLit}$`);
|
|
38032
|
-
|
|
38033
|
-
|
|
38034
|
-
|
|
38035
|
-
|
|
38036
|
-
|
|
38037
|
-
|
|
38038
|
-
return true;
|
|
38039
|
-
}
|
|
38040
|
-
for (const ch of line) {
|
|
38041
|
-
if (ch === "{")
|
|
38042
|
-
depth++;
|
|
38043
|
-
else if (ch === "}")
|
|
38044
|
-
depth--;
|
|
38045
|
-
}
|
|
38046
|
-
if (i2 > start2 && depth <= 0)
|
|
38047
|
-
break;
|
|
38771
|
+
for (let i2 = first;i2 <= last; i2++) {
|
|
38772
|
+
if (i2 === idx || unreachableLines?.has(i2 + 1))
|
|
38773
|
+
continue;
|
|
38774
|
+
const m = assign.exec(lines[i2]);
|
|
38775
|
+
if (m && m[1] && !literalOnly.test(m[1]))
|
|
38776
|
+
return true;
|
|
38048
38777
|
}
|
|
38049
38778
|
return false;
|
|
38050
38779
|
}
|
|
@@ -38172,6 +38901,22 @@ function detectExpressionScanFlows(calls, sources, sinks, sanitizers, unreachabl
|
|
|
38172
38901
|
const goFnStart = (line) => enclosingStart(goFnRanges, line);
|
|
38173
38902
|
const csFnRanges = methodRanges(["csharp"]);
|
|
38174
38903
|
const csFnStart = (line) => enclosingStart(csFnRanges, line);
|
|
38904
|
+
const csFieldNames = new Set;
|
|
38905
|
+
if (language === "csharp") {
|
|
38906
|
+
for (const t of types ?? [])
|
|
38907
|
+
for (const f of t.fields ?? [])
|
|
38908
|
+
csFieldNames.add(f.name);
|
|
38909
|
+
}
|
|
38910
|
+
const csDifferentMethod = (variable, sourceLine, otherLine) => {
|
|
38911
|
+
if (csFnRanges.length === 0)
|
|
38912
|
+
return false;
|
|
38913
|
+
const a = csFnStart(sourceLine);
|
|
38914
|
+
const b = csFnStart(otherLine);
|
|
38915
|
+
if (a === -1 || b === -1 || a === b)
|
|
38916
|
+
return false;
|
|
38917
|
+
const base = variable.replace(/^this\./, "").split(".")[0];
|
|
38918
|
+
return !csFieldNames.has(base);
|
|
38919
|
+
};
|
|
38175
38920
|
const sourcesWithVar = sources.filter((s) => typeof s.variable === "string" && s.variable.length > 0);
|
|
38176
38921
|
const aliasSanitizedFor = new Map;
|
|
38177
38922
|
if (language === "python" && typeof code === "string") {
|
|
@@ -38444,7 +39189,29 @@ function detectExpressionScanFlows(calls, sources, sinks, sanitizers, unreachabl
|
|
|
38444
39189
|
}
|
|
38445
39190
|
}
|
|
38446
39191
|
}
|
|
38447
|
-
if (
|
|
39192
|
+
if (language === "csharp" && typeof code === "string" && sourcesWithVar.length > 0 && csFnRanges.length > 0) {
|
|
39193
|
+
const csMethods = (types ?? []).flatMap((t) => t.methods);
|
|
39194
|
+
const seeds = sourcesWithVar.map((sv) => ({ variable: sv.variable, line: sv.line }));
|
|
39195
|
+
const methodStartOf = (line) => csFnStart(line);
|
|
39196
|
+
const aliases = csharpMethodScopedAliases(code, seeds, csMethods, csFieldNames);
|
|
39197
|
+
const fileAnchor = sourcesWithVar.reduce((a, sv) => sv.line < a.line ? sv : a, sourcesWithVar[0]);
|
|
39198
|
+
const have = new Set(sourcesWithVar.map((sv) => `${sv.variable}@${methodStartOf(sv.line)}`));
|
|
39199
|
+
for (const alias of aliases) {
|
|
39200
|
+
const fn = methodStartOf(alias.line);
|
|
39201
|
+
const isField = csFieldNames.has(alias.variable);
|
|
39202
|
+
const key = `${alias.variable}@${isField ? "field" : fn}`;
|
|
39203
|
+
if (have.has(key))
|
|
39204
|
+
continue;
|
|
39205
|
+
have.add(key);
|
|
39206
|
+
const inMethod = sourcesWithVar.filter((sv) => methodStartOf(sv.line) === fn && sv.line <= alias.line);
|
|
39207
|
+
const from = !alias.carried && inMethod.length > 0 ? inMethod.reduce((a, sv) => sv.line < a.line ? sv : a, inMethod[0]) : fileAnchor;
|
|
39208
|
+
sourcesWithVar.push({
|
|
39209
|
+
...from,
|
|
39210
|
+
variable: alias.variable,
|
|
39211
|
+
...alias.carried || inMethod.length === 0 ? { line: alias.line } : {}
|
|
39212
|
+
});
|
|
39213
|
+
}
|
|
39214
|
+
} else if ((language === "java" || language === "csharp") && typeof code === "string" && sourcesWithVar.length > 0) {
|
|
38448
39215
|
const seedVars = new Set(sourcesWithVar.map((s) => s.variable));
|
|
38449
39216
|
const derived = buildJavaTaintedVars(code, seedVars);
|
|
38450
39217
|
if (derived.size > 0) {
|
|
@@ -38493,23 +39260,26 @@ function detectExpressionScanFlows(calls, sources, sinks, sanitizers, unreachabl
|
|
|
38493
39260
|
const lines = code.split(`
|
|
38494
39261
|
`);
|
|
38495
39262
|
const ctRe = /(\w+)\s*\.\s*CommandText\s*\+?=\s*(.+)/;
|
|
38496
|
-
const
|
|
39263
|
+
const seeded = new Set;
|
|
38497
39264
|
let added = true;
|
|
38498
39265
|
let guard = 0;
|
|
38499
39266
|
while (added && guard < lines.length + 2) {
|
|
38500
39267
|
added = false;
|
|
38501
39268
|
guard++;
|
|
38502
|
-
const known = new Set(sourcesWithVar.map((s) => s.variable));
|
|
38503
39269
|
for (let i2 = 0;i2 < lines.length; i2++) {
|
|
38504
39270
|
const m = ctRe.exec(lines[i2]);
|
|
38505
39271
|
if (!m)
|
|
38506
39272
|
continue;
|
|
38507
39273
|
const [, obj, rhs] = m;
|
|
38508
|
-
|
|
39274
|
+
const key = `${obj}@${csFnStart(i2 + 1)}`;
|
|
39275
|
+
if (seeded.has(key))
|
|
38509
39276
|
continue;
|
|
38510
|
-
|
|
39277
|
+
const reachable = sourcesWithVar.filter((sv) => sv.line <= i2 + 1 && !csDifferentMethod(sv.variable, sv.line, i2 + 1));
|
|
39278
|
+
if (reachable.length === 0 || !argsReferenceTaintedVar(rhs, reachable))
|
|
38511
39279
|
continue;
|
|
38512
|
-
|
|
39280
|
+
const from = reachable.reduce((a, sv) => sv.line < a.line ? sv : a, reachable[0]);
|
|
39281
|
+
sourcesWithVar.push({ ...from, variable: obj, line: i2 + 1 });
|
|
39282
|
+
seeded.add(key);
|
|
38513
39283
|
added = true;
|
|
38514
39284
|
}
|
|
38515
39285
|
}
|
|
@@ -38528,24 +39298,26 @@ function detectExpressionScanFlows(calls, sources, sinks, sanitizers, unreachabl
|
|
|
38528
39298
|
if (builders.size > 0) {
|
|
38529
39299
|
const appendRe = /([\p{L}_][\p{L}\p{N}_]*)\s*\.\s*(?:Append|AppendLine|AppendFormat|Insert)\s*\(([\s\S]*)/u;
|
|
38530
39300
|
const anchor = sourcesWithVar.reduce((a, s) => s.line < a.line ? s : a, sourcesWithVar[0]);
|
|
39301
|
+
const builderSeeded = new Set;
|
|
38531
39302
|
let added = true;
|
|
38532
39303
|
let guard = 0;
|
|
38533
39304
|
while (added && guard < lines.length + 2) {
|
|
38534
39305
|
added = false;
|
|
38535
39306
|
guard++;
|
|
38536
|
-
const known = new Set(sourcesWithVar.map((s) => s.variable));
|
|
38537
39307
|
for (let i2 = 0;i2 < lines.length; i2++) {
|
|
38538
39308
|
const m = appendRe.exec(lines[i2]);
|
|
38539
39309
|
if (!m)
|
|
38540
39310
|
continue;
|
|
38541
39311
|
const [, obj, args2] = m;
|
|
38542
|
-
|
|
39312
|
+
const seedKey = `${obj}@${csFnStart(i2 + 1)}`;
|
|
39313
|
+
if (!builders.has(obj) || builderSeeded.has(seedKey))
|
|
38543
39314
|
continue;
|
|
38544
39315
|
const appendFn = csFnStart(i2 + 1);
|
|
38545
39316
|
const reachable = sourcesWithVar.filter((sv) => appendFn === -1 || csFnStart(sv.line) === -1 || csFnStart(sv.line) === appendFn);
|
|
38546
39317
|
if (reachable.length > 0 && argsReferenceTaintedVar(args2, reachable)) {
|
|
38547
39318
|
sourcesWithVar.push({ ...anchor, variable: obj, line: i2 + 1 });
|
|
38548
39319
|
builderSeeds.add(`${obj}@${i2 + 1}`);
|
|
39320
|
+
builderSeeded.add(seedKey);
|
|
38549
39321
|
added = true;
|
|
38550
39322
|
}
|
|
38551
39323
|
}
|
|
@@ -38628,6 +39400,8 @@ function detectExpressionScanFlows(calls, sources, sinks, sanitizers, unreachabl
|
|
|
38628
39400
|
if (srcFn !== sinkFn)
|
|
38629
39401
|
continue;
|
|
38630
39402
|
}
|
|
39403
|
+
if (csDifferentMethod(source.variable, source.line, sink.line))
|
|
39404
|
+
continue;
|
|
38631
39405
|
if (complex) {
|
|
38632
39406
|
const re = reCache.get(source.variable);
|
|
38633
39407
|
if (!re || !re.test(expr))
|
|
@@ -38641,7 +39415,7 @@ function detectExpressionScanFlows(calls, sources, sinks, sanitizers, unreachabl
|
|
|
38641
39415
|
if (aliasSanitizedFor.get(source.variable)?.has(sink.type)) {
|
|
38642
39416
|
break;
|
|
38643
39417
|
}
|
|
38644
|
-
if (typeof code === "string" && isReassignedToLiteralBetween(code, source.variable, source.line, sink.line)) {
|
|
39418
|
+
if (typeof code === "string" && isReassignedToLiteralBetween(code, source.variable, source.line, sink.line, unreachableLines)) {
|
|
38645
39419
|
break;
|
|
38646
39420
|
}
|
|
38647
39421
|
flows.push({
|
|
@@ -38948,7 +39722,14 @@ var CSHARP_NON_ESCAPE_METHODS = new Set([
|
|
|
38948
39722
|
"TryAdd",
|
|
38949
39723
|
"Insert",
|
|
38950
39724
|
"Push",
|
|
38951
|
-
"Enqueue"
|
|
39725
|
+
"Enqueue",
|
|
39726
|
+
"EscapeDataString",
|
|
39727
|
+
"EscapeUriString",
|
|
39728
|
+
"UrlEncode",
|
|
39729
|
+
"UrlPathEncode",
|
|
39730
|
+
"HtmlEncode",
|
|
39731
|
+
"HtmlAttributeEncode",
|
|
39732
|
+
"JavaScriptStringEncode"
|
|
38952
39733
|
]);
|
|
38953
39734
|
function isCSharpNonEscapeCall(call) {
|
|
38954
39735
|
const name2 = call.method_name;
|
|
@@ -39520,6 +40301,7 @@ class InterproceduralPass {
|
|
|
39520
40301
|
}
|
|
39521
40302
|
}
|
|
39522
40303
|
const language = graph.ir.meta.language;
|
|
40304
|
+
const csCredit = language === "csharp" && typeof ctx.code === "string" ? new CSharpCredit(ctx.code, graph.ir.types.flatMap((t) => t.methods)) : null;
|
|
39523
40305
|
if (sinks.length > 0) {
|
|
39524
40306
|
const interProc = analyzeInterprocedural2(graph, sources, sinks, sanitizers, {
|
|
39525
40307
|
taintedVariables: constProp.tainted
|
|
@@ -39552,6 +40334,9 @@ class InterproceduralPass {
|
|
|
39552
40334
|
continue;
|
|
39553
40335
|
if (sanitizerMethodNames.has(method.name))
|
|
39554
40336
|
continue;
|
|
40337
|
+
if (language === "csharp" && typeof ctx.code === "string" && !csharpParamReachesSink(ctx.code, method, edge.taintedArgs, sink, csCredit)) {
|
|
40338
|
+
continue;
|
|
40339
|
+
}
|
|
39555
40340
|
for (const source of sources) {
|
|
39556
40341
|
if (source.line > edge.callLine)
|
|
39557
40342
|
continue;
|
|
@@ -39693,9 +40478,59 @@ class InterproceduralPass {
|
|
|
39693
40478
|
return !sanitizedSinkKeys.has(`${s.line}:${s.type}`);
|
|
39694
40479
|
});
|
|
39695
40480
|
}
|
|
40481
|
+
if (csCredit) {
|
|
40482
|
+
const csSeeds = sources.flatMap((s) => s.variable ? [{ variable: s.variable, line: s.line }] : []);
|
|
40483
|
+
const credited = new Set;
|
|
40484
|
+
filteredAdditionalFlows = filteredAdditionalFlows.filter((f) => {
|
|
40485
|
+
if (f.sink_type !== "external_taint_escape" || !csCredit.creditedOnLine(f.sink_line, csSeeds))
|
|
40486
|
+
return true;
|
|
40487
|
+
credited.add(f.sink_line);
|
|
40488
|
+
return false;
|
|
40489
|
+
});
|
|
40490
|
+
filteredAdditionalSinks = filteredAdditionalSinks.filter((s) => !(s.type === "external_taint_escape" && credited.has(s.line)));
|
|
40491
|
+
}
|
|
39696
40492
|
return { additionalSinks: filteredAdditionalSinks, additionalFlows: filteredAdditionalFlows, interprocedural };
|
|
39697
40493
|
}
|
|
39698
40494
|
}
|
|
40495
|
+
function csharpParamReachesSink(code, method, taintedArgs, sink, credit) {
|
|
40496
|
+
const lines = code.split(/\r?\n/);
|
|
40497
|
+
const params = taintedArgs.length > 0 ? method.parameters.filter((p) => taintedArgs.includes(p.position)) : method.parameters;
|
|
40498
|
+
if (params.length === 0)
|
|
40499
|
+
return true;
|
|
40500
|
+
const derived = new Set(params.map((p) => p.name));
|
|
40501
|
+
const codeOf = (text) => text.replace(/\$@?"(?:[^"\\]|\\.)*"/g, (lit) => " " + [...lit.matchAll(/\{([^}]*)\}/g)].map((x) => x[1]).join(" ") + " ").replace(/@?"(?:[^"\\]|\\.)*"/g, " ").replace(/'(?:[^'\\]|\\.)'/g, " ");
|
|
40502
|
+
const mentionsDerived = (text) => {
|
|
40503
|
+
const bare = codeOf(text);
|
|
40504
|
+
return [...derived].filter((v) => new RegExp(`(?<![\\w.])${v}(?![\\w])`).test(bare));
|
|
40505
|
+
};
|
|
40506
|
+
const assignRe = /^\s*(?:var\s+|[A-Za-z_][\w.<>[\]?]*\s+)?([A-Za-z_]\w*)\s*=(?!=)\s*(.+)$/;
|
|
40507
|
+
const objectWriteRe = /^\s*([A-Za-z_]\w*)\s*\.\s*[A-Za-z_]\w*\s*(?:\+?=(?!=)|\()(.*)$/;
|
|
40508
|
+
for (let i2 = method.startLine + 1;i2 < sink.line; i2++) {
|
|
40509
|
+
const text = lines[i2 - 1] ?? "";
|
|
40510
|
+
const assign = assignRe.exec(text);
|
|
40511
|
+
if (assign) {
|
|
40512
|
+
if (mentionsDerived(assign[2]).length > 0)
|
|
40513
|
+
derived.add(assign[1]);
|
|
40514
|
+
else if (!params.some((p) => p.name === assign[1]))
|
|
40515
|
+
derived.delete(assign[1]);
|
|
40516
|
+
continue;
|
|
40517
|
+
}
|
|
40518
|
+
const write = objectWriteRe.exec(text);
|
|
40519
|
+
if (write && mentionsDerived(write[2]).length > 0)
|
|
40520
|
+
derived.add(write[1]);
|
|
40521
|
+
}
|
|
40522
|
+
let statement = lines[sink.line - 1] ?? "";
|
|
40523
|
+
for (let i2 = sink.line;i2 < lines.length && i2 < sink.line + 6 && !/[;{}]\s*$/.test(statement.trimEnd()); i2++) {
|
|
40524
|
+
statement += " " + (lines[i2] ?? "").trim();
|
|
40525
|
+
}
|
|
40526
|
+
const reaching = mentionsDerived(statement);
|
|
40527
|
+
if (reaching.length === 0)
|
|
40528
|
+
return false;
|
|
40529
|
+
if (!credit)
|
|
40530
|
+
return true;
|
|
40531
|
+
const safe = new Set([...credit.sanitizedVarsAt(sink.type, sink.line), ...credit.guardedVarsAt(sink.type, sink.line)]);
|
|
40532
|
+
return reaching.some((v) => !safe.has(v));
|
|
40533
|
+
}
|
|
39699
40534
|
|
|
39700
40535
|
// ../circle-ir/dist/analysis/passes/dead-code-pass.js
|
|
39701
40536
|
class DeadCodePass {
|
|
@@ -40408,7 +41243,7 @@ class NullDerefPass {
|
|
|
40408
41243
|
}
|
|
40409
41244
|
|
|
40410
41245
|
// ../circle-ir/dist/analysis/passes/resource-leak-pass.js
|
|
40411
|
-
function
|
|
41246
|
+
function escapeRegex3(s) {
|
|
40412
41247
|
return s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
40413
41248
|
}
|
|
40414
41249
|
var FACTORY_METHOD_NAME_RE = /^(?:open|create|new|get|make|build)[A-Z]/;
|
|
@@ -40599,7 +41434,7 @@ class ResourceLeakPass {
|
|
|
40599
41434
|
return false;
|
|
40600
41435
|
}
|
|
40601
41436
|
isReturnedToCaller(lines, variable, fromLine, toLine) {
|
|
40602
|
-
const varRe = new RegExp(`\\breturn\\b[^;]*\\b${
|
|
41437
|
+
const varRe = new RegExp(`\\breturn\\b[^;]*\\b${escapeRegex3(variable)}\\b`);
|
|
40603
41438
|
for (let l = fromLine;l <= toLine && l <= lines.length; l++) {
|
|
40604
41439
|
if (varRe.test(lines[l - 1] ?? ""))
|
|
40605
41440
|
return true;
|
|
@@ -40607,7 +41442,7 @@ class ResourceLeakPass {
|
|
|
40607
41442
|
return false;
|
|
40608
41443
|
}
|
|
40609
41444
|
fieldStoredName(lines, variable, openLine, toLine) {
|
|
40610
|
-
const thisAssignRe = new RegExp(`\\bthis\\s*\\.\\s*(\\w+)\\s*=\\s*${
|
|
41445
|
+
const thisAssignRe = new RegExp(`\\bthis\\s*\\.\\s*(\\w+)\\s*=\\s*${escapeRegex3(variable)}\\b`);
|
|
40611
41446
|
for (let l = openLine;l <= toLine && l <= lines.length; l++) {
|
|
40612
41447
|
const m = thisAssignRe.exec(lines[l - 1] ?? "");
|
|
40613
41448
|
if (m)
|
|
@@ -40650,7 +41485,7 @@ class ResourceLeakPass {
|
|
|
40650
41485
|
if (fieldNames.has(variable)) {
|
|
40651
41486
|
candidateField = variable;
|
|
40652
41487
|
} else {
|
|
40653
|
-
const thisAssignRe = new RegExp(`(?:\\bthis\\s*\\.\\s*)?(\\w+)\\s*=\\s*${
|
|
41488
|
+
const thisAssignRe = new RegExp(`(?:\\bthis\\s*\\.\\s*)?(\\w+)\\s*=\\s*${escapeRegex3(variable)}\\b`);
|
|
40654
41489
|
for (let l = fromLine;l <= toLine && l <= lines.length; l++) {
|
|
40655
41490
|
const m = thisAssignRe.exec(lines[l - 1] ?? "");
|
|
40656
41491
|
if (m && fieldNames.has(m[1])) {
|
|
@@ -51135,7 +51970,7 @@ var colors = {
|
|
|
51135
51970
|
};
|
|
51136
51971
|
|
|
51137
51972
|
// src/version.ts
|
|
51138
|
-
var version = "4.
|
|
51973
|
+
var version = "4.13.1";
|
|
51139
51974
|
|
|
51140
51975
|
// src/formatters.ts
|
|
51141
51976
|
var LIBRARY_API_SURFACE_TAG2 = "library-api-surface:caller-responsibility";
|
|
@@ -51687,8 +52522,8 @@ function generateSarifResults(results, crossFileData) {
|
|
|
51687
52522
|
|
|
51688
52523
|
// src/build-info.ts
|
|
51689
52524
|
var buildInfo = {
|
|
51690
|
-
gitSha: "
|
|
51691
|
-
builtAt: "2026-10-
|
|
52525
|
+
gitSha: "51b2da7",
|
|
52526
|
+
builtAt: "2026-10-08T18:47:29.958Z"
|
|
51692
52527
|
};
|
|
51693
52528
|
|
|
51694
52529
|
// src/utils/args.ts
|