cognium-dev 4.12.0 → 4.13.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +14 -21
- package/dist/cli.js +799 -80
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
[](https://www.npmjs.com/package/cognium-dev)
|
|
4
4
|
[](https://github.com/cogniumhq/cognium-dev/blob/main/LICENSE)
|
|
5
|
-
[](https://github.com/cogniumhq/cognium-dev#benchmark-results)
|
|
6
6
|
[](https://github.com/marketplace/actions/cognium-dev-scan)
|
|
7
7
|

|
|
8
8
|

|
|
@@ -467,35 +467,28 @@ For details on LLM integration and benchmark improvements (42.5% → 78.3% on CW
|
|
|
467
467
|
|
|
468
468
|
## Benchmark Results
|
|
469
469
|
|
|
470
|
-
|
|
470
|
+
Measured on cognium-dev **4.12.0**, the CLI as published. Fully deterministic.
|
|
471
471
|
|
|
472
|
-
| Benchmark |
|
|
473
|
-
|
|
474
|
-
| OWASP
|
|
475
|
-
| Juliet Test Suite |
|
|
476
|
-
| SecuriBench Micro |
|
|
477
|
-
| CWE-Bench-Java |
|
|
472
|
+
| Benchmark | TPR | FPR | Scope and rule |
|
|
473
|
+
|-----------|-----|-----|----------------|
|
|
474
|
+
| OWASP BenchmarkJava 1.2 | 90.0% | 3.0% | All 2,740 cases; scorecard rule (file-level CWE match) |
|
|
475
|
+
| Juliet Test Suite 1.3 | 99.4% | 21.9% | `_01` cases of 9 injection CWEs; `bad()` vs `good*()` methods; `require-entry-path` disabled |
|
|
476
|
+
| SecuriBench Micro | 88.0% | 6.7% | 123 annotated cases; found sink lines vs `vuln_count` |
|
|
477
|
+
| CWE-Bench-Java | 32.5% (39/120) | n/a | 120 real CVEs; a finding of the CVE's CWE inside the fix method |
|
|
478
478
|
|
|
479
479
|
### Reproducing Benchmarks
|
|
480
480
|
|
|
481
|
-
|
|
481
|
+
Each number above comes from a `cognium-dev scan` JSON report and a scoring script in this repository. For example, OWASP:
|
|
482
482
|
|
|
483
483
|
```bash
|
|
484
|
-
|
|
485
|
-
npm install -g cognium
|
|
486
|
-
|
|
487
|
-
# Clone benchmark repositories
|
|
484
|
+
npm install -g cognium-dev
|
|
488
485
|
git clone https://github.com/OWASP-Benchmark/BenchmarkJava
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
# Run scans
|
|
493
|
-
cognium-dev scan BenchmarkJava/src --format json -o owasp-results.json
|
|
494
|
-
cognium-dev scan juliet-test-suite-for-java --format json -o juliet-results.json
|
|
495
|
-
cognium-dev scan cwe-bench-java --format json -o cwe-bench-results.json
|
|
486
|
+
cd BenchmarkJava
|
|
487
|
+
cognium-dev scan . --format json --category security -l java > owasp.json
|
|
488
|
+
node <cognium-dev repo>/bench/sast/score-owasp.mjs expectedresults-1.2.csv owasp.json
|
|
496
489
|
```
|
|
497
490
|
|
|
498
|
-
|
|
491
|
+
The scoring rule and command for every benchmark are in [`bench/README.md`](https://github.com/cogniumhq/cognium-dev/blob/main/bench/README.md#sast-accuracy-benchsast).
|
|
499
492
|
|
|
500
493
|
## Links
|
|
501
494
|
|
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;
|
|
@@ -20165,14 +20174,21 @@ class ConstantPropagator {
|
|
|
20165
20174
|
break;
|
|
20166
20175
|
}
|
|
20167
20176
|
}
|
|
20168
|
-
|
|
20177
|
+
if (!switchValue) {
|
|
20178
|
+
const valueNode = node.childForFieldName("value");
|
|
20179
|
+
if (valueNode) {
|
|
20180
|
+
const literal = this.literalValue(valueNode);
|
|
20181
|
+
switchValue = literal !== null ? { type: typeof literal === "number" ? "int" : "string", value: literal, sourceLine: getNodeLine(valueNode) } : this.evaluateExpression(valueNode);
|
|
20182
|
+
}
|
|
20183
|
+
}
|
|
20184
|
+
const switchBlock = node.children.find((c) => c.type === "switch_block" || c.type === "switch_body");
|
|
20169
20185
|
if (!switchBlock) {
|
|
20170
20186
|
for (const child of node.children) {
|
|
20171
20187
|
this.visit(child);
|
|
20172
20188
|
}
|
|
20173
20189
|
return;
|
|
20174
20190
|
}
|
|
20175
|
-
const caseGroups = switchBlock.children.filter((c) => c.type === "switch_block_statement_group" || c.type === "switch_rule");
|
|
20191
|
+
const caseGroups = switchBlock.children.filter((c) => c.type === "switch_block_statement_group" || c.type === "switch_rule" || c.type === "switch_section");
|
|
20176
20192
|
if (switchValue && isKnown(switchValue)) {
|
|
20177
20193
|
let matchingIdx = -1;
|
|
20178
20194
|
let defaultIdx = -1;
|
|
@@ -20189,6 +20205,16 @@ class ConstantPropagator {
|
|
|
20189
20205
|
matchingIdx = i2;
|
|
20190
20206
|
}
|
|
20191
20207
|
}
|
|
20208
|
+
} else if (caseGroup.type === "switch_section") {
|
|
20209
|
+
if (child.type === "default") {
|
|
20210
|
+
defaultIdx = i2;
|
|
20211
|
+
} else if (child.type === "constant_pattern") {
|
|
20212
|
+
const inner = child.children.find((c) => c.isNamed);
|
|
20213
|
+
const caseValue = inner ? this.literalValue(inner) : null;
|
|
20214
|
+
if (caseValue !== null && caseValue === switchValue.value) {
|
|
20215
|
+
matchingIdx = i2;
|
|
20216
|
+
}
|
|
20217
|
+
}
|
|
20192
20218
|
}
|
|
20193
20219
|
}
|
|
20194
20220
|
}
|
|
@@ -20271,6 +20297,17 @@ class ConstantPropagator {
|
|
|
20271
20297
|
}
|
|
20272
20298
|
return false;
|
|
20273
20299
|
}
|
|
20300
|
+
literalValue(node) {
|
|
20301
|
+
const text = getNodeText2(node, this.source);
|
|
20302
|
+
if (node.type === "integer_literal" || node.type === "decimal_integer_literal") {
|
|
20303
|
+
const n = Number(text.replace(/_/g, "").replace(/[uUlL]+$/, ""));
|
|
20304
|
+
return Number.isInteger(n) ? n : null;
|
|
20305
|
+
}
|
|
20306
|
+
if (node.type === "character_literal" || node.type === "string_literal") {
|
|
20307
|
+
return text.slice(1, -1);
|
|
20308
|
+
}
|
|
20309
|
+
return null;
|
|
20310
|
+
}
|
|
20274
20311
|
extractCaseValue(labelNode) {
|
|
20275
20312
|
for (const child of labelNode.children) {
|
|
20276
20313
|
if (child.type === "decimal_integer_literal") {
|
|
@@ -34062,6 +34099,413 @@ class MyBatisAnnotationSqlSinkPass {
|
|
|
34062
34099
|
}
|
|
34063
34100
|
}
|
|
34064
34101
|
|
|
34102
|
+
// ../circle-ir/dist/analysis/passes/csharp-sanitizer-credit.js
|
|
34103
|
+
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*\)/;
|
|
34104
|
+
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*\)/;
|
|
34105
|
+
var CSHARP_SANITIZER_RES = [
|
|
34106
|
+
{ re: /\b(?:HtmlEncode|JavaScriptStringEncode)\s*\(/, type: "xss" },
|
|
34107
|
+
{ re: /\bHtmlEncoder\s*\.\s*(?:Default\s*\.\s*)?Encode\s*\(/, type: "xss" },
|
|
34108
|
+
{ re: /\bPath\s*\.\s*GetFileName\s*\(/, type: "path_traversal" },
|
|
34109
|
+
{ re: /\bSecurityElement\s*\.\s*Escape\s*\(/, type: "xpath_injection" },
|
|
34110
|
+
{ re: CSHARP_ALLOWLIST_STRIP_RE, type: "ldap_injection" },
|
|
34111
|
+
{ re: CSHARP_ALLOWLIST_STRIP_RE, type: "xpath_injection" },
|
|
34112
|
+
{ re: CSHARP_ALLOWLIST_STRIP_RE, type: "command_injection" },
|
|
34113
|
+
{ re: CSHARP_ALLOWLIST_STRIP_RE, type: "sql_injection" },
|
|
34114
|
+
{ re: CSHARP_ALLOWLIST_STRIP_RE, type: "crlf" },
|
|
34115
|
+
{ re: CSHARP_ALLOWLIST_FILTER_RE, type: "ldap_injection" },
|
|
34116
|
+
{ re: CSHARP_ALLOWLIST_FILTER_RE, type: "xpath_injection" },
|
|
34117
|
+
{ re: CSHARP_ALLOWLIST_FILTER_RE, type: "command_injection" },
|
|
34118
|
+
{ re: CSHARP_ALLOWLIST_FILTER_RE, type: "sql_injection" },
|
|
34119
|
+
{ re: CSHARP_ALLOWLIST_FILTER_RE, type: "crlf" }
|
|
34120
|
+
];
|
|
34121
|
+
var INERT_WHEN_ALPHANUMERIC = new Set([
|
|
34122
|
+
"ldap_injection",
|
|
34123
|
+
"xpath_injection",
|
|
34124
|
+
"command_injection",
|
|
34125
|
+
"sql_injection",
|
|
34126
|
+
"crlf",
|
|
34127
|
+
"xss",
|
|
34128
|
+
"path_traversal",
|
|
34129
|
+
"code_injection"
|
|
34130
|
+
]);
|
|
34131
|
+
var IDENT = "[A-Za-z_]\\w*";
|
|
34132
|
+
var ASSIGN_RE = new RegExp(`^\\s*(?:var\\s+|[A-Za-z_][\\w.<>\\[\\]?]*\\s+)?(${IDENT})\\s*=(?!=)\\s*(.+?);?\\s*$`);
|
|
34133
|
+
var ENDS_STATEMENT_RE = /[;{}]\s*(?:\/\/.*)?$/;
|
|
34134
|
+
function escapeRegex(s) {
|
|
34135
|
+
return s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
34136
|
+
}
|
|
34137
|
+
function mentions(text, name2) {
|
|
34138
|
+
return new RegExp(`(?<![\\w])${escapeRegex(name2)}(?![\\w])`).test(text);
|
|
34139
|
+
}
|
|
34140
|
+
function insideParens(text, open) {
|
|
34141
|
+
let depth = 0;
|
|
34142
|
+
for (let i2 = open;i2 < text.length; i2++) {
|
|
34143
|
+
if (text[i2] === "(")
|
|
34144
|
+
depth++;
|
|
34145
|
+
else if (text[i2] === ")" && --depth === 0)
|
|
34146
|
+
return text.slice(open + 1, i2);
|
|
34147
|
+
}
|
|
34148
|
+
return null;
|
|
34149
|
+
}
|
|
34150
|
+
function leadingIf(text) {
|
|
34151
|
+
const m = /^\s*(?:\}\s*)?(?:else\s+)?if\s*\(/.exec(text);
|
|
34152
|
+
if (!m)
|
|
34153
|
+
return null;
|
|
34154
|
+
const open = m[0].length - 1;
|
|
34155
|
+
const cond = insideParens(text, open);
|
|
34156
|
+
if (cond === null)
|
|
34157
|
+
return null;
|
|
34158
|
+
return { cond: cond.trim(), rest: text.slice(open + cond.length + 2).trim() };
|
|
34159
|
+
}
|
|
34160
|
+
var EXITS_RE = /^(?:\{\s*)?(?:return\b|throw\b|continue\b|break\b)/;
|
|
34161
|
+
|
|
34162
|
+
class CSharpCredit {
|
|
34163
|
+
methods;
|
|
34164
|
+
lines;
|
|
34165
|
+
helperCache = new Map;
|
|
34166
|
+
guardHelperCache = new Map;
|
|
34167
|
+
constructor(code, methods) {
|
|
34168
|
+
this.methods = methods;
|
|
34169
|
+
this.lines = code.split(/\r?\n/);
|
|
34170
|
+
}
|
|
34171
|
+
methodAt(line) {
|
|
34172
|
+
let best = null;
|
|
34173
|
+
for (const m of this.methods) {
|
|
34174
|
+
if (line < m.start_line || line > m.end_line)
|
|
34175
|
+
continue;
|
|
34176
|
+
if (!best || m.end_line - m.start_line < best.end_line - best.start_line)
|
|
34177
|
+
best = m;
|
|
34178
|
+
}
|
|
34179
|
+
return best;
|
|
34180
|
+
}
|
|
34181
|
+
statements(from, to) {
|
|
34182
|
+
const out2 = [];
|
|
34183
|
+
for (let i2 = Math.max(from, 1);i2 < to; i2++) {
|
|
34184
|
+
let text = (this.lines[i2 - 1] ?? "").trimEnd();
|
|
34185
|
+
const first = i2;
|
|
34186
|
+
if (/^\s*(?:[\w.<>[\]?]+\s+)?[A-Za-z_]\w*\s*=(?!=)/.test(text) && !ENDS_STATEMENT_RE.test(text)) {
|
|
34187
|
+
while (i2 < to - 1 && i2 < first + 12) {
|
|
34188
|
+
i2++;
|
|
34189
|
+
text += " " + (this.lines[i2 - 1] ?? "").trim();
|
|
34190
|
+
if (ENDS_STATEMENT_RE.test(this.lines[i2 - 1] ?? ""))
|
|
34191
|
+
break;
|
|
34192
|
+
}
|
|
34193
|
+
}
|
|
34194
|
+
out2.push({ line: first, text });
|
|
34195
|
+
}
|
|
34196
|
+
return out2;
|
|
34197
|
+
}
|
|
34198
|
+
builtinSanitizes(type, text) {
|
|
34199
|
+
for (const { re, type: t } of CSHARP_SANITIZER_RES) {
|
|
34200
|
+
if (t === type && re.test(text))
|
|
34201
|
+
return true;
|
|
34202
|
+
}
|
|
34203
|
+
if (type === "ldap_injection") {
|
|
34204
|
+
if (/\.\s*Replace\s*\(\s*"\("\s*,/.test(text) && /\.\s*Replace\s*\(\s*"\)"\s*,/.test(text))
|
|
34205
|
+
return true;
|
|
34206
|
+
}
|
|
34207
|
+
if (type === "path_traversal") {
|
|
34208
|
+
if (/\b([A-Za-z_]\w*)\s*\.\s*StartsWith\s*\([^)]*\)\s*\?\s*\1\s*:\s*null\b/.test(text))
|
|
34209
|
+
return true;
|
|
34210
|
+
}
|
|
34211
|
+
return false;
|
|
34212
|
+
}
|
|
34213
|
+
helpers(type) {
|
|
34214
|
+
const cached = this.helperCache.get(type);
|
|
34215
|
+
if (cached)
|
|
34216
|
+
return cached;
|
|
34217
|
+
const set = new Set;
|
|
34218
|
+
this.helperCache.set(type, set);
|
|
34219
|
+
for (const m of this.methods) {
|
|
34220
|
+
const returns = this.returnExprs(m);
|
|
34221
|
+
if (returns.length === 0)
|
|
34222
|
+
continue;
|
|
34223
|
+
const ok = returns.every((r) => {
|
|
34224
|
+
if (this.builtinSanitizes(type, r.expr))
|
|
34225
|
+
return true;
|
|
34226
|
+
const v = /^([A-Za-z_]\w*)$/.exec(r.expr.trim());
|
|
34227
|
+
return !!v && this.sanitizedVarsIn(type, m.start_line, r.line, false).has(v[1]);
|
|
34228
|
+
});
|
|
34229
|
+
if (ok)
|
|
34230
|
+
set.add(m.name);
|
|
34231
|
+
}
|
|
34232
|
+
return set;
|
|
34233
|
+
}
|
|
34234
|
+
returnExprs(m) {
|
|
34235
|
+
const out2 = [];
|
|
34236
|
+
for (const st of this.statements(m.start_line, m.end_line + 1)) {
|
|
34237
|
+
for (const r of st.text.matchAll(/\breturn\s+([^;]+);/g))
|
|
34238
|
+
out2.push({ line: st.line, expr: r[1] });
|
|
34239
|
+
if (st.line === m.start_line) {
|
|
34240
|
+
const body2 = /\)\s*=>\s*(.+?);\s*$/.exec(st.text);
|
|
34241
|
+
if (body2)
|
|
34242
|
+
out2.push({ line: st.line, expr: body2[1] });
|
|
34243
|
+
}
|
|
34244
|
+
}
|
|
34245
|
+
return out2;
|
|
34246
|
+
}
|
|
34247
|
+
sanitizes(type, text) {
|
|
34248
|
+
if (this.builtinSanitizes(type, text))
|
|
34249
|
+
return true;
|
|
34250
|
+
for (const name2 of this.helpers(type)) {
|
|
34251
|
+
if (new RegExp(`(?<![\\w.])${escapeRegex(name2)}\\s*\\(`).test(text))
|
|
34252
|
+
return true;
|
|
34253
|
+
}
|
|
34254
|
+
return false;
|
|
34255
|
+
}
|
|
34256
|
+
sanitizedVarsIn(type, from, to, withHelpers) {
|
|
34257
|
+
const sanitized = new Set;
|
|
34258
|
+
for (const st of this.statements(from, to)) {
|
|
34259
|
+
const m = ASSIGN_RE.exec(st.text);
|
|
34260
|
+
if (!m)
|
|
34261
|
+
continue;
|
|
34262
|
+
const [, lhs, rhs] = m;
|
|
34263
|
+
const credited = (withHelpers ? this.sanitizes(type, rhs) : this.builtinSanitizes(type, rhs)) || [...sanitized].some((v) => mentions(rhs, v));
|
|
34264
|
+
if (credited)
|
|
34265
|
+
sanitized.add(lhs);
|
|
34266
|
+
else
|
|
34267
|
+
sanitized.delete(lhs);
|
|
34268
|
+
}
|
|
34269
|
+
return sanitized;
|
|
34270
|
+
}
|
|
34271
|
+
sanitizedVarsAt(type, sinkLine) {
|
|
34272
|
+
return this.sanitizedVarsIn(type, this.methodAt(sinkLine)?.start_line ?? 1, sinkLine, true);
|
|
34273
|
+
}
|
|
34274
|
+
sinkText(line) {
|
|
34275
|
+
const text = this.lines[line - 1] ?? "";
|
|
34276
|
+
const guarded = leadingIf(text);
|
|
34277
|
+
return guarded && guarded.rest !== "" ? guarded.rest : text;
|
|
34278
|
+
}
|
|
34279
|
+
allowlistVars(cond) {
|
|
34280
|
+
const out2 = [];
|
|
34281
|
+
const anchored = new RegExp(`\\bRegex\\s*\\.\\s*IsMatch\\s*\\(\\s*(${IDENT})\\s*,\\s*@?"\\^\\[(?:[A-Za-z0-9_\\- ]|\\\\w|\\\\d)+\\](?:[+*]|\\{\\d+(?:,\\d*)?\\})\\$"`, "g");
|
|
34282
|
+
for (const m of cond.matchAll(anchored))
|
|
34283
|
+
out2.push(m[1]);
|
|
34284
|
+
const methodGroup = new RegExp(`(${IDENT})\\s*\\.\\s*All\\s*\\(\\s*char\\s*\\.\\s*Is(?:LetterOrDigit|Digit|Letter)\\s*\\)`, "g");
|
|
34285
|
+
for (const m of cond.matchAll(methodGroup))
|
|
34286
|
+
out2.push(m[1]);
|
|
34287
|
+
const lambda = new RegExp(`(${IDENT})\\s*\\.\\s*All\\s*\\(\\s*(${IDENT})\\s*=>`, "g");
|
|
34288
|
+
for (const m of cond.matchAll(lambda)) {
|
|
34289
|
+
const open = cond.indexOf("(", m.index + m[1].length);
|
|
34290
|
+
const body2 = insideParens(cond, open)?.replace(new RegExp(`^\\s*${m[2]}\\s*=>`), "") ?? "";
|
|
34291
|
+
const c = escapeRegex(m[2]);
|
|
34292
|
+
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, "");
|
|
34293
|
+
if (residue === "")
|
|
34294
|
+
out2.push(m[1]);
|
|
34295
|
+
}
|
|
34296
|
+
return out2;
|
|
34297
|
+
}
|
|
34298
|
+
numericVars(cond, parsedFrom) {
|
|
34299
|
+
const out2 = [];
|
|
34300
|
+
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");
|
|
34301
|
+
for (const m of cond.matchAll(tryParse))
|
|
34302
|
+
out2.push(m[1]);
|
|
34303
|
+
for (const [parsed, from] of parsedFrom) {
|
|
34304
|
+
if (new RegExp(`(?<![\\w])${escapeRegex(parsed)}\\s*(?:!=\\s*null|\\.\\s*HasValue)\\b`).test(cond))
|
|
34305
|
+
out2.push(from);
|
|
34306
|
+
}
|
|
34307
|
+
return out2;
|
|
34308
|
+
}
|
|
34309
|
+
hostVars(cond, hostOf) {
|
|
34310
|
+
const out2 = [];
|
|
34311
|
+
const compares = (expr) => {
|
|
34312
|
+
const e = escapeRegex(expr).replace(/\\ /g, "\\s*");
|
|
34313
|
+
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);
|
|
34314
|
+
};
|
|
34315
|
+
const direct = new RegExp(`new\\s+(?:System\\s*\\.\\s*)?Uri\\s*\\(\\s*(${IDENT})\\s*\\)\\s*\\.\\s*Host\\b`, "g");
|
|
34316
|
+
for (const m of cond.matchAll(direct)) {
|
|
34317
|
+
if (compares(m[0]))
|
|
34318
|
+
out2.push(m[1]);
|
|
34319
|
+
}
|
|
34320
|
+
for (const [hostVar, from] of hostOf) {
|
|
34321
|
+
if (mentions(cond, hostVar) && compares(hostVar))
|
|
34322
|
+
out2.push(from);
|
|
34323
|
+
}
|
|
34324
|
+
return out2;
|
|
34325
|
+
}
|
|
34326
|
+
guardHelpers(kind) {
|
|
34327
|
+
const cached = this.guardHelperCache.get(kind);
|
|
34328
|
+
if (cached)
|
|
34329
|
+
return cached;
|
|
34330
|
+
const map = new Map;
|
|
34331
|
+
this.guardHelperCache.set(kind, map);
|
|
34332
|
+
for (const m of this.methods) {
|
|
34333
|
+
const signature = this.lines[m.start_line - 1] ?? "";
|
|
34334
|
+
const open = signature.indexOf("(", signature.indexOf(m.name));
|
|
34335
|
+
const params = open === -1 ? null : insideParens(signature, open);
|
|
34336
|
+
if (!params)
|
|
34337
|
+
continue;
|
|
34338
|
+
const names = params.split(",").map((p) => p.trim().split(/\s+/).pop() ?? "");
|
|
34339
|
+
const returns = this.returnExprs(m);
|
|
34340
|
+
if (returns.length !== 1)
|
|
34341
|
+
continue;
|
|
34342
|
+
const proven = this.provenBy(kind, returns[0].expr, new Map, new Map, false);
|
|
34343
|
+
const pos = names.findIndex((n) => proven.includes(n));
|
|
34344
|
+
if (pos !== -1)
|
|
34345
|
+
map.set(m.name, pos);
|
|
34346
|
+
}
|
|
34347
|
+
return map;
|
|
34348
|
+
}
|
|
34349
|
+
provenBy(kind, cond, parsedFrom, hostOf, withHelpers) {
|
|
34350
|
+
const out2 = kind === "allowlist" ? this.allowlistVars(cond) : kind === "numeric" ? this.numericVars(cond, parsedFrom) : this.hostVars(cond, hostOf);
|
|
34351
|
+
if (withHelpers) {
|
|
34352
|
+
for (const [name2, pos] of this.guardHelpers(kind)) {
|
|
34353
|
+
for (const call of cond.matchAll(new RegExp(`(?<![\\w.])${escapeRegex(name2)}\\s*\\(`, "g"))) {
|
|
34354
|
+
const args2 = insideParens(cond, call.index + call[0].length - 1)?.split(",") ?? [];
|
|
34355
|
+
const arg = args2[pos]?.trim();
|
|
34356
|
+
if (arg && new RegExp(`^${IDENT}$`).test(arg))
|
|
34357
|
+
out2.push(arg);
|
|
34358
|
+
}
|
|
34359
|
+
}
|
|
34360
|
+
}
|
|
34361
|
+
return out2;
|
|
34362
|
+
}
|
|
34363
|
+
guardedVarsAt(type, sinkLine) {
|
|
34364
|
+
const kinds = [];
|
|
34365
|
+
if (INERT_WHEN_ALPHANUMERIC.has(type))
|
|
34366
|
+
kinds.push("allowlist", "numeric");
|
|
34367
|
+
if (type === "ssrf")
|
|
34368
|
+
kinds.push("host");
|
|
34369
|
+
const proven = new Set;
|
|
34370
|
+
if (kinds.length === 0)
|
|
34371
|
+
return proven;
|
|
34372
|
+
const scopeStart = this.methodAt(sinkLine)?.start_line ?? 1;
|
|
34373
|
+
const before = this.statements(scopeStart, sinkLine);
|
|
34374
|
+
const boolOf = new Map;
|
|
34375
|
+
const parsedFrom = new Map;
|
|
34376
|
+
const hostOf = new Map;
|
|
34377
|
+
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");
|
|
34378
|
+
for (const st of before) {
|
|
34379
|
+
for (const p of st.text.matchAll(parseAnywhere))
|
|
34380
|
+
parsedFrom.set(p[1], p[2]);
|
|
34381
|
+
const m = ASSIGN_RE.exec(st.text);
|
|
34382
|
+
if (!m)
|
|
34383
|
+
continue;
|
|
34384
|
+
const [, lhs, rhs] = m;
|
|
34385
|
+
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);
|
|
34386
|
+
if (parse2)
|
|
34387
|
+
parsedFrom.set(lhs, parse2[1]);
|
|
34388
|
+
const host = new RegExp(`^new\\s+(?:System\\s*\\.\\s*)?Uri\\s*\\(\\s*(${IDENT})\\s*\\)\\s*\\.\\s*Host$`).exec(rhs.trim());
|
|
34389
|
+
if (host)
|
|
34390
|
+
hostOf.set(lhs, host[1]);
|
|
34391
|
+
boolOf.set(lhs, rhs);
|
|
34392
|
+
}
|
|
34393
|
+
const credit = (cond) => {
|
|
34394
|
+
const resolved = new RegExp(`^${IDENT}$`).test(cond) ? boolOf.get(cond) ?? cond : cond;
|
|
34395
|
+
for (const kind of kinds) {
|
|
34396
|
+
for (const v of this.provenBy(kind, resolved, parsedFrom, hostOf, true))
|
|
34397
|
+
proven.add(v);
|
|
34398
|
+
}
|
|
34399
|
+
};
|
|
34400
|
+
const own = leadingIf(this.lines[sinkLine - 1] ?? "");
|
|
34401
|
+
if (own && !own.cond.startsWith("!"))
|
|
34402
|
+
credit(own.cond);
|
|
34403
|
+
let depth = 0;
|
|
34404
|
+
for (let i2 = sinkLine - 1;i2 >= scopeStart; i2--) {
|
|
34405
|
+
const text = this.lines[i2 - 1] ?? "";
|
|
34406
|
+
for (let c = text.length - 1;c >= 0; c--) {
|
|
34407
|
+
if (text[c] === "}")
|
|
34408
|
+
depth++;
|
|
34409
|
+
else if (text[c] === "{") {
|
|
34410
|
+
if (depth > 0) {
|
|
34411
|
+
depth--;
|
|
34412
|
+
continue;
|
|
34413
|
+
}
|
|
34414
|
+
let header = text.slice(0, c);
|
|
34415
|
+
for (let h = i2 - 1;header.trim() === "" && h >= scopeStart; h--)
|
|
34416
|
+
header = this.lines[h - 1] ?? "";
|
|
34417
|
+
const enclosing = leadingIf(header);
|
|
34418
|
+
if (enclosing && !enclosing.cond.startsWith("!"))
|
|
34419
|
+
credit(enclosing.cond);
|
|
34420
|
+
}
|
|
34421
|
+
}
|
|
34422
|
+
}
|
|
34423
|
+
for (let i2 = scopeStart;i2 < sinkLine; i2++) {
|
|
34424
|
+
const guard = leadingIf(this.lines[i2 - 1] ?? "");
|
|
34425
|
+
if (!guard)
|
|
34426
|
+
continue;
|
|
34427
|
+
let rest = guard.rest;
|
|
34428
|
+
for (let n = i2 + 1;rest === "" && n < sinkLine; n++)
|
|
34429
|
+
rest = (this.lines[n - 1] ?? "").trim();
|
|
34430
|
+
if (!EXITS_RE.test(rest))
|
|
34431
|
+
continue;
|
|
34432
|
+
if (guard.cond.startsWith("!")) {
|
|
34433
|
+
const inner = guard.cond.slice(1).trim();
|
|
34434
|
+
const wrapped = inner.startsWith("(") && insideParens(inner, 0)?.length === inner.length - 2;
|
|
34435
|
+
credit(wrapped ? inner.slice(1, -1) : inner);
|
|
34436
|
+
} else if (/!=/.test(guard.cond) && !/[|&]/.test(guard.cond))
|
|
34437
|
+
credit(guard.cond.replace("!=", "=="));
|
|
34438
|
+
}
|
|
34439
|
+
return proven;
|
|
34440
|
+
}
|
|
34441
|
+
safeBuilders(credited, sinkLine) {
|
|
34442
|
+
const scopeStart = this.methodAt(sinkLine)?.start_line ?? 1;
|
|
34443
|
+
const onlySafe = (expr) => {
|
|
34444
|
+
const idents = expr.replace(/\$?@?"(?:[^"\\]|\\.)*"/g, " ").replace(/'(?:[^'\\]|\\.)'/g, " ").replace(/\.\s*[A-Za-z_]\w*\s*\(\s*\)/g, " ").match(/[A-Za-z_]\w*/g) ?? [];
|
|
34445
|
+
return idents.every((id) => credited.has(id));
|
|
34446
|
+
};
|
|
34447
|
+
const state = new Map;
|
|
34448
|
+
const declRe = new RegExp(`\\b(?:StringBuilder|var)\\s+(${IDENT})\\s*=\\s*new\\s+(?:System\\s*\\.\\s*Text\\s*\\.\\s*)?StringBuilder\\s*\\(`);
|
|
34449
|
+
const writeRe = new RegExp(`^\\s*(${IDENT})\\s*\\.\\s*(?:Append|AppendLine|AppendFormat|Insert)\\s*\\(`);
|
|
34450
|
+
for (const st of this.statements(scopeStart, sinkLine)) {
|
|
34451
|
+
const decl = declRe.exec(st.text);
|
|
34452
|
+
if (decl) {
|
|
34453
|
+
const args2 = insideParens(st.text, decl.index + decl[0].length - 1) ?? "";
|
|
34454
|
+
state.set(decl[1], onlySafe(args2));
|
|
34455
|
+
continue;
|
|
34456
|
+
}
|
|
34457
|
+
const write = writeRe.exec(st.text);
|
|
34458
|
+
if (write && state.has(write[1])) {
|
|
34459
|
+
const args2 = insideParens(st.text, write[0].length - 1);
|
|
34460
|
+
if (args2 === null || !onlySafe(args2))
|
|
34461
|
+
state.set(write[1], false);
|
|
34462
|
+
}
|
|
34463
|
+
}
|
|
34464
|
+
return new Set([...state].filter(([, safe]) => safe).map(([name2]) => name2));
|
|
34465
|
+
}
|
|
34466
|
+
creditedOnLine(line) {
|
|
34467
|
+
const text = this.sinkText(line);
|
|
34468
|
+
const types = new Set(CSHARP_SANITIZER_RES.map((r) => r.type));
|
|
34469
|
+
for (const t of INERT_WHEN_ALPHANUMERIC)
|
|
34470
|
+
types.add(t);
|
|
34471
|
+
types.add("ssrf");
|
|
34472
|
+
for (const type of types) {
|
|
34473
|
+
if (this.sanitizes(type, text))
|
|
34474
|
+
return true;
|
|
34475
|
+
const credited = new Set([...this.sanitizedVarsAt(type, line), ...this.guardedVarsAt(type, line)]);
|
|
34476
|
+
for (const v of this.safeBuilders(credited, line))
|
|
34477
|
+
credited.add(v);
|
|
34478
|
+
for (const v of credited)
|
|
34479
|
+
if (mentions(text, v))
|
|
34480
|
+
return true;
|
|
34481
|
+
}
|
|
34482
|
+
return this.fixedHostRequest(line);
|
|
34483
|
+
}
|
|
34484
|
+
fixedHostRequest(sinkLine) {
|
|
34485
|
+
const fixedLiteral = '\\$?@?"https?:\\/\\/[A-Za-z0-9.\\-]+(?::\\d+)?[\\/?#][^"]*"';
|
|
34486
|
+
const text = this.lines[sinkLine - 1] ?? "";
|
|
34487
|
+
const argStart = `\\(\\s*(?:new\\s+(?:System\\s*\\.\\s*)?Uri\\s*\\(\\s*)?`;
|
|
34488
|
+
if (new RegExp(argStart + fixedLiteral).test(text))
|
|
34489
|
+
return true;
|
|
34490
|
+
const scopeStart = this.methodAt(sinkLine)?.start_line ?? 1;
|
|
34491
|
+
const fixed = new Set;
|
|
34492
|
+
for (const st of this.statements(scopeStart, sinkLine)) {
|
|
34493
|
+
const m = ASSIGN_RE.exec(st.text);
|
|
34494
|
+
if (!m)
|
|
34495
|
+
continue;
|
|
34496
|
+
if (new RegExp("^(?:new\\s+(?:System\\s*\\.\\s*)?Uri\\s*\\(\\s*)?" + fixedLiteral).test(m[2].trim()))
|
|
34497
|
+
fixed.add(m[1]);
|
|
34498
|
+
else
|
|
34499
|
+
fixed.delete(m[1]);
|
|
34500
|
+
}
|
|
34501
|
+
for (const v of fixed) {
|
|
34502
|
+
if (new RegExp(argStart + escapeRegex(v) + "\\s*[,)]").test(text))
|
|
34503
|
+
return true;
|
|
34504
|
+
}
|
|
34505
|
+
return false;
|
|
34506
|
+
}
|
|
34507
|
+
}
|
|
34508
|
+
|
|
34065
34509
|
// ../circle-ir/dist/analysis/passes/sink-filter-pass.js
|
|
34066
34510
|
function escapeReSf(s) {
|
|
34067
34511
|
return s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
@@ -34364,7 +34808,6 @@ function jsSsrfHostGuardedLines(code) {
|
|
|
34364
34808
|
}
|
|
34365
34809
|
return covered;
|
|
34366
34810
|
}
|
|
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
34811
|
function isUnwrittenLocalMapKeyRead(code, source) {
|
|
34369
34812
|
if (source.type !== "plugin_param")
|
|
34370
34813
|
return false;
|
|
@@ -34391,15 +34834,6 @@ function isUnwrittenLocalMapKeyRead(code, source) {
|
|
|
34391
34834
|
return false;
|
|
34392
34835
|
return true;
|
|
34393
34836
|
}
|
|
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
34837
|
function csharpSanitizedVarsByType(code) {
|
|
34404
34838
|
const lines = code.split(`
|
|
34405
34839
|
`);
|
|
@@ -34432,7 +34866,7 @@ function csharpSanitizedVarsByType(code) {
|
|
|
34432
34866
|
for (const [type, set] of byType) {
|
|
34433
34867
|
if (set.has(lhs))
|
|
34434
34868
|
continue;
|
|
34435
|
-
const refsSanitized = [...set].some((v) => new RegExp(`(?<![\\w])${
|
|
34869
|
+
const refsSanitized = [...set].some((v) => new RegExp(`(?<![\\w])${escapeRegex2(v)}(?![\\w])`).test(rhs));
|
|
34436
34870
|
if (refsSanitized) {
|
|
34437
34871
|
set.add(lhs);
|
|
34438
34872
|
changed = true;
|
|
@@ -35328,22 +35762,34 @@ class SinkFilterPass {
|
|
|
35328
35762
|
const sourceLines = ctx.code.split(`
|
|
35329
35763
|
`);
|
|
35330
35764
|
const sanitizedByType = csharpSanitizedVarsByType(ctx.code);
|
|
35765
|
+
const csFields = new Set(ctx.graph.ir.types.flatMap((t) => (t.fields ?? []).map((f) => f.name)));
|
|
35766
|
+
const credit = new CSharpCredit(ctx.code, ctx.graph.ir.types.flatMap((t) => t.methods));
|
|
35331
35767
|
const xxeHardened = /\bXmlResolver\s*=\s*null\b/.test(ctx.code) || /\bDtdProcessing\s*=\s*(?:DtdProcessing\s*\.\s*)?(?:Prohibit|Ignore)\b/.test(ctx.code);
|
|
35332
35768
|
filtered = filtered.filter((sink) => {
|
|
35333
35769
|
if (sink.type === "xxe" && xxeHardened)
|
|
35334
35770
|
return false;
|
|
35335
35771
|
const sinkLineText = sourceLines[sink.line - 1] ?? "";
|
|
35336
|
-
|
|
35337
|
-
|
|
35772
|
+
if (credit.sanitizes(sink.type, sinkLineText))
|
|
35773
|
+
return false;
|
|
35774
|
+
if (sink.type === "ssrf" && credit.fixedHostRequest(sink.line))
|
|
35775
|
+
return false;
|
|
35776
|
+
const scoped = credit.sanitizedVarsAt(sink.type, sink.line);
|
|
35777
|
+
const fileWide = sanitizedByType.get(sink.type);
|
|
35778
|
+
const sanitizedVars = new Set(scoped);
|
|
35779
|
+
if (fileWide) {
|
|
35780
|
+
for (const v of fileWide)
|
|
35781
|
+
if (csFields.has(v))
|
|
35782
|
+
sanitizedVars.add(v);
|
|
35783
|
+
}
|
|
35784
|
+
for (const v of credit.guardedVarsAt(sink.type, sink.line))
|
|
35785
|
+
sanitizedVars.add(v);
|
|
35786
|
+
for (const v of credit.safeBuilders(sanitizedVars, sink.line))
|
|
35787
|
+
sanitizedVars.add(v);
|
|
35788
|
+
const received = credit.sinkText(sink.line);
|
|
35789
|
+
for (const v of sanitizedVars) {
|
|
35790
|
+
if (new RegExp(`(?<![\\w])${escapeRegex2(v)}(?![\\w])`).test(received))
|
|
35338
35791
|
return false;
|
|
35339
35792
|
}
|
|
35340
|
-
const sanitizedVars = sanitizedByType.get(sink.type);
|
|
35341
|
-
if (sanitizedVars && sanitizedVars.size > 0) {
|
|
35342
|
-
for (const v of sanitizedVars) {
|
|
35343
|
-
if (new RegExp(`(?<![\\w])${escapeRegex(v)}(?![\\w])`).test(sinkLineText))
|
|
35344
|
-
return false;
|
|
35345
|
-
}
|
|
35346
|
-
}
|
|
35347
35793
|
return true;
|
|
35348
35794
|
});
|
|
35349
35795
|
}
|
|
@@ -35351,7 +35797,7 @@ class SinkFilterPass {
|
|
|
35351
35797
|
const sourceLines = ctx.code.split(`
|
|
35352
35798
|
`);
|
|
35353
35799
|
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*\)/;
|
|
35800
|
+
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
35801
|
for (const line of sourceLines) {
|
|
35356
35802
|
const m = declRe.exec(line);
|
|
35357
35803
|
if (m)
|
|
@@ -35365,14 +35811,22 @@ class SinkFilterPass {
|
|
|
35365
35811
|
const text = sourceLines[sink.line - 1] ?? "";
|
|
35366
35812
|
let onConstClient = false;
|
|
35367
35813
|
for (const v of constBaseClients) {
|
|
35368
|
-
if (new RegExp(`(?<![\\w])${
|
|
35814
|
+
if (new RegExp(`(?<![\\w])${escapeRegex2(v)}\\s*\\.`).test(text)) {
|
|
35369
35815
|
onConstClient = true;
|
|
35370
35816
|
break;
|
|
35371
35817
|
}
|
|
35372
35818
|
}
|
|
35373
35819
|
if (!onConstClient)
|
|
35374
35820
|
return true;
|
|
35375
|
-
|
|
35821
|
+
let m = relativeLiteralArgRe.exec(text);
|
|
35822
|
+
if (!m) {
|
|
35823
|
+
const arg = /Async\s*\(\s*([A-Za-z_]\w*)\s*[,)]/.exec(text)?.[1];
|
|
35824
|
+
if (arg) {
|
|
35825
|
+
const assigned = new RegExp(`\\b${escapeRegex2(arg)}\\s*=\\s*(?:@?\\$?)"([^"]*)"`);
|
|
35826
|
+
for (let i2 = sink.line - 2;i2 >= 0 && !m; i2--)
|
|
35827
|
+
m = assigned.exec(sourceLines[i2] ?? "");
|
|
35828
|
+
}
|
|
35829
|
+
}
|
|
35376
35830
|
if (!m)
|
|
35377
35831
|
return true;
|
|
35378
35832
|
const lit = m[1];
|
|
@@ -35408,7 +35862,7 @@ class SinkFilterPass {
|
|
|
35408
35862
|
const wrappers = findPythonLdapStripWrappers(sourceLines);
|
|
35409
35863
|
const allLdapSanitizers = [...PY_BUILTIN_LDAP_SANITIZERS, ...wrappers];
|
|
35410
35864
|
if (allLdapSanitizers.length > 0) {
|
|
35411
|
-
const ldapPatterns = allLdapSanitizers.map((name2) => new RegExp(`\\b${
|
|
35865
|
+
const ldapPatterns = allLdapSanitizers.map((name2) => new RegExp(`\\b${escapeRegex2(name2)}\\s*\\(`));
|
|
35412
35866
|
filtered = filtered.filter((sink) => {
|
|
35413
35867
|
if (sink.type !== "ldap_injection")
|
|
35414
35868
|
return true;
|
|
@@ -35494,7 +35948,7 @@ class SinkFilterPass {
|
|
|
35494
35948
|
return { sources, sinks: filtered, sanitizers };
|
|
35495
35949
|
}
|
|
35496
35950
|
}
|
|
35497
|
-
function
|
|
35951
|
+
function escapeRegex2(s) {
|
|
35498
35952
|
return s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
35499
35953
|
}
|
|
35500
35954
|
var RESERVED_KEYWORDS = new Set([
|
|
@@ -35576,7 +36030,7 @@ function extractSourceIdentifiers(text) {
|
|
|
35576
36030
|
return out2;
|
|
35577
36031
|
}
|
|
35578
36032
|
function isAssignedFromSanitizerPattern(sourceLines, sinkLine, varName, sanitizerPatterns, lookback = 30) {
|
|
35579
|
-
const escapedVar =
|
|
36033
|
+
const escapedVar = escapeRegex2(varName);
|
|
35580
36034
|
const assignRe = new RegExp(`(?:\\b(?:const|let|var|final|String)\\s+)?\\b${escapedVar}\\s*=\\s*(.+)`);
|
|
35581
36035
|
const fnBoundaryRe = /^\s*(?:def\s+\w+\s*\(|function\s+\w+\s*\(|\w+\s*[:=]\s*(?:async\s+)?\(?[^)]*\)?\s*=>)/;
|
|
35582
36036
|
const lo = Math.max(0, sinkLine - 1 - lookback);
|
|
@@ -37145,7 +37599,7 @@ class LibraryProfileXssGatePass {
|
|
|
37145
37599
|
|
|
37146
37600
|
// ../circle-ir/dist/analysis/passes/taint-propagation-pass.js
|
|
37147
37601
|
var CONTAINER_READ_SOURCES = new Set(["plugin_param", "config_param"]);
|
|
37148
|
-
function makeConstPropVeto(constProp, graph, sources, code) {
|
|
37602
|
+
function makeConstPropVeto(constProp, graph, sources, code, language) {
|
|
37149
37603
|
const lines = typeof code === "string" ? code.split(`
|
|
37150
37604
|
`) : [];
|
|
37151
37605
|
const selfReassign = new Map;
|
|
@@ -37182,6 +37636,8 @@ function makeConstPropVeto(constProp, graph, sources, code) {
|
|
|
37182
37636
|
return false;
|
|
37183
37637
|
if (r.reason !== "variable_not_tainted")
|
|
37184
37638
|
return true;
|
|
37639
|
+
if (language === "csharp")
|
|
37640
|
+
return false;
|
|
37185
37641
|
const lo = Math.min(sourceLine, sinkLine);
|
|
37186
37642
|
let d = defAt(step.variable, step.line);
|
|
37187
37643
|
let selfHops = 0;
|
|
@@ -37220,7 +37676,7 @@ class TaintPropagationPass {
|
|
|
37220
37676
|
return { flows: [] };
|
|
37221
37677
|
}
|
|
37222
37678
|
const propagationResult = propagateTaint(graph, sources, sinks, sanitizers);
|
|
37223
|
-
const isVetoed = makeConstPropVeto(constProp, graph, sources, ctx.code);
|
|
37679
|
+
const isVetoed = makeConstPropVeto(constProp, graph, sources, ctx.code, ctx.language);
|
|
37224
37680
|
const verifiedFlows = propagationResult.flows.filter((flow) => {
|
|
37225
37681
|
if (constProp.unreachableLines.has(flow.sink.line))
|
|
37226
37682
|
return false;
|
|
@@ -37790,7 +38246,7 @@ function detectCollectionFlows(calls, sources, sinks, taintedVars, unreachableLi
|
|
|
37790
38246
|
if (source.variable && source.variable !== varName && source.in_method && call.in_method && source.in_method !== call.in_method) {
|
|
37791
38247
|
continue;
|
|
37792
38248
|
}
|
|
37793
|
-
if (typeof code === "string" && isReassignedToLiteralBetween(code, varName, source.line, sink.line)) {
|
|
38249
|
+
if (typeof code === "string" && isReassignedToLiteralBetween(code, varName, source.line, sink.line, unreachableLines)) {
|
|
37794
38250
|
continue;
|
|
37795
38251
|
}
|
|
37796
38252
|
flows.push({
|
|
@@ -37830,7 +38286,7 @@ function detectCollectionFlows(calls, sources, sinks, taintedVars, unreachableLi
|
|
|
37830
38286
|
if (source.variable && source.variable !== collectionVar && source.in_method && call.in_method && source.in_method !== call.in_method) {
|
|
37831
38287
|
continue;
|
|
37832
38288
|
}
|
|
37833
|
-
if (typeof code === "string" && isReassignedToLiteralBetween(code, collectionVar, source.line, sink.line)) {
|
|
38289
|
+
if (typeof code === "string" && isReassignedToLiteralBetween(code, collectionVar, source.line, sink.line, unreachableLines)) {
|
|
37834
38290
|
continue;
|
|
37835
38291
|
}
|
|
37836
38292
|
flows.push({
|
|
@@ -37949,7 +38405,7 @@ function detectParameterSinkFlows(types, calls, sources, sinks, unreachableLines
|
|
|
37949
38405
|
if (paramSource) {
|
|
37950
38406
|
const exists = flows.some((f) => f.source_line === paramSource.line && f.sink_line === sink.line && f.sink_type === sink.type);
|
|
37951
38407
|
if (!exists) {
|
|
37952
|
-
if (typeof code === "string" && isReassignedToLiteralBetween(code, arg.variable, paramSource.line, sink.line)) {
|
|
38408
|
+
if (typeof code === "string" && isReassignedToLiteralBetween(code, arg.variable, paramSource.line, sink.line, unreachableLines)) {
|
|
37953
38409
|
continue;
|
|
37954
38410
|
}
|
|
37955
38411
|
flows.push({
|
|
@@ -37972,6 +38428,96 @@ function detectParameterSinkFlows(types, calls, sources, sinks, unreachableLines
|
|
|
37972
38428
|
}
|
|
37973
38429
|
return flows;
|
|
37974
38430
|
}
|
|
38431
|
+
function csharpMethodScopedAliases(code, seeds, methods, fieldNames) {
|
|
38432
|
+
const lines = code.split(`
|
|
38433
|
+
`);
|
|
38434
|
+
const methodOf = (line) => {
|
|
38435
|
+
let best = -1;
|
|
38436
|
+
let span = Infinity;
|
|
38437
|
+
methods.forEach((m, i2) => {
|
|
38438
|
+
if (line >= m.start_line && line <= m.end_line && m.end_line - m.start_line < span) {
|
|
38439
|
+
best = i2;
|
|
38440
|
+
span = m.end_line - m.start_line;
|
|
38441
|
+
}
|
|
38442
|
+
});
|
|
38443
|
+
return best;
|
|
38444
|
+
};
|
|
38445
|
+
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*$/;
|
|
38446
|
+
const assignRe = /^\s*(?:this\s*\.\s*)?([A-Za-z_]\w*)\s*=\s*(.+?);\s*$/;
|
|
38447
|
+
const returnRe = /^\s*return\s+(.+?);\s*$/;
|
|
38448
|
+
const KEYWORDS = new Set(["return", "throw", "new", "await", "yield", "else", "case", "using", "var"]);
|
|
38449
|
+
const codeOf = (rhs) => rhs.replace(/\$@?"(?:[^"\\]|\\.)*"/g, (lit) => " " + [...lit.matchAll(/\{([^}]*)\}/g)].map((x) => x[1]).join(" ") + " ").replace(/@?"(?:[^"\\]|\\.)*"/g, " ").replace(/'(?:[^'\\]|\\.)'/g, " ");
|
|
38450
|
+
const local = new Map;
|
|
38451
|
+
const taintedFields = new Set;
|
|
38452
|
+
const returnsTainted = new Set;
|
|
38453
|
+
const localsOf = (m) => {
|
|
38454
|
+
let set = local.get(m);
|
|
38455
|
+
if (!set) {
|
|
38456
|
+
set = new Set;
|
|
38457
|
+
local.set(m, set);
|
|
38458
|
+
}
|
|
38459
|
+
return set;
|
|
38460
|
+
};
|
|
38461
|
+
for (const seed of seeds) {
|
|
38462
|
+
const base = seed.variable.replace(/^this\./, "");
|
|
38463
|
+
if (fieldNames.has(base))
|
|
38464
|
+
taintedFields.add(base);
|
|
38465
|
+
else
|
|
38466
|
+
localsOf(methodOf(seed.line)).add(seed.variable);
|
|
38467
|
+
}
|
|
38468
|
+
const out2 = [];
|
|
38469
|
+
let changed = true;
|
|
38470
|
+
for (let guard = 0;changed && guard < lines.length + 2; guard++) {
|
|
38471
|
+
changed = false;
|
|
38472
|
+
for (let i2 = 0;i2 < lines.length; i2++) {
|
|
38473
|
+
let text = lines[i2];
|
|
38474
|
+
const trimmed = text.trimStart();
|
|
38475
|
+
if (trimmed.startsWith("//") || trimmed.startsWith("*"))
|
|
38476
|
+
continue;
|
|
38477
|
+
if (/^\s*(?:[\w.<>\[\]?]+\s+)?[A-Za-z_]\w*\s*=(?!=)/.test(text) && !/;\s*(?:\/\/.*)?$/.test(text)) {
|
|
38478
|
+
for (let j = i2 + 1;j < lines.length && j <= i2 + 12; j++) {
|
|
38479
|
+
text = text.trimEnd() + " " + lines[j].trim();
|
|
38480
|
+
if (/;\s*(?:\/\/.*)?$/.test(lines[j]))
|
|
38481
|
+
break;
|
|
38482
|
+
}
|
|
38483
|
+
}
|
|
38484
|
+
const m = methodOf(i2 + 1);
|
|
38485
|
+
const here = localsOf(m);
|
|
38486
|
+
const ret = returnRe.exec(text);
|
|
38487
|
+
if (ret && m !== -1 && !returnsTainted.has(methods[m].name)) {
|
|
38488
|
+
const idents2 = codeOf(ret[1]).match(/[\p{L}\p{N}_]+/gu) ?? [];
|
|
38489
|
+
if (idents2.some((t) => here.has(t) || taintedFields.has(t))) {
|
|
38490
|
+
returnsTainted.add(methods[m].name);
|
|
38491
|
+
changed = true;
|
|
38492
|
+
}
|
|
38493
|
+
continue;
|
|
38494
|
+
}
|
|
38495
|
+
const d = declRe.exec(text) ?? assignRe.exec(text);
|
|
38496
|
+
if (!d)
|
|
38497
|
+
continue;
|
|
38498
|
+
const [, lhs, rhs] = d;
|
|
38499
|
+
if (KEYWORDS.has(lhs))
|
|
38500
|
+
continue;
|
|
38501
|
+
const isField = fieldNames.has(lhs) && !declRe.test(text);
|
|
38502
|
+
if (isField ? taintedFields.has(lhs) : here.has(lhs))
|
|
38503
|
+
continue;
|
|
38504
|
+
const rhsCode = codeOf(rhs);
|
|
38505
|
+
const idents = rhsCode.match(/[\p{L}\p{N}_]+/gu) ?? [];
|
|
38506
|
+
const viaLocal = idents.some((t) => here.has(t));
|
|
38507
|
+
const viaField = idents.some((t) => taintedFields.has(t) && !here.has(t));
|
|
38508
|
+
const viaCall = [...rhsCode.matchAll(/([\p{L}_][\p{L}\p{N}_]*)\s*\(/gu)].some((c) => returnsTainted.has(c[1]));
|
|
38509
|
+
if (!viaLocal && !viaField && !viaCall)
|
|
38510
|
+
continue;
|
|
38511
|
+
if (isField)
|
|
38512
|
+
taintedFields.add(lhs);
|
|
38513
|
+
else
|
|
38514
|
+
here.add(lhs);
|
|
38515
|
+
out2.push({ variable: lhs, line: i2 + 1, carried: !viaLocal });
|
|
38516
|
+
changed = true;
|
|
38517
|
+
}
|
|
38518
|
+
}
|
|
38519
|
+
return out2;
|
|
38520
|
+
}
|
|
37975
38521
|
var _reassignSplitCode;
|
|
37976
38522
|
var _reassignSplitLines;
|
|
37977
38523
|
function splitLinesCached(code) {
|
|
@@ -37982,7 +38528,7 @@ function splitLinesCached(code) {
|
|
|
37982
38528
|
`);
|
|
37983
38529
|
return _reassignSplitLines;
|
|
37984
38530
|
}
|
|
37985
|
-
function isReassignedToLiteralBetween(code, variable, srcLine, sinkLine) {
|
|
38531
|
+
function isReassignedToLiteralBetween(code, variable, srcLine, sinkLine, unreachableLines) {
|
|
37986
38532
|
if (!variable || sinkLine - srcLine < 2)
|
|
37987
38533
|
return false;
|
|
37988
38534
|
if (!/^[A-Za-z_][\w]*$/.test(variable))
|
|
@@ -38002,49 +38548,117 @@ function isReassignedToLiteralBetween(code, variable, srcLine, sinkLine) {
|
|
|
38002
38548
|
return true;
|
|
38003
38549
|
if (!reNaked.test(line) && !reSwitchCase.test(line))
|
|
38004
38550
|
continue;
|
|
38005
|
-
if (
|
|
38551
|
+
if (siblingBranchAssignsNonLiteral(lines, i2, variable, strLit, unreachableLines))
|
|
38006
38552
|
continue;
|
|
38007
38553
|
return true;
|
|
38008
38554
|
}
|
|
38009
38555
|
return false;
|
|
38010
38556
|
}
|
|
38011
|
-
function
|
|
38557
|
+
function enclosingBrace(lines, line, col) {
|
|
38012
38558
|
let depth = 0;
|
|
38013
|
-
let
|
|
38014
|
-
|
|
38015
|
-
|
|
38016
|
-
|
|
38017
|
-
if (line[c] === "}")
|
|
38559
|
+
for (let i2 = line;i2 >= 0; i2--) {
|
|
38560
|
+
const text = lines[i2];
|
|
38561
|
+
for (let c = i2 === line ? Math.min(col, text.length) - 1 : text.length - 1;c >= 0; c--) {
|
|
38562
|
+
if (text[c] === "}")
|
|
38018
38563
|
depth++;
|
|
38019
|
-
else if (
|
|
38564
|
+
else if (text[c] === "{") {
|
|
38565
|
+
if (depth === 0) {
|
|
38566
|
+
let header = text.slice(0, c).trim();
|
|
38567
|
+
let headerLine = i2;
|
|
38568
|
+
for (let h = i2 - 1;header === "" && h >= 0; h--) {
|
|
38569
|
+
header = lines[h].trim();
|
|
38570
|
+
headerLine = h;
|
|
38571
|
+
}
|
|
38572
|
+
return { line: i2, col: c, header, headerLine };
|
|
38573
|
+
}
|
|
38020
38574
|
depth--;
|
|
38575
|
+
}
|
|
38021
38576
|
}
|
|
38022
|
-
|
|
38023
|
-
|
|
38024
|
-
|
|
38025
|
-
|
|
38577
|
+
}
|
|
38578
|
+
return null;
|
|
38579
|
+
}
|
|
38580
|
+
function matchingClose(lines, line, col) {
|
|
38581
|
+
let depth = 0;
|
|
38582
|
+
for (let i2 = line;i2 < lines.length; i2++) {
|
|
38583
|
+
const text = lines[i2];
|
|
38584
|
+
for (let c = i2 === line ? col : 0;c < text.length; c++) {
|
|
38585
|
+
if (text[c] === "{")
|
|
38586
|
+
depth++;
|
|
38587
|
+
else if (text[c] === "}" && --depth === 0)
|
|
38588
|
+
return { line: i2, col: c };
|
|
38026
38589
|
}
|
|
38027
38590
|
}
|
|
38028
|
-
|
|
38591
|
+
return null;
|
|
38592
|
+
}
|
|
38593
|
+
function siblingBranchAssignsNonLiteral(lines, idx, variable, strLit, unreachableLines) {
|
|
38594
|
+
let block = enclosingBrace(lines, idx, 0);
|
|
38595
|
+
if (block && /^(?:case\b.*|default\s*):$/.test(block.header)) {
|
|
38596
|
+
block = enclosingBrace(lines, block.line, block.col);
|
|
38597
|
+
}
|
|
38598
|
+
if (!block)
|
|
38599
|
+
return false;
|
|
38600
|
+
let first = block.headerLine;
|
|
38601
|
+
let last;
|
|
38602
|
+
const end = matchingClose(lines, block.line, block.col);
|
|
38603
|
+
if (!end)
|
|
38029
38604
|
return false;
|
|
38605
|
+
last = end.line;
|
|
38606
|
+
if (/\bswitch\b/.test(block.header)) {} else if (/^(?:\}\s*)?else\b/.test(block.header) || /^(?:\}\s*)?(?:else\s+)?if\s*\(/.test(block.header)) {
|
|
38607
|
+
let cur = block;
|
|
38608
|
+
for (let guard = 0;guard < 64 && /^(?:\}\s*)?else\b/.test(cur.header); guard++) {
|
|
38609
|
+
let closeLine = cur.headerLine;
|
|
38610
|
+
let closeCol = lines[closeLine].indexOf("}");
|
|
38611
|
+
if (closeCol === -1 || closeCol > lines[closeLine].search(/\belse\b/)) {
|
|
38612
|
+
closeLine--;
|
|
38613
|
+
while (closeLine >= 0 && lines[closeLine].trim() === "")
|
|
38614
|
+
closeLine--;
|
|
38615
|
+
if (closeLine < 0 || !lines[closeLine].trimEnd().endsWith("}"))
|
|
38616
|
+
return false;
|
|
38617
|
+
closeCol = lines[closeLine].lastIndexOf("}");
|
|
38618
|
+
}
|
|
38619
|
+
const prev = enclosingBrace(lines, closeLine, closeCol);
|
|
38620
|
+
if (!prev)
|
|
38621
|
+
return false;
|
|
38622
|
+
cur = prev;
|
|
38623
|
+
first = prev.headerLine;
|
|
38624
|
+
}
|
|
38625
|
+
let close = end;
|
|
38626
|
+
for (let guard = 0;guard < 64; guard++) {
|
|
38627
|
+
let l = close.line;
|
|
38628
|
+
let rest = lines[l].slice(close.col + 1).trim();
|
|
38629
|
+
while (rest === "" && l + 1 < lines.length) {
|
|
38630
|
+
l++;
|
|
38631
|
+
rest = lines[l].trim();
|
|
38632
|
+
}
|
|
38633
|
+
if (!/^else\b/.test(rest))
|
|
38634
|
+
break;
|
|
38635
|
+
let openLine = l;
|
|
38636
|
+
let openCol = lines[l].indexOf("{", l === close.line ? close.col + 1 : 0);
|
|
38637
|
+
if (openCol === -1) {
|
|
38638
|
+
openLine = l + 1;
|
|
38639
|
+
while (openLine < lines.length && lines[openLine].trim() === "")
|
|
38640
|
+
openLine++;
|
|
38641
|
+
if (openLine >= lines.length || !lines[openLine].trimStart().startsWith("{"))
|
|
38642
|
+
break;
|
|
38643
|
+
openCol = lines[openLine].indexOf("{");
|
|
38644
|
+
}
|
|
38645
|
+
const next = matchingClose(lines, openLine, openCol);
|
|
38646
|
+
if (!next)
|
|
38647
|
+
break;
|
|
38648
|
+
close = next;
|
|
38649
|
+
last = next.line;
|
|
38650
|
+
}
|
|
38651
|
+
} else {
|
|
38652
|
+
return false;
|
|
38653
|
+
}
|
|
38030
38654
|
const assign = new RegExp(`(?:^|[^\\w.$])${variable}\\s*(?::?=)(?!=)\\s*(.*?)\\s*;?\\s*(?:break\\s*;?)?\\s*$`);
|
|
38031
38655
|
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;
|
|
38656
|
+
for (let i2 = first;i2 <= last; i2++) {
|
|
38657
|
+
if (i2 === idx || unreachableLines?.has(i2 + 1))
|
|
38658
|
+
continue;
|
|
38659
|
+
const m = assign.exec(lines[i2]);
|
|
38660
|
+
if (m && m[1] && !literalOnly.test(m[1]))
|
|
38661
|
+
return true;
|
|
38048
38662
|
}
|
|
38049
38663
|
return false;
|
|
38050
38664
|
}
|
|
@@ -38172,6 +38786,22 @@ function detectExpressionScanFlows(calls, sources, sinks, sanitizers, unreachabl
|
|
|
38172
38786
|
const goFnStart = (line) => enclosingStart(goFnRanges, line);
|
|
38173
38787
|
const csFnRanges = methodRanges(["csharp"]);
|
|
38174
38788
|
const csFnStart = (line) => enclosingStart(csFnRanges, line);
|
|
38789
|
+
const csFieldNames = new Set;
|
|
38790
|
+
if (language === "csharp") {
|
|
38791
|
+
for (const t of types ?? [])
|
|
38792
|
+
for (const f of t.fields ?? [])
|
|
38793
|
+
csFieldNames.add(f.name);
|
|
38794
|
+
}
|
|
38795
|
+
const csDifferentMethod = (variable, sourceLine, otherLine) => {
|
|
38796
|
+
if (csFnRanges.length === 0)
|
|
38797
|
+
return false;
|
|
38798
|
+
const a = csFnStart(sourceLine);
|
|
38799
|
+
const b = csFnStart(otherLine);
|
|
38800
|
+
if (a === -1 || b === -1 || a === b)
|
|
38801
|
+
return false;
|
|
38802
|
+
const base = variable.replace(/^this\./, "").split(".")[0];
|
|
38803
|
+
return !csFieldNames.has(base);
|
|
38804
|
+
};
|
|
38175
38805
|
const sourcesWithVar = sources.filter((s) => typeof s.variable === "string" && s.variable.length > 0);
|
|
38176
38806
|
const aliasSanitizedFor = new Map;
|
|
38177
38807
|
if (language === "python" && typeof code === "string") {
|
|
@@ -38444,7 +39074,29 @@ function detectExpressionScanFlows(calls, sources, sinks, sanitizers, unreachabl
|
|
|
38444
39074
|
}
|
|
38445
39075
|
}
|
|
38446
39076
|
}
|
|
38447
|
-
if (
|
|
39077
|
+
if (language === "csharp" && typeof code === "string" && sourcesWithVar.length > 0 && csFnRanges.length > 0) {
|
|
39078
|
+
const csMethods = (types ?? []).flatMap((t) => t.methods);
|
|
39079
|
+
const seeds = sourcesWithVar.map((sv) => ({ variable: sv.variable, line: sv.line }));
|
|
39080
|
+
const methodStartOf = (line) => csFnStart(line);
|
|
39081
|
+
const aliases = csharpMethodScopedAliases(code, seeds, csMethods, csFieldNames);
|
|
39082
|
+
const fileAnchor = sourcesWithVar.reduce((a, sv) => sv.line < a.line ? sv : a, sourcesWithVar[0]);
|
|
39083
|
+
const have = new Set(sourcesWithVar.map((sv) => `${sv.variable}@${methodStartOf(sv.line)}`));
|
|
39084
|
+
for (const alias of aliases) {
|
|
39085
|
+
const fn = methodStartOf(alias.line);
|
|
39086
|
+
const isField = csFieldNames.has(alias.variable);
|
|
39087
|
+
const key = `${alias.variable}@${isField ? "field" : fn}`;
|
|
39088
|
+
if (have.has(key))
|
|
39089
|
+
continue;
|
|
39090
|
+
have.add(key);
|
|
39091
|
+
const inMethod = sourcesWithVar.filter((sv) => methodStartOf(sv.line) === fn && sv.line <= alias.line);
|
|
39092
|
+
const from = !alias.carried && inMethod.length > 0 ? inMethod.reduce((a, sv) => sv.line < a.line ? sv : a, inMethod[0]) : fileAnchor;
|
|
39093
|
+
sourcesWithVar.push({
|
|
39094
|
+
...from,
|
|
39095
|
+
variable: alias.variable,
|
|
39096
|
+
...alias.carried || inMethod.length === 0 ? { line: alias.line } : {}
|
|
39097
|
+
});
|
|
39098
|
+
}
|
|
39099
|
+
} else if ((language === "java" || language === "csharp") && typeof code === "string" && sourcesWithVar.length > 0) {
|
|
38448
39100
|
const seedVars = new Set(sourcesWithVar.map((s) => s.variable));
|
|
38449
39101
|
const derived = buildJavaTaintedVars(code, seedVars);
|
|
38450
39102
|
if (derived.size > 0) {
|
|
@@ -38493,23 +39145,26 @@ function detectExpressionScanFlows(calls, sources, sinks, sanitizers, unreachabl
|
|
|
38493
39145
|
const lines = code.split(`
|
|
38494
39146
|
`);
|
|
38495
39147
|
const ctRe = /(\w+)\s*\.\s*CommandText\s*\+?=\s*(.+)/;
|
|
38496
|
-
const
|
|
39148
|
+
const seeded = new Set;
|
|
38497
39149
|
let added = true;
|
|
38498
39150
|
let guard = 0;
|
|
38499
39151
|
while (added && guard < lines.length + 2) {
|
|
38500
39152
|
added = false;
|
|
38501
39153
|
guard++;
|
|
38502
|
-
const known = new Set(sourcesWithVar.map((s) => s.variable));
|
|
38503
39154
|
for (let i2 = 0;i2 < lines.length; i2++) {
|
|
38504
39155
|
const m = ctRe.exec(lines[i2]);
|
|
38505
39156
|
if (!m)
|
|
38506
39157
|
continue;
|
|
38507
39158
|
const [, obj, rhs] = m;
|
|
38508
|
-
|
|
39159
|
+
const key = `${obj}@${csFnStart(i2 + 1)}`;
|
|
39160
|
+
if (seeded.has(key))
|
|
38509
39161
|
continue;
|
|
38510
|
-
|
|
39162
|
+
const reachable = sourcesWithVar.filter((sv) => sv.line <= i2 + 1 && !csDifferentMethod(sv.variable, sv.line, i2 + 1));
|
|
39163
|
+
if (reachable.length === 0 || !argsReferenceTaintedVar(rhs, reachable))
|
|
38511
39164
|
continue;
|
|
38512
|
-
|
|
39165
|
+
const from = reachable.reduce((a, sv) => sv.line < a.line ? sv : a, reachable[0]);
|
|
39166
|
+
sourcesWithVar.push({ ...from, variable: obj, line: i2 + 1 });
|
|
39167
|
+
seeded.add(key);
|
|
38513
39168
|
added = true;
|
|
38514
39169
|
}
|
|
38515
39170
|
}
|
|
@@ -38528,24 +39183,26 @@ function detectExpressionScanFlows(calls, sources, sinks, sanitizers, unreachabl
|
|
|
38528
39183
|
if (builders.size > 0) {
|
|
38529
39184
|
const appendRe = /([\p{L}_][\p{L}\p{N}_]*)\s*\.\s*(?:Append|AppendLine|AppendFormat|Insert)\s*\(([\s\S]*)/u;
|
|
38530
39185
|
const anchor = sourcesWithVar.reduce((a, s) => s.line < a.line ? s : a, sourcesWithVar[0]);
|
|
39186
|
+
const builderSeeded = new Set;
|
|
38531
39187
|
let added = true;
|
|
38532
39188
|
let guard = 0;
|
|
38533
39189
|
while (added && guard < lines.length + 2) {
|
|
38534
39190
|
added = false;
|
|
38535
39191
|
guard++;
|
|
38536
|
-
const known = new Set(sourcesWithVar.map((s) => s.variable));
|
|
38537
39192
|
for (let i2 = 0;i2 < lines.length; i2++) {
|
|
38538
39193
|
const m = appendRe.exec(lines[i2]);
|
|
38539
39194
|
if (!m)
|
|
38540
39195
|
continue;
|
|
38541
39196
|
const [, obj, args2] = m;
|
|
38542
|
-
|
|
39197
|
+
const seedKey = `${obj}@${csFnStart(i2 + 1)}`;
|
|
39198
|
+
if (!builders.has(obj) || builderSeeded.has(seedKey))
|
|
38543
39199
|
continue;
|
|
38544
39200
|
const appendFn = csFnStart(i2 + 1);
|
|
38545
39201
|
const reachable = sourcesWithVar.filter((sv) => appendFn === -1 || csFnStart(sv.line) === -1 || csFnStart(sv.line) === appendFn);
|
|
38546
39202
|
if (reachable.length > 0 && argsReferenceTaintedVar(args2, reachable)) {
|
|
38547
39203
|
sourcesWithVar.push({ ...anchor, variable: obj, line: i2 + 1 });
|
|
38548
39204
|
builderSeeds.add(`${obj}@${i2 + 1}`);
|
|
39205
|
+
builderSeeded.add(seedKey);
|
|
38549
39206
|
added = true;
|
|
38550
39207
|
}
|
|
38551
39208
|
}
|
|
@@ -38628,6 +39285,8 @@ function detectExpressionScanFlows(calls, sources, sinks, sanitizers, unreachabl
|
|
|
38628
39285
|
if (srcFn !== sinkFn)
|
|
38629
39286
|
continue;
|
|
38630
39287
|
}
|
|
39288
|
+
if (csDifferentMethod(source.variable, source.line, sink.line))
|
|
39289
|
+
continue;
|
|
38631
39290
|
if (complex) {
|
|
38632
39291
|
const re = reCache.get(source.variable);
|
|
38633
39292
|
if (!re || !re.test(expr))
|
|
@@ -38641,7 +39300,7 @@ function detectExpressionScanFlows(calls, sources, sinks, sanitizers, unreachabl
|
|
|
38641
39300
|
if (aliasSanitizedFor.get(source.variable)?.has(sink.type)) {
|
|
38642
39301
|
break;
|
|
38643
39302
|
}
|
|
38644
|
-
if (typeof code === "string" && isReassignedToLiteralBetween(code, source.variable, source.line, sink.line)) {
|
|
39303
|
+
if (typeof code === "string" && isReassignedToLiteralBetween(code, source.variable, source.line, sink.line, unreachableLines)) {
|
|
38645
39304
|
break;
|
|
38646
39305
|
}
|
|
38647
39306
|
flows.push({
|
|
@@ -38948,7 +39607,14 @@ var CSHARP_NON_ESCAPE_METHODS = new Set([
|
|
|
38948
39607
|
"TryAdd",
|
|
38949
39608
|
"Insert",
|
|
38950
39609
|
"Push",
|
|
38951
|
-
"Enqueue"
|
|
39610
|
+
"Enqueue",
|
|
39611
|
+
"EscapeDataString",
|
|
39612
|
+
"EscapeUriString",
|
|
39613
|
+
"UrlEncode",
|
|
39614
|
+
"UrlPathEncode",
|
|
39615
|
+
"HtmlEncode",
|
|
39616
|
+
"HtmlAttributeEncode",
|
|
39617
|
+
"JavaScriptStringEncode"
|
|
38952
39618
|
]);
|
|
38953
39619
|
function isCSharpNonEscapeCall(call) {
|
|
38954
39620
|
const name2 = call.method_name;
|
|
@@ -39520,6 +40186,7 @@ class InterproceduralPass {
|
|
|
39520
40186
|
}
|
|
39521
40187
|
}
|
|
39522
40188
|
const language = graph.ir.meta.language;
|
|
40189
|
+
const csCredit = language === "csharp" && typeof ctx.code === "string" ? new CSharpCredit(ctx.code, graph.ir.types.flatMap((t) => t.methods)) : null;
|
|
39523
40190
|
if (sinks.length > 0) {
|
|
39524
40191
|
const interProc = analyzeInterprocedural2(graph, sources, sinks, sanitizers, {
|
|
39525
40192
|
taintedVariables: constProp.tainted
|
|
@@ -39552,6 +40219,9 @@ class InterproceduralPass {
|
|
|
39552
40219
|
continue;
|
|
39553
40220
|
if (sanitizerMethodNames.has(method.name))
|
|
39554
40221
|
continue;
|
|
40222
|
+
if (language === "csharp" && typeof ctx.code === "string" && !csharpParamReachesSink(ctx.code, method, edge.taintedArgs, sink, csCredit)) {
|
|
40223
|
+
continue;
|
|
40224
|
+
}
|
|
39555
40225
|
for (const source of sources) {
|
|
39556
40226
|
if (source.line > edge.callLine)
|
|
39557
40227
|
continue;
|
|
@@ -39693,9 +40363,58 @@ class InterproceduralPass {
|
|
|
39693
40363
|
return !sanitizedSinkKeys.has(`${s.line}:${s.type}`);
|
|
39694
40364
|
});
|
|
39695
40365
|
}
|
|
40366
|
+
if (csCredit) {
|
|
40367
|
+
const credited = new Set;
|
|
40368
|
+
filteredAdditionalFlows = filteredAdditionalFlows.filter((f) => {
|
|
40369
|
+
if (f.sink_type !== "external_taint_escape" || !csCredit.creditedOnLine(f.sink_line))
|
|
40370
|
+
return true;
|
|
40371
|
+
credited.add(f.sink_line);
|
|
40372
|
+
return false;
|
|
40373
|
+
});
|
|
40374
|
+
filteredAdditionalSinks = filteredAdditionalSinks.filter((s) => !(s.type === "external_taint_escape" && credited.has(s.line)));
|
|
40375
|
+
}
|
|
39696
40376
|
return { additionalSinks: filteredAdditionalSinks, additionalFlows: filteredAdditionalFlows, interprocedural };
|
|
39697
40377
|
}
|
|
39698
40378
|
}
|
|
40379
|
+
function csharpParamReachesSink(code, method, taintedArgs, sink, credit) {
|
|
40380
|
+
const lines = code.split(/\r?\n/);
|
|
40381
|
+
const params = taintedArgs.length > 0 ? method.parameters.filter((p) => taintedArgs.includes(p.position)) : method.parameters;
|
|
40382
|
+
if (params.length === 0)
|
|
40383
|
+
return true;
|
|
40384
|
+
const derived = new Set(params.map((p) => p.name));
|
|
40385
|
+
const codeOf = (text) => text.replace(/\$@?"(?:[^"\\]|\\.)*"/g, (lit) => " " + [...lit.matchAll(/\{([^}]*)\}/g)].map((x) => x[1]).join(" ") + " ").replace(/@?"(?:[^"\\]|\\.)*"/g, " ").replace(/'(?:[^'\\]|\\.)'/g, " ");
|
|
40386
|
+
const mentionsDerived = (text) => {
|
|
40387
|
+
const bare = codeOf(text);
|
|
40388
|
+
return [...derived].filter((v) => new RegExp(`(?<![\\w.])${v}(?![\\w])`).test(bare));
|
|
40389
|
+
};
|
|
40390
|
+
const assignRe = /^\s*(?:var\s+|[A-Za-z_][\w.<>[\]?]*\s+)?([A-Za-z_]\w*)\s*=(?!=)\s*(.+)$/;
|
|
40391
|
+
const objectWriteRe = /^\s*([A-Za-z_]\w*)\s*\.\s*[A-Za-z_]\w*\s*(?:\+?=(?!=)|\()(.*)$/;
|
|
40392
|
+
for (let i2 = method.startLine + 1;i2 < sink.line; i2++) {
|
|
40393
|
+
const text = lines[i2 - 1] ?? "";
|
|
40394
|
+
const assign = assignRe.exec(text);
|
|
40395
|
+
if (assign) {
|
|
40396
|
+
if (mentionsDerived(assign[2]).length > 0)
|
|
40397
|
+
derived.add(assign[1]);
|
|
40398
|
+
else if (!params.some((p) => p.name === assign[1]))
|
|
40399
|
+
derived.delete(assign[1]);
|
|
40400
|
+
continue;
|
|
40401
|
+
}
|
|
40402
|
+
const write = objectWriteRe.exec(text);
|
|
40403
|
+
if (write && mentionsDerived(write[2]).length > 0)
|
|
40404
|
+
derived.add(write[1]);
|
|
40405
|
+
}
|
|
40406
|
+
let statement = lines[sink.line - 1] ?? "";
|
|
40407
|
+
for (let i2 = sink.line;i2 < lines.length && i2 < sink.line + 6 && !/[;{}]\s*$/.test(statement.trimEnd()); i2++) {
|
|
40408
|
+
statement += " " + (lines[i2] ?? "").trim();
|
|
40409
|
+
}
|
|
40410
|
+
const reaching = mentionsDerived(statement);
|
|
40411
|
+
if (reaching.length === 0)
|
|
40412
|
+
return false;
|
|
40413
|
+
if (!credit)
|
|
40414
|
+
return true;
|
|
40415
|
+
const safe = new Set([...credit.sanitizedVarsAt(sink.type, sink.line), ...credit.guardedVarsAt(sink.type, sink.line)]);
|
|
40416
|
+
return reaching.some((v) => !safe.has(v));
|
|
40417
|
+
}
|
|
39699
40418
|
|
|
39700
40419
|
// ../circle-ir/dist/analysis/passes/dead-code-pass.js
|
|
39701
40420
|
class DeadCodePass {
|
|
@@ -40408,7 +41127,7 @@ class NullDerefPass {
|
|
|
40408
41127
|
}
|
|
40409
41128
|
|
|
40410
41129
|
// ../circle-ir/dist/analysis/passes/resource-leak-pass.js
|
|
40411
|
-
function
|
|
41130
|
+
function escapeRegex3(s) {
|
|
40412
41131
|
return s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
40413
41132
|
}
|
|
40414
41133
|
var FACTORY_METHOD_NAME_RE = /^(?:open|create|new|get|make|build)[A-Z]/;
|
|
@@ -40599,7 +41318,7 @@ class ResourceLeakPass {
|
|
|
40599
41318
|
return false;
|
|
40600
41319
|
}
|
|
40601
41320
|
isReturnedToCaller(lines, variable, fromLine, toLine) {
|
|
40602
|
-
const varRe = new RegExp(`\\breturn\\b[^;]*\\b${
|
|
41321
|
+
const varRe = new RegExp(`\\breturn\\b[^;]*\\b${escapeRegex3(variable)}\\b`);
|
|
40603
41322
|
for (let l = fromLine;l <= toLine && l <= lines.length; l++) {
|
|
40604
41323
|
if (varRe.test(lines[l - 1] ?? ""))
|
|
40605
41324
|
return true;
|
|
@@ -40607,7 +41326,7 @@ class ResourceLeakPass {
|
|
|
40607
41326
|
return false;
|
|
40608
41327
|
}
|
|
40609
41328
|
fieldStoredName(lines, variable, openLine, toLine) {
|
|
40610
|
-
const thisAssignRe = new RegExp(`\\bthis\\s*\\.\\s*(\\w+)\\s*=\\s*${
|
|
41329
|
+
const thisAssignRe = new RegExp(`\\bthis\\s*\\.\\s*(\\w+)\\s*=\\s*${escapeRegex3(variable)}\\b`);
|
|
40611
41330
|
for (let l = openLine;l <= toLine && l <= lines.length; l++) {
|
|
40612
41331
|
const m = thisAssignRe.exec(lines[l - 1] ?? "");
|
|
40613
41332
|
if (m)
|
|
@@ -40650,7 +41369,7 @@ class ResourceLeakPass {
|
|
|
40650
41369
|
if (fieldNames.has(variable)) {
|
|
40651
41370
|
candidateField = variable;
|
|
40652
41371
|
} else {
|
|
40653
|
-
const thisAssignRe = new RegExp(`(?:\\bthis\\s*\\.\\s*)?(\\w+)\\s*=\\s*${
|
|
41372
|
+
const thisAssignRe = new RegExp(`(?:\\bthis\\s*\\.\\s*)?(\\w+)\\s*=\\s*${escapeRegex3(variable)}\\b`);
|
|
40654
41373
|
for (let l = fromLine;l <= toLine && l <= lines.length; l++) {
|
|
40655
41374
|
const m = thisAssignRe.exec(lines[l - 1] ?? "");
|
|
40656
41375
|
if (m && fieldNames.has(m[1])) {
|
|
@@ -51135,7 +51854,7 @@ var colors = {
|
|
|
51135
51854
|
};
|
|
51136
51855
|
|
|
51137
51856
|
// src/version.ts
|
|
51138
|
-
var version = "4.
|
|
51857
|
+
var version = "4.13.0";
|
|
51139
51858
|
|
|
51140
51859
|
// src/formatters.ts
|
|
51141
51860
|
var LIBRARY_API_SURFACE_TAG2 = "library-api-surface:caller-responsibility";
|
|
@@ -51687,8 +52406,8 @@ function generateSarifResults(results, crossFileData) {
|
|
|
51687
52406
|
|
|
51688
52407
|
// src/build-info.ts
|
|
51689
52408
|
var buildInfo = {
|
|
51690
|
-
gitSha: "
|
|
51691
|
-
builtAt: "2026-10-
|
|
52409
|
+
gitSha: "a45b21b",
|
|
52410
|
+
builtAt: "2026-10-08T13:38:40.827Z"
|
|
51692
52411
|
};
|
|
51693
52412
|
|
|
51694
52413
|
// src/utils/args.ts
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "cognium-dev",
|
|
3
|
-
"version": "4.
|
|
3
|
+
"version": "4.13.0",
|
|
4
4
|
"description": "Static Application Security Testing CLI for detecting security vulnerabilities via taint tracking",
|
|
5
5
|
"bin": {
|
|
6
6
|
"cognium-dev": "dist/cli.js"
|
|
@@ -65,7 +65,7 @@
|
|
|
65
65
|
},
|
|
66
66
|
"dependencies": {
|
|
67
67
|
"@cognium/project-profile-detect": "1.1.2",
|
|
68
|
-
"circle-ir": "4.
|
|
68
|
+
"circle-ir": "4.13.0"
|
|
69
69
|
},
|
|
70
70
|
"devDependencies": {
|
|
71
71
|
"@types/node": "^26.6.4",
|