circle-ir 4.13.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.
@@ -22021,7 +22021,13 @@ var ConstantPropagator = class _ConstantPropagator {
22021
22021
  }
22022
22022
  return;
22023
22023
  }
22024
- const condValue = this.evaluateExpression(condition);
22024
+ let condValue = this.evaluateExpression(condition);
22025
+ if (!(isKnown(condValue) && condValue.type === "bool")) {
22026
+ const literal = this.literalCondition(condition);
22027
+ if (literal !== null) {
22028
+ condValue = { type: "bool", value: literal, sourceLine: getNodeLine(condition) };
22029
+ }
22030
+ }
22025
22031
  if (isKnown(condValue) && condValue.type === "bool") {
22026
22032
  if (condValue.value === true) {
22027
22033
  if (alternative) {
@@ -22185,13 +22191,27 @@ var ConstantPropagator = class _ConstantPropagator {
22185
22191
  return;
22186
22192
  }
22187
22193
  const caseGroups = switchBlock.children.filter(
22188
- (c) => c.type === "switch_block_statement_group" || c.type === "switch_rule" || c.type === "switch_section"
22194
+ (c) => c.type === "switch_block_statement_group" || c.type === "switch_rule" || c.type === "switch_section" || c.type === "switch_case" || c.type === "switch_default"
22189
22195
  );
22196
+ if (caseGroups.length === 0) {
22197
+ this.visit(switchBlock);
22198
+ return;
22199
+ }
22190
22200
  if (switchValue && isKnown(switchValue)) {
22191
22201
  let matchingIdx = -1;
22192
22202
  let defaultIdx = -1;
22193
22203
  for (let i2 = 0; i2 < caseGroups.length; i2++) {
22194
22204
  const caseGroup = caseGroups[i2];
22205
+ if (caseGroup.type === "switch_default") {
22206
+ defaultIdx = i2;
22207
+ continue;
22208
+ }
22209
+ if (caseGroup.type === "switch_case") {
22210
+ const label = caseGroup.childForFieldName("value");
22211
+ const caseValue = label ? this.literalValue(label) : null;
22212
+ if (caseValue !== null && caseValue === switchValue.value) matchingIdx = i2;
22213
+ continue;
22214
+ }
22195
22215
  for (const child of caseGroup.children) {
22196
22216
  if (child.type === "switch_label") {
22197
22217
  const labelText = getNodeText2(child, this.source);
@@ -22294,6 +22314,33 @@ var ConstantPropagator = class _ConstantPropagator {
22294
22314
  }
22295
22315
  return false;
22296
22316
  }
22317
+ /**
22318
+ * A condition written with literals only, in the C# grammar's node names:
22319
+ * `true`, `false`, `5 == 5`, `5 != 5`. Null when it is anything else. The
22320
+ * shared evaluator does not know `boolean_literal` / `integer_literal`.
22321
+ */
22322
+ literalCondition(node) {
22323
+ if (node.type === "parenthesized_expression") {
22324
+ const inner = node.children.find((c) => c.isNamed);
22325
+ return inner ? this.literalCondition(inner) : null;
22326
+ }
22327
+ if (node.type === "boolean_literal") {
22328
+ const text = getNodeText2(node, this.source);
22329
+ return text === "true" ? true : text === "false" ? false : null;
22330
+ }
22331
+ if (node.type === "binary_expression") {
22332
+ const left = node.childForFieldName("left");
22333
+ const right = node.childForFieldName("right");
22334
+ const op = node.childForFieldName("operator");
22335
+ const opText = op ? getNodeText2(op, this.source) : "";
22336
+ if (!left || !right || opText !== "==" && opText !== "!=") return null;
22337
+ const l = this.literalValue(left);
22338
+ const r = this.literalValue(right);
22339
+ if (l === null || r === null) return null;
22340
+ return opText === "==" ? l === r : l !== r;
22341
+ }
22342
+ return null;
22343
+ }
22297
22344
  /** The value of an integer, character or string literal node; null for anything else. */
22298
22345
  literalValue(node) {
22299
22346
  const text = getNodeText2(node, this.source);
@@ -35149,6 +35196,9 @@ var ENDS_STATEMENT_RE = /[;{}]\s*(?:\/\/.*)?$/;
35149
35196
  function escapeRegex(s) {
35150
35197
  return s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
35151
35198
  }
35199
+ function withoutStringLiterals(text) {
35200
+ return text.replace(/@"(?:[^"]|"")*"|"(?:[^"\\]|\\.)*"/g, '""');
35201
+ }
35152
35202
  function mentions(text, name2) {
35153
35203
  return new RegExp(`(?<![\\w])${escapeRegex(name2)}(?![\\w])`).test(text);
35154
35204
  }
@@ -35160,6 +35210,16 @@ function insideParens(text, open) {
35160
35210
  }
35161
35211
  return null;
35162
35212
  }
35213
+ function lastCallArguments(text) {
35214
+ const close = text.lastIndexOf(")");
35215
+ if (close < 0) return null;
35216
+ let depth = 0;
35217
+ for (let i2 = close; i2 >= 0; i2--) {
35218
+ if (text[i2] === ")") depth++;
35219
+ else if (text[i2] === "(" && --depth === 0) return text.slice(i2 + 1, close);
35220
+ }
35221
+ return null;
35222
+ }
35163
35223
  function leadingIf(text) {
35164
35224
  const m = /^\s*(?:\}\s*)?(?:else\s+)?if\s*\(/.exec(text);
35165
35225
  if (!m) return null;
@@ -35285,6 +35345,64 @@ var CSharpCredit = class {
35285
35345
  sanitizedVarsAt(type, sinkLine) {
35286
35346
  return this.sanitizedVarsIn(type, this.methodAt(sinkLine)?.start_line ?? 1, sinkLine, true);
35287
35347
  }
35348
+ /**
35349
+ * True when the sink on `sinkLine` also receives a tainted value that no
35350
+ * sanitizer covers. In `Write(HtmlEncode(a) + b)` the encoder covers `a`
35351
+ * only: `b` arrives raw, so the sanitizer on the line must not clear the
35352
+ * sink. `credited` are the names already known safe for `type` there;
35353
+ * `seeds` are the taint sources that name a variable.
35354
+ *
35355
+ * Taint is followed through the sink's method top to bottom, last
35356
+ * assignment wins. The sink text is then read without its string literals
35357
+ * and without the sanitizer calls for `type`; a tainted, uncredited name
35358
+ * left over is a raw operand.
35359
+ */
35360
+ rawTaintReaches(type, sinkLine, credited, seeds) {
35361
+ if (seeds.length === 0) return false;
35362
+ const method = this.methodAt(sinkLine);
35363
+ const from = method?.start_line ?? 1;
35364
+ const tainted = /* @__PURE__ */ new Set();
35365
+ const stmts = this.statements(from, sinkLine);
35366
+ for (let i2 = 0; i2 < stmts.length; i2++) {
35367
+ const st = stmts[i2];
35368
+ const end = i2 + 1 < stmts.length ? stmts[i2 + 1].line : sinkLine;
35369
+ const m = ASSIGN_RE.exec(st.text);
35370
+ if (m) {
35371
+ const rhs = withoutStringLiterals(m[2]);
35372
+ if ([...tainted].some((v) => mentions(rhs, v))) tainted.add(m[1]);
35373
+ else tainted.delete(m[1]);
35374
+ }
35375
+ for (const s of seeds) if (s.line >= st.line && s.line < end) tainted.add(s.variable);
35376
+ }
35377
+ if (tainted.size === 0) return false;
35378
+ const call = withoutStringLiterals(this.sinkText(sinkLine));
35379
+ const rest = this.withoutSanitizerCalls(type, lastCallArguments(call) ?? call);
35380
+ if (this.builtinSanitizes(type, rest)) return false;
35381
+ for (const v of tainted) if (!credited.has(v) && mentions(rest, v)) return true;
35382
+ return false;
35383
+ }
35384
+ /** `text` with every sanitizer call for `type`, arguments included, removed. */
35385
+ withoutSanitizerCalls(type, text) {
35386
+ const res = [];
35387
+ for (const { re, type: t } of CSHARP_SANITIZER_RES) if (t === type) res.push(re);
35388
+ for (const name2 of this.helpers(type)) res.push(new RegExp(`(?<![\\w.])${escapeRegex(name2)}\\s*\\(`));
35389
+ let out2 = text;
35390
+ for (const re of res) {
35391
+ for (let guard = 0; guard < 16; guard++) {
35392
+ const m = re.exec(out2);
35393
+ if (!m) break;
35394
+ if (!m[0].endsWith("(")) {
35395
+ out2 = out2.slice(0, m.index) + out2.slice(m.index + m[0].length);
35396
+ continue;
35397
+ }
35398
+ const open = m.index + m[0].length - 1;
35399
+ const inner = insideParens(out2, open);
35400
+ if (inner === null) break;
35401
+ out2 = out2.slice(0, m.index) + out2.slice(open + inner.length + 2);
35402
+ }
35403
+ }
35404
+ return out2;
35405
+ }
35288
35406
  /**
35289
35407
  * The text of the statement at `line` that a sink call can be in: for
35290
35408
  * `if (COND) sink(x);` that is `sink(x);`, without the condition. A name
@@ -35501,16 +35619,16 @@ var CSharpCredit = class {
35501
35619
  * sink type. Used for the untyped "tainted data reaches an external call"
35502
35620
  * fallback, which has no sink type of its own to ask about.
35503
35621
  */
35504
- creditedOnLine(line) {
35622
+ creditedOnLine(line, seeds = []) {
35505
35623
  const text = this.sinkText(line);
35506
35624
  const types = new Set(CSHARP_SANITIZER_RES.map((r) => r.type));
35507
35625
  for (const t of INERT_WHEN_ALPHANUMERIC) types.add(t);
35508
35626
  types.add("ssrf");
35509
35627
  for (const type of types) {
35510
- if (this.sanitizes(type, text)) return true;
35511
35628
  const credited = /* @__PURE__ */ new Set([...this.sanitizedVarsAt(type, line), ...this.guardedVarsAt(type, line)]);
35512
35629
  for (const v of this.safeBuilders(credited, line)) credited.add(v);
35513
- for (const v of credited) if (mentions(text, v)) return true;
35630
+ const covered = this.sanitizes(type, text) || [...credited].some((v) => mentions(text, v));
35631
+ if (covered && !this.rawTaintReaches(type, line, credited, seeds)) return true;
35514
35632
  }
35515
35633
  return this.fixedHostRequest(line);
35516
35634
  }
@@ -36676,11 +36794,12 @@ var SinkFilterPass = class {
36676
36794
  const sanitizedByType = csharpSanitizedVarsByType(ctx.code);
36677
36795
  const csFields = new Set(ctx.graph.ir.types.flatMap((t) => (t.fields ?? []).map((f) => f.name)));
36678
36796
  const credit = new CSharpCredit(ctx.code, ctx.graph.ir.types.flatMap((t) => t.methods));
36797
+ const csSeeds = mergedSources.flatMap((s) => s.variable ? [{ variable: s.variable, line: s.line }] : []);
36679
36798
  const xxeHardened = /\bXmlResolver\s*=\s*null\b/.test(ctx.code) || /\bDtdProcessing\s*=\s*(?:DtdProcessing\s*\.\s*)?(?:Prohibit|Ignore)\b/.test(ctx.code);
36680
36799
  filtered = filtered.filter((sink) => {
36681
36800
  if (sink.type === "xxe" && xxeHardened) return false;
36682
36801
  const sinkLineText = sourceLines[sink.line - 1] ?? "";
36683
- if (credit.sanitizes(sink.type, sinkLineText)) return false;
36802
+ const inlineSanitized = credit.sanitizes(sink.type, sinkLineText);
36684
36803
  if (sink.type === "ssrf" && credit.fixedHostRequest(sink.line)) return false;
36685
36804
  const scoped = credit.sanitizedVarsAt(sink.type, sink.line);
36686
36805
  const fileWide = sanitizedByType.get(sink.type);
@@ -36691,10 +36810,8 @@ var SinkFilterPass = class {
36691
36810
  for (const v of credit.guardedVarsAt(sink.type, sink.line)) sanitizedVars.add(v);
36692
36811
  for (const v of credit.safeBuilders(sanitizedVars, sink.line)) sanitizedVars.add(v);
36693
36812
  const received = credit.sinkText(sink.line);
36694
- for (const v of sanitizedVars) {
36695
- if (new RegExp(`(?<![\\w])${escapeRegex2(v)}(?![\\w])`).test(received)) return false;
36696
- }
36697
- return true;
36813
+ const covered = inlineSanitized || [...sanitizedVars].some((v) => new RegExp(`(?<![\\w])${escapeRegex2(v)}(?![\\w])`).test(received));
36814
+ return !covered || credit.rawTaintReaches(sink.type, sink.line, sanitizedVars, csSeeds);
36698
36815
  });
36699
36816
  }
36700
36817
  if (language === "csharp") {
@@ -40223,9 +40340,10 @@ var InterproceduralPass = class {
40223
40340
  });
40224
40341
  }
40225
40342
  if (csCredit) {
40343
+ const csSeeds = sources.flatMap((s) => s.variable ? [{ variable: s.variable, line: s.line }] : []);
40226
40344
  const credited = /* @__PURE__ */ new Set();
40227
40345
  filteredAdditionalFlows = filteredAdditionalFlows.filter((f) => {
40228
- if (f.sink_type !== "external_taint_escape" || !csCredit.creditedOnLine(f.sink_line)) return true;
40346
+ if (f.sink_type !== "external_taint_escape" || !csCredit.creditedOnLine(f.sink_line, csSeeds)) return true;
40229
40347
  credited.add(f.sink_line);
40230
40348
  return false;
40231
40349
  });
@@ -19200,7 +19200,13 @@ var ConstantPropagator = class _ConstantPropagator {
19200
19200
  }
19201
19201
  return;
19202
19202
  }
19203
- const condValue = this.evaluateExpression(condition);
19203
+ let condValue = this.evaluateExpression(condition);
19204
+ if (!(isKnown(condValue) && condValue.type === "bool")) {
19205
+ const literal = this.literalCondition(condition);
19206
+ if (literal !== null) {
19207
+ condValue = { type: "bool", value: literal, sourceLine: getNodeLine(condition) };
19208
+ }
19209
+ }
19204
19210
  if (isKnown(condValue) && condValue.type === "bool") {
19205
19211
  if (condValue.value === true) {
19206
19212
  if (alternative) {
@@ -19364,13 +19370,27 @@ var ConstantPropagator = class _ConstantPropagator {
19364
19370
  return;
19365
19371
  }
19366
19372
  const caseGroups = switchBlock.children.filter(
19367
- (c) => c.type === "switch_block_statement_group" || c.type === "switch_rule" || c.type === "switch_section"
19373
+ (c) => c.type === "switch_block_statement_group" || c.type === "switch_rule" || c.type === "switch_section" || c.type === "switch_case" || c.type === "switch_default"
19368
19374
  );
19375
+ if (caseGroups.length === 0) {
19376
+ this.visit(switchBlock);
19377
+ return;
19378
+ }
19369
19379
  if (switchValue && isKnown(switchValue)) {
19370
19380
  let matchingIdx = -1;
19371
19381
  let defaultIdx = -1;
19372
19382
  for (let i2 = 0; i2 < caseGroups.length; i2++) {
19373
19383
  const caseGroup = caseGroups[i2];
19384
+ if (caseGroup.type === "switch_default") {
19385
+ defaultIdx = i2;
19386
+ continue;
19387
+ }
19388
+ if (caseGroup.type === "switch_case") {
19389
+ const label = caseGroup.childForFieldName("value");
19390
+ const caseValue = label ? this.literalValue(label) : null;
19391
+ if (caseValue !== null && caseValue === switchValue.value) matchingIdx = i2;
19392
+ continue;
19393
+ }
19374
19394
  for (const child of caseGroup.children) {
19375
19395
  if (child.type === "switch_label") {
19376
19396
  const labelText = getNodeText2(child, this.source);
@@ -19473,6 +19493,33 @@ var ConstantPropagator = class _ConstantPropagator {
19473
19493
  }
19474
19494
  return false;
19475
19495
  }
19496
+ /**
19497
+ * A condition written with literals only, in the C# grammar's node names:
19498
+ * `true`, `false`, `5 == 5`, `5 != 5`. Null when it is anything else. The
19499
+ * shared evaluator does not know `boolean_literal` / `integer_literal`.
19500
+ */
19501
+ literalCondition(node) {
19502
+ if (node.type === "parenthesized_expression") {
19503
+ const inner = node.children.find((c) => c.isNamed);
19504
+ return inner ? this.literalCondition(inner) : null;
19505
+ }
19506
+ if (node.type === "boolean_literal") {
19507
+ const text = getNodeText2(node, this.source);
19508
+ return text === "true" ? true : text === "false" ? false : null;
19509
+ }
19510
+ if (node.type === "binary_expression") {
19511
+ const left = node.childForFieldName("left");
19512
+ const right = node.childForFieldName("right");
19513
+ const op = node.childForFieldName("operator");
19514
+ const opText = op ? getNodeText2(op, this.source) : "";
19515
+ if (!left || !right || opText !== "==" && opText !== "!=") return null;
19516
+ const l = this.literalValue(left);
19517
+ const r = this.literalValue(right);
19518
+ if (l === null || r === null) return null;
19519
+ return opText === "==" ? l === r : l !== r;
19520
+ }
19521
+ return null;
19522
+ }
19476
19523
  /** The value of an integer, character or string literal node; null for anything else. */
19477
19524
  literalValue(node) {
19478
19525
  const text = getNodeText2(node, this.source);
@@ -19134,7 +19134,13 @@ var ConstantPropagator = class _ConstantPropagator {
19134
19134
  }
19135
19135
  return;
19136
19136
  }
19137
- const condValue = this.evaluateExpression(condition);
19137
+ let condValue = this.evaluateExpression(condition);
19138
+ if (!(isKnown(condValue) && condValue.type === "bool")) {
19139
+ const literal = this.literalCondition(condition);
19140
+ if (literal !== null) {
19141
+ condValue = { type: "bool", value: literal, sourceLine: getNodeLine(condition) };
19142
+ }
19143
+ }
19138
19144
  if (isKnown(condValue) && condValue.type === "bool") {
19139
19145
  if (condValue.value === true) {
19140
19146
  if (alternative) {
@@ -19298,13 +19304,27 @@ var ConstantPropagator = class _ConstantPropagator {
19298
19304
  return;
19299
19305
  }
19300
19306
  const caseGroups = switchBlock.children.filter(
19301
- (c) => c.type === "switch_block_statement_group" || c.type === "switch_rule" || c.type === "switch_section"
19307
+ (c) => c.type === "switch_block_statement_group" || c.type === "switch_rule" || c.type === "switch_section" || c.type === "switch_case" || c.type === "switch_default"
19302
19308
  );
19309
+ if (caseGroups.length === 0) {
19310
+ this.visit(switchBlock);
19311
+ return;
19312
+ }
19303
19313
  if (switchValue && isKnown(switchValue)) {
19304
19314
  let matchingIdx = -1;
19305
19315
  let defaultIdx = -1;
19306
19316
  for (let i2 = 0; i2 < caseGroups.length; i2++) {
19307
19317
  const caseGroup = caseGroups[i2];
19318
+ if (caseGroup.type === "switch_default") {
19319
+ defaultIdx = i2;
19320
+ continue;
19321
+ }
19322
+ if (caseGroup.type === "switch_case") {
19323
+ const label = caseGroup.childForFieldName("value");
19324
+ const caseValue = label ? this.literalValue(label) : null;
19325
+ if (caseValue !== null && caseValue === switchValue.value) matchingIdx = i2;
19326
+ continue;
19327
+ }
19308
19328
  for (const child of caseGroup.children) {
19309
19329
  if (child.type === "switch_label") {
19310
19330
  const labelText = getNodeText2(child, this.source);
@@ -19407,6 +19427,33 @@ var ConstantPropagator = class _ConstantPropagator {
19407
19427
  }
19408
19428
  return false;
19409
19429
  }
19430
+ /**
19431
+ * A condition written with literals only, in the C# grammar's node names:
19432
+ * `true`, `false`, `5 == 5`, `5 != 5`. Null when it is anything else. The
19433
+ * shared evaluator does not know `boolean_literal` / `integer_literal`.
19434
+ */
19435
+ literalCondition(node) {
19436
+ if (node.type === "parenthesized_expression") {
19437
+ const inner = node.children.find((c) => c.isNamed);
19438
+ return inner ? this.literalCondition(inner) : null;
19439
+ }
19440
+ if (node.type === "boolean_literal") {
19441
+ const text = getNodeText2(node, this.source);
19442
+ return text === "true" ? true : text === "false" ? false : null;
19443
+ }
19444
+ if (node.type === "binary_expression") {
19445
+ const left = node.childForFieldName("left");
19446
+ const right = node.childForFieldName("right");
19447
+ const op = node.childForFieldName("operator");
19448
+ const opText = op ? getNodeText2(op, this.source) : "";
19449
+ if (!left || !right || opText !== "==" && opText !== "!=") return null;
19450
+ const l = this.literalValue(left);
19451
+ const r = this.literalValue(right);
19452
+ if (l === null || r === null) return null;
19453
+ return opText === "==" ? l === r : l !== r;
19454
+ }
19455
+ return null;
19456
+ }
19410
19457
  /** The value of an integer, character or string literal node; null for anything else. */
19411
19458
  literalValue(node) {
19412
19459
  const text = getNodeText2(node, this.source);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "circle-ir",
3
- "version": "4.13.0",
3
+ "version": "4.13.1",
4
4
  "description": "High-performance Static Application Security Testing (SAST) library for detecting security vulnerabilities through taint analysis",
5
5
  "main": "dist/index.js",
6
6
  "module": "dist/index.js",