circle-ir 3.123.0 → 3.125.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.
@@ -23654,6 +23654,9 @@ var LanguageSourcesPass = class {
23654
23654
  if (language === "python") {
23655
23655
  additionalSanitizers.push(...findPythonNetlocAllowlistGuardSanitizers(code));
23656
23656
  additionalSanitizers.push(...findPythonRangeCheckGuardSanitizers(code));
23657
+ additionalSanitizers.push(...findPythonRegexAllowlistWrapperSanitizers(code));
23658
+ additionalSanitizers.push(...findPythonSetMembershipXssGuardSanitizers(code));
23659
+ additionalSanitizers.push(...findPythonDefusedXmlSanitizers(code));
23657
23660
  const pyMisconfigFindings = findPythonPatternFindings(code, graph.ir.meta.file);
23658
23661
  for (const finding of pyMisconfigFindings) {
23659
23662
  ctx.addFinding(finding);
@@ -23667,6 +23670,15 @@ var LanguageSourcesPass = class {
23667
23670
  ctx.addFinding(finding);
23668
23671
  }
23669
23672
  }
23673
+ if (language === "javascript" || language === "typescript" || language === "htmljs") {
23674
+ additionalSanitizers.push(...findJsSafeJsonParseSanitizers(code));
23675
+ additionalSanitizers.push(...findJsCryptoHashSanitizers(code));
23676
+ additionalSanitizers.push(...findJsCsvFormulaPrefixSanitizers(code));
23677
+ additionalSanitizers.push(...findJsWrapperFunctionSanitizers(code));
23678
+ }
23679
+ if (language === "java") {
23680
+ additionalSanitizers.push(...findJavaSafeJsonParseSanitizers(code));
23681
+ }
23670
23682
  if (language === "python" || language === "javascript" || language === "typescript" || language === "go") {
23671
23683
  const exfilFindings = findExternalSecretExfiltrationFindings(
23672
23684
  code,
@@ -24911,6 +24923,227 @@ function findPythonRangeCheckGuardSanitizers(code) {
24911
24923
  }
24912
24924
  return sanitizers;
24913
24925
  }
24926
+ function findPythonRegexAllowlistWrapperSanitizers(code) {
24927
+ const sanitizers = [];
24928
+ const lines = code.split("\n");
24929
+ const defOpen = /^(\s*)def\s+([A-Za-z_][A-Za-z0-9_]*)\s*\(\s*([A-Za-z_][A-Za-z0-9_]*)\s*\)\s*:\s*$/;
24930
+ const tightRegex = /r?["'](?:\^)?(?:\\w[+*]|\\d[+*]|\[[A-Za-z0-9_\\\-\.\s]+\][+*]|[A-Za-z0-9_\-\.]+)(?:\$)?["']/;
24931
+ const terminator = /\b(return\s+(?:None|"|'|\(|\[|\{|False|jsonify|make_response|redirect|abort)|raise\s+\w|abort\s*\(|sys\.exit\s*\()/;
24932
+ const wrappers = [];
24933
+ for (let i2 = 0; i2 < lines.length; i2++) {
24934
+ const m = defOpen.exec(lines[i2]);
24935
+ if (!m) continue;
24936
+ const defIndent = m[1].length;
24937
+ const wrapperName = m[2];
24938
+ const argName = m[3];
24939
+ let foundGuard = false;
24940
+ let returnsArg = false;
24941
+ let blockEnd = -1;
24942
+ const maxScan = Math.min(lines.length, i2 + 40);
24943
+ for (let j = i2 + 1; j < maxScan; j++) {
24944
+ const line = lines[j];
24945
+ if (line.trim() === "") continue;
24946
+ const indent = line.length - line.trimStart().length;
24947
+ if (indent <= defIndent) {
24948
+ blockEnd = j - 1;
24949
+ break;
24950
+ }
24951
+ if (!foundGuard) {
24952
+ const guard = new RegExp(
24953
+ `if\\s+not\\s+re\\.(?:fullmatch|match)\\s*\\(\\s*(${tightRegex.source})\\s*,\\s*${argName}\\s*\\)\\s*:`
24954
+ );
24955
+ if (guard.test(line)) {
24956
+ for (let k = j + 1; k < maxScan; k++) {
24957
+ const klin = lines[k];
24958
+ if (klin.trim() === "") continue;
24959
+ const kindent = klin.length - klin.trimStart().length;
24960
+ if (kindent <= indent) break;
24961
+ if (terminator.test(klin)) foundGuard = true;
24962
+ break;
24963
+ }
24964
+ }
24965
+ }
24966
+ if (new RegExp(`^\\s*return\\s+${argName}\\s*$`).test(line)) {
24967
+ returnsArg = true;
24968
+ }
24969
+ }
24970
+ if (blockEnd === -1) blockEnd = Math.min(lines.length - 1, i2 + 39);
24971
+ if (!foundGuard || !returnsArg) continue;
24972
+ wrappers.push({ name: wrapperName, defLine: i2, defIndent });
24973
+ }
24974
+ if (wrappers.length === 0) return sanitizers;
24975
+ const wrapperKinds = [
24976
+ "ldap_injection",
24977
+ "xpath_injection",
24978
+ "sql_injection",
24979
+ "command_injection",
24980
+ "path_traversal",
24981
+ "xss",
24982
+ "external_taint_escape"
24983
+ ];
24984
+ for (const w of wrappers) {
24985
+ const assignedCallRe = new RegExp(
24986
+ `^(\\s*)([A-Za-z_][A-Za-z0-9_]*)\\s*=\\s*${w.name}\\s*\\(`
24987
+ );
24988
+ const bareCallRe = new RegExp(`\\b${w.name}\\s*\\(`);
24989
+ for (let i2 = 0; i2 < lines.length; i2++) {
24990
+ if (i2 === w.defLine) continue;
24991
+ const line = lines[i2];
24992
+ if (!bareCallRe.test(line)) continue;
24993
+ if (i2 > w.defLine) {
24994
+ const lineIndent = line.length - line.trimStart().length;
24995
+ if (lineIndent > w.defIndent) {
24996
+ let stillInside = true;
24997
+ for (let k = w.defLine + 1; k < i2; k++) {
24998
+ const kline = lines[k];
24999
+ if (kline.trim() === "") continue;
25000
+ const kindent = kline.length - kline.trimStart().length;
25001
+ if (kindent <= w.defIndent) {
25002
+ stillInside = false;
25003
+ break;
25004
+ }
25005
+ }
25006
+ if (stillInside) continue;
25007
+ }
25008
+ }
25009
+ sanitizers.push({
25010
+ type: "python_regex_allowlist_wrapper",
25011
+ method: w.name,
25012
+ line: i2 + 1,
25013
+ sanitizes: [...wrapperKinds]
25014
+ });
25015
+ const am = assignedCallRe.exec(line);
25016
+ if (!am) continue;
25017
+ const callIndent = am[1].length;
25018
+ const validated = am[2];
25019
+ const validatedRe = new RegExp(`\\b${validated}\\b`);
25020
+ for (let j = i2 + 1; j < lines.length; j++) {
25021
+ const jline = lines[j];
25022
+ if (jline.trim() === "") continue;
25023
+ const jindent = jline.length - jline.trimStart().length;
25024
+ if (jindent < callIndent) break;
25025
+ if (validatedRe.test(jline)) {
25026
+ sanitizers.push({
25027
+ type: "python_regex_allowlist_wrapper",
25028
+ method: w.name,
25029
+ line: j + 1,
25030
+ sanitizes: [...wrapperKinds]
25031
+ });
25032
+ }
25033
+ }
25034
+ }
25035
+ }
25036
+ return sanitizers;
25037
+ }
25038
+ function findPythonSetMembershipXssGuardSanitizers(code) {
25039
+ const sanitizers = [];
25040
+ const lines = code.split("\n");
25041
+ const guardOpen = /^(\s*)if\s+([A-Za-z_][A-Za-z0-9_]*)\s+not\s+in\s+([A-Za-z_][A-Za-z0-9_]*)\s*:\s*$/;
25042
+ const allowlistName = /^(?:[A-Z][A-Z0-9_]+|.*?(allowed|accepted|whitelist|permitted|valid|approved).*)$/i;
25043
+ const terminator = /\b(return|raise|abort\s*\(|sys\.exit\s*\()/;
25044
+ for (let i2 = 0; i2 < lines.length; i2++) {
25045
+ const m = guardOpen.exec(lines[i2]);
25046
+ if (!m) continue;
25047
+ const guardIndent = m[1].length;
25048
+ const guardedVar = m[2];
25049
+ const allowName = m[3];
25050
+ if (!allowlistName.test(allowName)) continue;
25051
+ let bodyHasTerminator = false;
25052
+ let blockEnd = -1;
25053
+ const maxScan = Math.min(lines.length, i2 + 26);
25054
+ for (let j = i2 + 1; j < maxScan; j++) {
25055
+ const line = lines[j];
25056
+ if (line.trim() === "") continue;
25057
+ const indent = line.length - line.trimStart().length;
25058
+ if (indent <= guardIndent) {
25059
+ blockEnd = j - 1;
25060
+ break;
25061
+ }
25062
+ if (terminator.test(line)) bodyHasTerminator = true;
25063
+ }
25064
+ if (blockEnd === -1) blockEnd = Math.min(lines.length - 1, i2 + 25);
25065
+ if (!bodyHasTerminator) continue;
25066
+ const varRe = new RegExp(`\\b${guardedVar}\\b`);
25067
+ for (let l = blockEnd + 2; l <= lines.length; l++) {
25068
+ const lineText = lines[l - 1];
25069
+ if (!varRe.test(lineText)) continue;
25070
+ sanitizers.push({
25071
+ type: "python_set_membership_xss_guard",
25072
+ method: "if",
25073
+ line: l,
25074
+ sanitizes: ["xss", "external_taint_escape"]
25075
+ });
25076
+ }
25077
+ }
25078
+ return sanitizers;
25079
+ }
25080
+ function findPythonDefusedXmlSanitizers(code) {
25081
+ const sanitizers = [];
25082
+ const lines = code.split("\n");
25083
+ const moduleAliases = /* @__PURE__ */ new Set();
25084
+ const fromNames = /* @__PURE__ */ new Set();
25085
+ const importAs = /^\s*import\s+defusedxml(?:\.[A-Za-z_][A-Za-z0-9_.]*)?\s+as\s+([A-Za-z_][A-Za-z0-9_]*)\s*$/;
25086
+ const importBare = /^\s*import\s+defusedxml(?:\.([A-Za-z_][A-Za-z0-9_.]*))?\s*$/;
25087
+ const fromImport = /^\s*from\s+defusedxml(?:\.[A-Za-z_][A-Za-z0-9_.]*)?\s+import\s+(.+)$/;
25088
+ for (const raw of lines) {
25089
+ const line = raw.replace(/#.*$/, "");
25090
+ let m;
25091
+ if (m = importAs.exec(line)) {
25092
+ moduleAliases.add(m[1]);
25093
+ continue;
25094
+ }
25095
+ if (m = importBare.exec(line)) {
25096
+ if (m[1]) {
25097
+ moduleAliases.add(m[1].split(".").pop());
25098
+ }
25099
+ moduleAliases.add("defusedxml");
25100
+ continue;
25101
+ }
25102
+ if (m = fromImport.exec(line)) {
25103
+ const items = m[1].split(",");
25104
+ for (const item of items) {
25105
+ const trimmed = item.trim().replace(/[()\\]/g, "");
25106
+ if (!trimmed) continue;
25107
+ const asMatch = /^([A-Za-z_][A-Za-z0-9_]*)\s+as\s+([A-Za-z_][A-Za-z0-9_]*)$/.exec(trimmed);
25108
+ if (asMatch) {
25109
+ fromNames.add(asMatch[2]);
25110
+ } else if (/^[A-Za-z_][A-Za-z0-9_]*$/.test(trimmed)) {
25111
+ fromNames.add(trimmed);
25112
+ }
25113
+ }
25114
+ }
25115
+ }
25116
+ if (moduleAliases.size === 0 && fromNames.size === 0) return sanitizers;
25117
+ for (let i2 = 0; i2 < lines.length; i2++) {
25118
+ const line = lines[i2];
25119
+ if (/^\s*(?:import|from)\s+/.test(line)) continue;
25120
+ let matched = false;
25121
+ for (const alias of moduleAliases) {
25122
+ const re = new RegExp(`\\b${alias}\\s*\\.\\s*[A-Za-z_][A-Za-z0-9_]*\\s*\\(`);
25123
+ if (re.test(line)) {
25124
+ matched = true;
25125
+ break;
25126
+ }
25127
+ }
25128
+ if (!matched) {
25129
+ for (const name2 of fromNames) {
25130
+ const re = new RegExp(`\\b${name2}\\s*\\(`);
25131
+ if (re.test(line)) {
25132
+ matched = true;
25133
+ break;
25134
+ }
25135
+ }
25136
+ }
25137
+ if (!matched) continue;
25138
+ sanitizers.push({
25139
+ type: "python_defusedxml_import",
25140
+ method: "defusedxml",
25141
+ line: i2 + 1,
25142
+ sanitizes: ["xxe", "external_taint_escape"]
25143
+ });
25144
+ }
25145
+ return sanitizers;
25146
+ }
24914
25147
  function findRustSetAllowlistGuardSanitizers(code) {
24915
25148
  const sanitizers = [];
24916
25149
  const lines = code.split("\n");
@@ -25005,6 +25238,109 @@ function findRustCanonicalizeGuardSanitizers(code) {
25005
25238
  }
25006
25239
  return sanitizers;
25007
25240
  }
25241
+ function findJavaSafeJsonParseSanitizers(code) {
25242
+ const sanitizers = [];
25243
+ const lines = code.split("\n");
25244
+ const safeParse = /\.(?:readValue|fromJson)\s*\(/;
25245
+ for (let i2 = 0; i2 < lines.length; i2++) {
25246
+ if (!safeParse.test(lines[i2])) continue;
25247
+ sanitizers.push({
25248
+ type: "java_safe_json_parse",
25249
+ method: "readValue",
25250
+ line: i2 + 1,
25251
+ sanitizes: ["external_taint_escape"]
25252
+ });
25253
+ }
25254
+ return sanitizers;
25255
+ }
25256
+ function findJsSafeJsonParseSanitizers(code) {
25257
+ const sanitizers = [];
25258
+ const lines = code.split("\n");
25259
+ const safeParse = /\bJSON\.parse\s*\(/;
25260
+ for (let i2 = 0; i2 < lines.length; i2++) {
25261
+ if (!safeParse.test(lines[i2])) continue;
25262
+ sanitizers.push({
25263
+ type: "js_safe_json_parse",
25264
+ method: "JSON.parse",
25265
+ line: i2 + 1,
25266
+ sanitizes: ["external_taint_escape"]
25267
+ });
25268
+ }
25269
+ return sanitizers;
25270
+ }
25271
+ function findJsCryptoHashSanitizers(code) {
25272
+ const sanitizers = [];
25273
+ const lines = code.split("\n");
25274
+ const hashCall = /\b(?:bcrypt|argon2)\s*\.\s*hash(?:Sync)?\s*\(|\bcrypto\s*\.\s*(?:scrypt(?:Sync)?|createHash)\s*\(|\.digest\s*\(/;
25275
+ for (let i2 = 0; i2 < lines.length; i2++) {
25276
+ if (!hashCall.test(lines[i2])) continue;
25277
+ sanitizers.push({
25278
+ type: "js_crypto_hash",
25279
+ method: "hash",
25280
+ line: i2 + 1,
25281
+ sanitizes: ["external_taint_escape"]
25282
+ });
25283
+ }
25284
+ return sanitizers;
25285
+ }
25286
+ function findJsCsvFormulaPrefixSanitizers(code) {
25287
+ const sanitizers = [];
25288
+ const lines = code.split("\n");
25289
+ const tickPrefix = /`'\$\{/;
25290
+ const concatPrefix = /["']\\?'["']\s*\+\s*[A-Za-z_]/;
25291
+ for (let i2 = 0; i2 < lines.length; i2++) {
25292
+ if (tickPrefix.test(lines[i2]) || concatPrefix.test(lines[i2])) {
25293
+ sanitizers.push({
25294
+ type: "js_csv_formula_prefix",
25295
+ method: "'-prefix",
25296
+ line: i2 + 1,
25297
+ sanitizes: ["external_taint_escape"]
25298
+ });
25299
+ }
25300
+ }
25301
+ return sanitizers;
25302
+ }
25303
+ function findJsWrapperFunctionSanitizers(code) {
25304
+ const sanitizers = [];
25305
+ const lines = code.split("\n");
25306
+ const wrappers = /* @__PURE__ */ new Map();
25307
+ const fnDeclRe = /\bfunction\s+([A-Za-z_]\w*)\s*\(/;
25308
+ const arrowDeclRe = /\b(?:const|let|var)\s+([A-Za-z_]\w*)\s*=\s*(?:\([^)]*\)|[A-Za-z_]\w*)\s*=>/;
25309
+ const xssCharClass = /\.replace\s*\(\s*\/[^/]*[&<>"'][^/]*\//;
25310
+ const crlfCharClass = /\.replace\s*\(\s*\/[^/]*(?:\\r|\\n|\\t)[^/]*\//;
25311
+ for (let i2 = 0; i2 < lines.length; i2++) {
25312
+ const m = fnDeclRe.exec(lines[i2]) ?? arrowDeclRe.exec(lines[i2]);
25313
+ if (!m) continue;
25314
+ const name2 = m[1];
25315
+ const body2 = lines.slice(i2, Math.min(lines.length, i2 + 8)).join("\n");
25316
+ const kinds = /* @__PURE__ */ new Set();
25317
+ if (xssCharClass.test(body2)) {
25318
+ kinds.add("xss");
25319
+ kinds.add("external_taint_escape");
25320
+ }
25321
+ if (crlfCharClass.test(body2)) {
25322
+ kinds.add("log_injection");
25323
+ kinds.add("external_taint_escape");
25324
+ }
25325
+ if (kinds.size > 0) wrappers.set(name2, kinds);
25326
+ }
25327
+ if (wrappers.size === 0) return sanitizers;
25328
+ for (let i2 = 0; i2 < lines.length; i2++) {
25329
+ for (const [name2, kinds] of wrappers) {
25330
+ const declSelf = new RegExp(`\\b(?:function|const|let|var)\\s+${name2}\\b`);
25331
+ if (declSelf.test(lines[i2])) continue;
25332
+ const callRe = new RegExp(`\\b${name2}\\s*\\(`);
25333
+ if (!callRe.test(lines[i2])) continue;
25334
+ sanitizers.push({
25335
+ type: "js_wrapper_function",
25336
+ method: name2,
25337
+ line: i2 + 1,
25338
+ sanitizes: [...kinds]
25339
+ });
25340
+ }
25341
+ }
25342
+ return sanitizers;
25343
+ }
25008
25344
  function collectEnvSecretVars(lines, language) {
25009
25345
  const out2 = /* @__PURE__ */ new Map();
25010
25346
  for (let i2 = 0; i2 < lines.length; i2++) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "circle-ir",
3
- "version": "3.123.0",
3
+ "version": "3.125.0",
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",