circle-ir 4.13.0 → 4.13.2

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,84 @@ 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 rest = this.withoutSanitizerCalls(type, this.receivedOperands(type, sinkLine));
35379
+ if (this.builtinSanitizes(type, rest)) return false;
35380
+ for (const v of tainted) if (!credited.has(v) && mentions(rest, v)) return true;
35381
+ return false;
35382
+ }
35383
+ /**
35384
+ * The operands a sink on `sinkLine` receives, string literals blanked. For a
35385
+ * property sink (`searcher.Filter = rhs;`) that is the right-hand side. For
35386
+ * a call sink it is the call's arguments, so a tainted receiver or cast
35387
+ * target (`((XmlDocument)ctx).SelectSingleNode(q)`) is not an operand. The
35388
+ * last call on the line is the sink only when it is not itself a sanitizer:
35389
+ * in `x.Filter = Escape(input);` it is the sanitizer (cognium-dev#643).
35390
+ */
35391
+ receivedOperands(type, sinkLine) {
35392
+ const text = withoutStringLiterals(this.sinkText(sinkLine)).replace(/\s*;?\s*(?:\/\/.*)?$/, "");
35393
+ const property = /^\s*[A-Za-z_][\w.]*\.[A-Za-z_]\w*\s*=(?!=)\s*(.*)$/.exec(text);
35394
+ if (property) return property[1];
35395
+ if (!text.endsWith(")")) return text;
35396
+ const args2 = lastCallArguments(text);
35397
+ if (args2 === null) return text;
35398
+ const open = text.length - args2.length - 2;
35399
+ const callStart = text.slice(0, open).search(/[A-Za-z_][\w.]*\s*$/);
35400
+ const lastCall = callStart < 0 ? "" : text.slice(callStart);
35401
+ if (lastCall && this.withoutSanitizerCalls(type, lastCall).trim() === "") return text;
35402
+ return args2;
35403
+ }
35404
+ /** `text` with every sanitizer call for `type`, arguments included, removed. */
35405
+ withoutSanitizerCalls(type, text) {
35406
+ const res = [];
35407
+ for (const { re, type: t } of CSHARP_SANITIZER_RES) if (t === type) res.push(re);
35408
+ for (const name2 of this.helpers(type)) res.push(new RegExp(`(?<![\\w.])${escapeRegex(name2)}\\s*\\(`));
35409
+ let out2 = text;
35410
+ for (const re of res) {
35411
+ for (let guard = 0; guard < 16; guard++) {
35412
+ const m = re.exec(out2);
35413
+ if (!m) break;
35414
+ if (!m[0].endsWith("(")) {
35415
+ out2 = out2.slice(0, m.index) + out2.slice(m.index + m[0].length);
35416
+ continue;
35417
+ }
35418
+ const open = m.index + m[0].length - 1;
35419
+ const inner = insideParens(out2, open);
35420
+ if (inner === null) break;
35421
+ out2 = out2.slice(0, m.index) + out2.slice(open + inner.length + 2);
35422
+ }
35423
+ }
35424
+ return out2;
35425
+ }
35288
35426
  /**
35289
35427
  * The text of the statement at `line` that a sink call can be in: for
35290
35428
  * `if (COND) sink(x);` that is `sink(x);`, without the condition. A name
@@ -35501,16 +35639,16 @@ var CSharpCredit = class {
35501
35639
  * sink type. Used for the untyped "tainted data reaches an external call"
35502
35640
  * fallback, which has no sink type of its own to ask about.
35503
35641
  */
