circle-ir 4.16.0 → 4.16.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.
@@ -21865,14 +21865,17 @@ function findCSharpEarlyReturnGuardSanitizers(code, language) {
21865
21865
  const sanitizers = [];
21866
21866
  const lines = code.split("\n");
21867
21867
  const terminatorRe = /\b(throw|return)\b/;
21868
+ const literalArrays = collectCSharpLiteralArrayFields(code);
21869
+ const indexOfRe = /\bif\s*\(\s*(?:System\s*\.\s*)?Array\s*\.\s*IndexOf\s*\(\s*([A-Za-z_]\w*)\s*,\s*([A-Za-z_][\w.]*)\s*\)\s*(?:<\s*0|==\s*-\s*1)\s*\)/;
21868
21870
  const allowlistRes = [
21869
- /\bif\s*\(\s*Array\s*\.\s*IndexOf\s*\([^,]+,\s*([A-Za-z_][\w.]*)\s*\)\s*<\s*0\s*\)/,
21870
21871
  /\bif\s*\(\s*!\s*[A-Za-z_][\w.]*\s*\.\s*Contains\s*\(\s*([A-Za-z_][\w.]*)\s*\)\s*\)/
21871
21872
  ];
21872
21873
  const charClassRe = /\bif\s*\(\s*!\s*([A-Za-z_][\w.]*)\s*\.\s*All\s*\(\s*char\s*\.\s*Is[A-Za-z]+\s*\)\s*\)/;
21873
21874
  for (let i2 = 0; i2 < lines.length; i2++) {
21874
21875
  let guardedExpr = null;
21875
- for (const re of allowlistRes) {
21876
+ const im = indexOfRe.exec(lines[i2]);
21877
+ if (im && literalArrays.has(im[1])) guardedExpr = im[2];
21878
+ if (!guardedExpr) for (const re of allowlistRes) {
21876
21879
  const m = re.exec(lines[i2]);
21877
21880
  if (m) {
21878
21881
  guardedExpr = m[1];
@@ -21900,6 +21903,7 @@ function findCSharpEarlyReturnGuardSanitizers(code, language) {
21900
21903
  for (let l = i2 + 1; l < lines.length; l++) {
21901
21904
  const a = assignRe.exec(lines[l]);
21902
21905
  if (a && mentions2(a[2])) guarded.add(a[1]);
21906
+ else if (a) guarded.delete(a[1]);
21903
21907
  if (mentions2(lines[l])) {
21904
21908
  sanitizers.push({
21905
21909
  type: "csharp_early_return_guard",
@@ -21920,6 +21924,25 @@ function findCSharpEarlyReturnGuardSanitizers(code, language) {
21920
21924
  }
21921
21925
  return sanitizers;
21922
21926
  }
21927
+ function collectCSharpLiteralArrayFields(code) {
21928
+ const names = /* @__PURE__ */ new Set();
21929
+ const lit = String.raw`(?:@"(?:[^"]|"")*"|"(?:[^"\\\n]|\\.)*")`;
21930
+ const elems = new RegExp(String.raw`^\s*${lit}(?:\s*,\s*${lit})*\s*,?\s*$`);
21931
+ const declRe = /((?:\b(?:public|private|protected|internal|static|readonly)\s+)+)string\s*\[\s*\]\s+([A-Za-z_]\w*)\s*=\s*(?:new\s+(?:string\s*)?\[\s*\]\s*)?\{([^{}]*)\}\s*;/g;
21932
+ for (const m of code.matchAll(declRe)) {
21933
+ const mods = m[1];
21934
+ const name2 = m[2];
21935
+ if (!/\bstatic\b/.test(mods) || !/\breadonly\b/.test(mods)) continue;
21936
+ if (!elems.test(m[3])) continue;
21937
+ const rest = code.slice(0, m.index) + code.slice(m.index + m[0].length);
21938
+ const written = new RegExp(
21939
+ String.raw`\b${name2}\s*(?:\[[^\]]*\]\s*)?(?:[-+*/%&|^]|\?\?)?=(?!=)` + String.raw`|\b(?:ref|out)\s+${name2}\b` + String.raw`|\b${name2}\s*\.\s*SetValue\s*\(` + String.raw`|\bArray\s*\.\s*(?:Clear|Copy|ConstrainedCopy|Fill|Resize|Reverse|Sort)\s*\([^;]*\b${name2}\b`
21940
+ );
21941
+ if (written.test(rest)) continue;
21942
+ names.add(name2);
21943
+ }
21944
+ return names;
21945
+ }
21923
21946
  function findCSharpFullPathStartsWithGuardSanitizers(code) {
21924
21947
  const sanitizers = [];
21925
21948
  const lines = code.split("\n");
@@ -30968,10 +30991,12 @@ function isJavaStaticFormatterCall(call, declaredTypeNames, imports, calls) {
30968
30991
  if (other === call || other.method_name === "format") continue;
30969
30992
  if (other.arguments.some((a) => a.expression.includes(needle))) return false;
30970
30993
  }
30971
- const receiver = call.receiver;
30994
+ return isJdkClassReceiver(call.receiver, JAVA_FORMATTER_CLASSES, declaredTypeNames, imports);
30995
+ }
30996
+ function isJdkClassReceiver(receiver, classes, declaredTypeNames, imports) {
30972
30997
  const dot = receiver.lastIndexOf(".");
30973
30998
  const simple = dot < 0 ? receiver : receiver.slice(dot + 1);
30974
- const pkg = JAVA_FORMATTER_CLASSES.get(simple);
30999
+ const pkg = classes.get(simple);
30975
31000
  if (pkg === void 0) return false;
30976
31001
  if (dot >= 0) return receiver === `${pkg}.${simple}`;
30977
31002
  if (declaredTypeNames.has(simple)) return false;
@@ -30982,8 +31007,41 @@ function isJavaStaticFormatterCall(call, declaredTypeNames, imports, calls) {
30982
31007
  if (pkg === "java.lang") return true;
30983
31008
  return imports.some((imp) => imp.from_package === pkg && (imp.imported_name === simple || imp.is_wildcard));
30984
31009
  }
31010
+ var JAVA_CHARACTER_CLASS = /* @__PURE__ */ new Map([["Character", "java.lang"]]);
31011
+ var JAVA_CHARACTER_PREDICATES = /* @__PURE__ */ new Set([
31012
+ "isLetter",
31013
+ "isDigit",
31014
+ "isLetterOrDigit",
31015
+ "isAlphabetic",
31016
+ "isWhitespace",
31017
+ "isSpaceChar",
31018
+ "isUpperCase",
31019
+ "isLowerCase",
31020
+ "isTitleCase",
31021
+ "isISOControl",
31022
+ "isDefined",
31023
+ "isIdeographic",
31024
+ "isMirrored",
31025
+ "isJavaIdentifierStart",
31026
+ "isJavaIdentifierPart",
31027
+ "isUnicodeIdentifierStart",
31028
+ "isUnicodeIdentifierPart",
31029
+ "isIdentifierIgnorable",
31030
+ "isHighSurrogate",
31031
+ "isLowSurrogate",
31032
+ "isSurrogate",
31033
+ "isSurrogatePair",
31034
+ "isValidCodePoint",
31035
+ "isBmpCodePoint",
31036
+ "isSupplementaryCodePoint"
31037
+ ]);
31038
+ function isJavaCharacterPredicateCall(call, declaredTypeNames, imports) {
31039
+ if (!call.receiver || !JAVA_CHARACTER_PREDICATES.has(call.method_name)) return false;
31040
+ return isJdkClassReceiver(call.receiver, JAVA_CHARACTER_CLASS, declaredTypeNames, imports);
31041
+ }
30985
31042
  function isJavaNonEscapeCall(call, declaredTypeNames, imports, calls) {
30986
31043
  if (isJavaStaticFormatterCall(call, declaredTypeNames, imports, calls)) return true;
31044
+ if (isJavaCharacterPredicateCall(call, declaredTypeNames, imports)) return true;
30987
31045
  if (call.receiver) return false;
30988
31046
  const isCtor = call.is_constructor === true || (call.resolution?.target ?? "").endsWith(".<init>");
30989
31047
  if (!isCtor) return false;
@@ -38627,6 +38685,11 @@ function bareIdentifiers(text) {
38627
38685
  for (const m of stripped.matchAll(/\b([A-Za-z_]\w*)\b/g)) out2.push(m[1]);
38628
38686
  return out2;
38629
38687
  }
38688
+ function isLiteralValue(rhs) {
38689
+ if (/\$"/.test(rhs)) return false;
38690
+ const ids = bareIdentifiers(rhs);
38691
+ return ids.every((id) => id === "null" || id === "true" || id === "false" || id === "default") || /^\s*(?:string|String)\s*\.\s*Empty\s*$/.test(rhs);
38692
+ }
38630
38693
  function insideParens(text, open) {
38631
38694
  let depth = 0;
38632
38695
  for (let i2 = open; i2 < text.length; i2++) {
@@ -38807,6 +38870,36 @@ var CSharpCredit = class {
38807
38870
  sanitizedVarsAt(type, sinkLine) {
38808
38871
  return this.sanitizedVarsIn(type, this.methodAt(sinkLine)?.start_line ?? 1, sinkLine, true);
38809
38872
  }
38873
+ /**
38874
+ * Class fields judged across every method that writes them (cognium-dev#808).
38875
+ * A field is read in one method and written in others, so each write is
38876
+ * judged in its own method with the same in-method credit as a local, and
38877
+ * the field is safe for `type` only when every write is: `sanitized` holds
38878
+ * the fields with at least one credited write and no uncredited one,
38879
+ * `unsanitized` the fields with an uncredited write. A literal write
38880
+ * (`_v = "";`, `_v = null;`) is neutral.
38881
+ */
38882
+ fieldWrites(type, fields) {
38883
+ const credited = /* @__PURE__ */ new Set();
38884
+ const unsanitized = /* @__PURE__ */ new Set();
38885
+ for (const m of this.methods) {
38886
+ const sanitized = /* @__PURE__ */ new Set();
38887
+ for (const st of this.statements(m.start_line, m.end_line + 1)) {
38888
+ if (this.methodAt(st.line) !== m) continue;
38889
+ for (const op of this.creditOpsInStatement(st.text)) {
38890
+ const lhs = op.lhs;
38891
+ const keeps = op.kind === "append" ? sanitized.has(lhs) && this.rhsKeepsSanitizerCredit(type, op.rhs, sanitized, true, true) : this.rhsKeepsSanitizerCredit(type, op.rhs, sanitized, true, false);
38892
+ if (keeps) sanitized.add(lhs);
38893
+ else sanitized.delete(lhs);
38894
+ if (!fields.has(lhs) || isLiteralValue(op.rhs)) continue;
38895
+ if (keeps) credited.add(lhs);
38896
+ else unsanitized.add(lhs);
38897
+ }
38898
+ }
38899
+ }
38900
+ for (const f of unsanitized) credited.delete(f);
38901
+ return { sanitized: credited, unsanitized };
38902
+ }
38810
38903
  /**
38811
38904
  * True when the sink on `sinkLine` also receives a tainted value that no
38812
38905
  * sanitizer covers. In `Write(HtmlEncode(a) + b)` the encoder covers `a`
@@ -41832,6 +41925,15 @@ var SinkFilterPass = class {
41832
41925
  const csFields = new Set(ctx.graph.ir.types.flatMap((t) => (t.fields ?? []).map((f) => f.name)));
41833
41926
  const credit = new CSharpCredit(ctx.code, ctx.graph.ir.types.flatMap((t) => t.methods));
41834
41927
  const csSeeds = mergedSources.flatMap((s) => s.variable ? [{ variable: s.variable, line: s.line }] : []);
41928
+ const fieldWritesByType = /* @__PURE__ */ new Map();
41929
+ const fieldWritesFor = (type) => {
41930
+ let w = fieldWritesByType.get(type);
41931
+ if (!w) {
41932
+ w = credit.fieldWrites(type, csFields);
41933
+ fieldWritesByType.set(type, w);
41934
+ }
41935
+ return w;
41936
+ };
41835
41937
  const xxeHardened = /\bXmlResolver\s*=\s*null\b/.test(ctx.code) || /\bDtdProcessing\s*=\s*(?:DtdProcessing\s*\.\s*)?(?:Prohibit|Ignore)\b/.test(ctx.code);
41836
41938
  filtered = filtered.filter((sink) => {
41837
41939
  if (sink.type === "xxe" && xxeHardened) return false;
@@ -41841,9 +41943,13 @@ var SinkFilterPass = class {
41841
41943
  const scoped = credit.sanitizedVarsAt(sink.type, sink.line);
41842
41944
  const fileWide = sanitizedByType.get(sink.type);
41843
41945
  const sanitizedVars = new Set(scoped);
41946
+ const fieldWrites = fieldWritesFor(sink.type);
41844
41947
  if (fileWide) {
41845
- for (const v of fileWide) if (csFields.has(v)) sanitizedVars.add(v);
41948
+ for (const v of fileWide) {
41949
+ if (csFields.has(v) && !fieldWrites.unsanitized.has(v)) sanitizedVars.add(v);
41950
+ }
41846
41951
  }
41952
+ for (const v of fieldWrites.sanitized) sanitizedVars.add(v);
41847
41953
  for (const v of credit.guardedVarsAt(sink.type, sink.line)) sanitizedVars.add(v);
41848
41954
  for (const v of credit.safeBuilders(sanitizedVars, sink.line)) sanitizedVars.add(v);
41849
41955
  const received = credit.sinkText(sink.line);
@@ -43954,21 +44060,30 @@ function resolveJavaDerivedAnchor(varName, derived, realSourceByVar, codeLines,
43954
44060
  }
43955
44061
  return best;
43956
44062
  }
43957
- function javaReceiverChainDerivedVar(code, localVar, seedVar) {
44063
+ function javaReceiverChainDerivedVar(rhsByLocal, localVar, seedVar) {
43958
44064
  if (!seedVar || localVar === seedVar) return false;
44065
+ const rhsList = rhsByLocal.get(localVar);
44066
+ if (!rhsList) return false;
43959
44067
  const esc = seedVar.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
43960
44068
  const receiverOnRhs = new RegExp(`(?:^|[^\\w.$])${esc}\\s*\\.`, "u");
44069
+ return rhsList.some((rhs) => receiverOnRhs.test(rhs));
44070
+ }
44071
+ function indexJavaLocalAssignments(code) {
43961
44072
  const declRe = /^\s*(?:public|private|protected|static|final|volatile|transient|\s)*\s*(?:[A-Za-z_][\w.]*(?:\s*<[^>]*>)?(?:\s*\[\s*\])*)\s+([A-Za-z_]\w*)\s*=\s*(.+?);\s*$/;
43962
44073
  const assignRe = /^\s*([A-Za-z_]\w*)\s*=\s*(.+?);\s*$/;
44074
+ const index = /* @__PURE__ */ new Map();
43963
44075
  for (const line of code.split("\n")) {
43964
44076
  const trimmed = line.trimStart();
43965
44077
  if (trimmed.startsWith("//") || trimmed.startsWith("*")) continue;
44078
+ if (!line.includes("=") || !line.includes(";")) continue;
43966
44079
  const m = declRe.exec(line) ?? assignRe.exec(line);
43967
- if (!m || m[1] !== localVar) continue;
44080
+ if (!m) continue;
44081
+ const list = index.get(m[1]);
43968
44082
  const rhs = stripJavaTrailingComment(m[2]);
43969
- if (receiverOnRhs.test(rhs)) return true;
44083
+ if (list) list.push(rhs);
44084
+ else index.set(m[1], [rhs]);
43970
44085
  }
43971
- return false;
44086
+ return index;
43972
44087
  }
43973
44088
  function makeConstPropVeto(constProp, graph, sources, code, language, sanitizers) {
43974
44089
  const lines = typeof code === "string" ? code.split("\n") : [];
@@ -44071,13 +44186,17 @@ var TaintPropagationPass = class {
44071
44186
  const sinkFilter = ctx.getResult("sink-filter");
44072
44187
  const { sources, sinks, sanitizers } = sinkFilter;
44073
44188
  if (ctx.language === "java" && typeof ctx.code === "string") {
44189
+ const rhsByLocal = indexJavaLocalAssignments(ctx.code);
44190
+ const seen = /* @__PURE__ */ new Set();
44074
44191
  for (const s of sources) {
44075
- if (!s.variable) continue;
44076
- for (const [v] of buildJavaTaintedVars(ctx.code, /* @__PURE__ */ new Set([s.variable]))) {
44077
- if (v === s.variable) continue;
44078
- if (javaReceiverChainDerivedVar(ctx.code, v, s.variable)) {
44079
- constProp.tainted.add(v);
44080
- }
44192
+ if (!s.variable || seen.has(s.variable)) continue;
44193
+ seen.add(s.variable);
44194
+ const seedVar = s.variable;
44195
+ const candidates = [...rhsByLocal.keys()].filter((v) => !constProp.tainted.has(v) && javaReceiverChainDerivedVar(rhsByLocal, v, seedVar));
44196
+ if (candidates.length === 0) continue;
44197
+ const derived = buildJavaTaintedVars(ctx.code, /* @__PURE__ */ new Set([seedVar]));
44198
+ for (const v of candidates) {
44199
+ if (derived.has(v)) constProp.tainted.add(v);
44081
44200
  }
44082
44201
  }
44083
44202
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "circle-ir",
3
- "version": "4.16.0",
3
+ "version": "4.16.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",