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.
- package/dist/analysis/interprocedural.d.ts.map +1 -1
- package/dist/analysis/interprocedural.js +37 -8
- package/dist/analysis/interprocedural.js.map +1 -1
- package/dist/analysis/passes/csharp-sanitizer-credit.d.ts +13 -0
- package/dist/analysis/passes/csharp-sanitizer-credit.d.ts.map +1 -1
- package/dist/analysis/passes/csharp-sanitizer-credit.js +48 -0
- package/dist/analysis/passes/csharp-sanitizer-credit.js.map +1 -1
- package/dist/analysis/passes/language-sources-pass.d.ts.map +1 -1
- package/dist/analysis/passes/language-sources-pass.js +58 -8
- package/dist/analysis/passes/language-sources-pass.js.map +1 -1
- package/dist/analysis/passes/sink-filter-pass.d.ts.map +1 -1
- package/dist/analysis/passes/sink-filter-pass.js +19 -3
- package/dist/analysis/passes/sink-filter-pass.js.map +1 -1
- package/dist/analysis/passes/taint-propagation-pass.d.ts.map +1 -1
- package/dist/analysis/passes/taint-propagation-pass.js +38 -11
- package/dist/analysis/passes/taint-propagation-pass.js.map +1 -1
- package/dist/browser/circle-ir.js +134 -15
- package/package.json +1 -1
|
@@ -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
|
-
|
|
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
|
-
|
|
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 =
|
|
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)
|
|
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(
|
|
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
|
|
44080
|
+
if (!m) continue;
|
|
44081
|
+
const list = index.get(m[1]);
|
|
43968
44082
|
const rhs = stripJavaTrailingComment(m[2]);
|
|
43969
|
-
if (
|
|
44083
|
+
if (list) list.push(rhs);
|
|
44084
|
+
else index.set(m[1], [rhs]);
|
|
43970
44085
|
}
|
|
43971
|
-
return
|
|
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
|
-
|
|
44077
|
-
|
|
44078
|
-
|
|
44079
|
-
|
|
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.
|
|
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",
|