35504
- creditedOnLine(line) {
35642
+ creditedOnLine(line, seeds = []) {
35505
35643
  const text = this.sinkText(line);
35506
35644
  const types = new Set(CSHARP_SANITIZER_RES.map((r) => r.type));
35507
35645
  for (const t of INERT_WHEN_ALPHANUMERIC) types.add(t);
35508
35646
  types.add("ssrf");
35509
35647
  for (const type of types) {
35510
- if (this.sanitizes(type, text)) return true;
35511
35648
  const credited = /* @__PURE__ */ new Set([...this.sanitizedVarsAt(type, line), ...this.guardedVarsAt(type, line)]);
35512
35649
  for (const v of this.safeBuilders(credited, line)) credited.add(v);
35513
- for (const v of credited) if (mentions(text, v)) return true;
35650
+ const covered = this.sanitizes(type, text) || [...credited].some((v) => mentions(text, v));
35651
+ if (covered && !this.rawTaintReaches(type, line, credited, seeds)) return true;
35514
35652
  }
35515
35653
  return this.fixedHostRequest(line);
35516
35654
  }
@@ -36676,11 +36814,12 @@ var SinkFilterPass = class {
36676
36814
  const sanitizedByType = csharpSanitizedVarsByType(ctx.code);
36677
36815
  const csFields = new Set(ctx.graph.ir.types.flatMap((t) => (t.fields ?? []).map((f) => f.name)));
36678
36816
  const credit = new CSharpCredit(ctx.code, ctx.graph.ir.types.flatMap((t) => t.methods));
36817
+ const csSeeds = mergedSources.flatMap((s) => s.variable ? [{ variable: s.variable, line: s.line }] : []);
36679
36818
  const xxeHardened = /\bXmlResolver\s*=\s*null\b/.test(ctx.code) || /\bDtdProcessing\s*=\s*(?:DtdProcessing\s*\.\s*)?(?:Prohibit|Ignore)\b/.test(ctx.code);
36680
36819
  filtered = filtered.filter((sink) => {
36681
36820
  if (sink.type === "xxe" && xxeHardened) return false;
36682
36821
  const sinkLineText = sourceLines[sink.line - 1] ?? "";
36683
- if (credit.sanitizes(sink.type, sinkLineText)) return false;
36822
+ const inlineSanitized = credit.sanitizes(sink.type, sinkLineText);
36684
36823
  if (sink.type === "ssrf" && credit.fixedHostRequest(sink.line)) return false;
36685
36824
  const scoped = credit.sanitizedVarsAt(sink.type, sink.line);
36686
36825
  const fileWide = sanitizedByType.get(sink.type);
@@ -36691,10 +36830,8 @@ var SinkFilterPass = class {
36691
36830
  for (const v of credit.guardedVarsAt(sink.type, sink.line)) sanitizedVars.add(v);
36692
36831
  for (const v of credit.safeBuilders(sanitizedVars, sink.line)) sanitizedVars.add(v);
36693
36832
  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;
36833
+ const covered = inlineSanitized || [...sanitizedVars].some((v) => new RegExp(`(?<![\\w])${escapeRegex2(v)}(?![\\w])`).test(received));
36834
+ return !covered || credit.rawTaintReaches(sink.type, sink.line, sanitizedVars, csSeeds);
36698
36835
  });
36699
36836
  }
36700
36837
  if (language === "csharp") {
@@ -40223,9 +40360,10 @@ var InterproceduralPass = class {
40223
40360
  });
40224
40361
  }
40225
40362
  if (csCredit) {
40363
+ const csSeeds = sources.flatMap((s) => s.variable ? [{ variable: s.variable, line: s.line }] : []);
40226
40364
  const credited = /* @__PURE__ */ new Set();
40227
40365
  filteredAdditionalFlows = filteredAdditionalFlows.filter((f) => {
40228
- if (f.sink_type !== "external_taint_escape" || !csCredit.creditedOnLine(f.sink_line)) return true;
40366
+ if (f.sink_type !== "external_taint_escape" || !csCredit.creditedOnLine(f.sink_line, csSeeds)) return true;
40229
40367
  credited.add(f.sink_line);
40230
40368
  return false;
40231
40369
  });
@@ -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.2",
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",