circle-ir 3.121.0 → 3.124.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.
@@ -12924,7 +12924,11 @@ var PYTHON_TAINTED_PATTERNS = [
12924
12924
  { pattern: /\brequest\.META\b/, sourceType: "http_header" },
12925
12925
  { pattern: /\brequest\.FILES\b/, sourceType: "file_input" },
12926
12926
  { pattern: /\brequest\.query_params\b/, sourceType: "http_param" },
12927
- { pattern: /\brequest\.path_params\b/, sourceType: "http_param" }
12927
+ { pattern: /\brequest\.path_params\b/, sourceType: "http_param" },
12928
+ // Sprint 72 (#183 residual): builtin `input()` is a stdin user-input
12929
+ // source. Already registered in configs/sources/python.json; add to the
12930
+ // regex registry so it's recognized in subscript/var contexts.
12931
+ { pattern: /\binput\s*\(/, sourceType: "io_input" }
12928
12932
  ];
12929
12933
  function analyzeTaint(calls, types, config = getDefaultConfig(), typeHierarchy, language, code) {
12930
12934
  const sourceLines = code !== void 0 ? code.split("\n") : void 0;
@@ -16226,7 +16230,32 @@ function analyzeInterprocedural(graphOrTypes, callsOrSources, dfgOrSinks, source
16226
16230
  if (isBash && bashSafeBuiltins.has(call.method_name)) {
16227
16231
  continue;
16228
16232
  }
16229
- const sink = isBash ? {
16233
+ if (isBash && taintedArgPositions.length > 0) {
16234
+ const terminatorPos = call.arguments.filter((a) => a.expression === "--").map((a) => a.position).reduce((min, p) => min === null || p < min ? p : min, null);
16235
+ const earliestTainted = Math.min(...taintedArgPositions);
16236
+ if (terminatorPos !== null && terminatorPos < earliestTainted) {
16237
+ const allTaintedQuoted = taintedArgPositions.every((p) => {
16238
+ const a = call.arguments[p];
16239
+ if (!a) return false;
16240
+ const expr = a.expression.trim();
16241
+ return expr.startsWith('"') && expr.endsWith('"');
16242
+ });
16243
+ if (allTaintedQuoted) {
16244
+ continue;
16245
+ }
16246
+ }
16247
+ }
16248
+ const bashSqlCliTools = /* @__PURE__ */ new Set(["sqlite3", "mysql", "psql", "mariadb"]);
16249
+ const isBashSqlCli = isBash && bashSqlCliTools.has(call.method_name);
16250
+ const sink = isBashSqlCli ? {
16251
+ type: "sql_injection",
16252
+ cwe: "CWE-89",
16253
+ location: `Tainted data (${taintedArgVars.join(", ")}) interpolated into SQL query passed to ${call.method_name}`,
16254
+ line: call.location.line,
16255
+ confidence: 0.7,
16256
+ method: call.method_name,
16257
+ argPositions: taintedArgPositions
16258
+ } : isBash ? {
16230
16259
  type: "command_injection",
16231
16260
  cwe: "CWE-78",
16232
16261
  location: `Tainted data (${taintedArgVars.join(", ")}) passed unquoted to shell utility ${call.method_name}`,
@@ -23528,7 +23557,12 @@ var PYTHON_TAINTED_PATTERNS2 = [
23528
23557
  { pattern: /\bget_form_parameter\s*\(/, type: "http_body" },
23529
23558
  { pattern: /\bget_query_parameter\s*\(/, type: "http_param" },
23530
23559
  { pattern: /\bget_header_value\s*\(/, type: "http_header" },
23531
- { pattern: /\bget_cookie_value\s*\(/, type: "http_cookie" }
23560
+ { pattern: /\bget_cookie_value\s*\(/, type: "http_cookie" },
23561
+ // Sprint 72 (#183 residual): `input()` is registered in
23562
+ // configs/sources/python.json but was not in the forward-taint regex
23563
+ // registry, so `name = input()` was not added to pyTaintedVars. Closes
23564
+ // the deferred `getattr(obj, input())()` reflection-invocation shape.
23565
+ { pattern: /\binput\s*\(/, type: "io_input" }
23532
23566
  ];
23533
23567
  var LanguageSourcesPass = class {
23534
23568
  name = "language-sources";
@@ -23587,6 +23621,21 @@ var LanguageSourcesPass = class {
23587
23621
  });
23588
23622
  }
23589
23623
  }
23624
+ for (const r of findPythonReflectionInvocationSinks(code, pyTaintedVars)) {
23625
+ const alreadyExists = additionalSinks.some(
23626
+ (s) => s.line === r.sinkLine && s.type === "code_injection" && s.method === r.method
23627
+ );
23628
+ if (!alreadyExists) {
23629
+ additionalSinks.push({
23630
+ type: "code_injection",
23631
+ cwe: "CWE-94",
23632
+ line: r.sinkLine,
23633
+ location: `reflection invocation (getattr result called) with tainted attribute name at line ${r.sinkLine}`,
23634
+ method: r.method,
23635
+ confidence: 0.85
23636
+ });
23637
+ }
23638
+ }
23590
23639
  }
23591
23640
  const jsTaintedVars = buildJavaScriptTaintedVars(code, language);
23592
23641
  if (language === "bash") {
@@ -23605,10 +23654,37 @@ var LanguageSourcesPass = class {
23605
23654
  if (language === "python") {
23606
23655
  additionalSanitizers.push(...findPythonNetlocAllowlistGuardSanitizers(code));
23607
23656
  additionalSanitizers.push(...findPythonRangeCheckGuardSanitizers(code));
23657
+ const pyMisconfigFindings = findPythonPatternFindings(code, graph.ir.meta.file);
23658
+ for (const finding of pyMisconfigFindings) {
23659
+ ctx.addFinding(finding);
23660
+ }
23608
23661
  }
23609
23662
  if (language === "rust") {
23610
23663
  additionalSanitizers.push(...findRustSetAllowlistGuardSanitizers(code));
23611
23664
  additionalSanitizers.push(...findRustCanonicalizeGuardSanitizers(code));
23665
+ const rustMisconfigFindings = findRustPatternFindings(code, graph.ir.meta.file);
23666
+ for (const finding of rustMisconfigFindings) {
23667
+ ctx.addFinding(finding);
23668
+ }
23669
+ }
23670
+ if (language === "javascript" || language === "typescript" || language === "htmljs") {
23671
+ additionalSanitizers.push(...findJsSafeJsonParseSanitizers(code));
23672
+ additionalSanitizers.push(...findJsCryptoHashSanitizers(code));
23673
+ additionalSanitizers.push(...findJsCsvFormulaPrefixSanitizers(code));
23674
+ additionalSanitizers.push(...findJsWrapperFunctionSanitizers(code));
23675
+ }
23676
+ if (language === "java") {
23677
+ additionalSanitizers.push(...findJavaSafeJsonParseSanitizers(code));
23678
+ }
23679
+ if (language === "python" || language === "javascript" || language === "typescript" || language === "go") {
23680
+ const exfilFindings = findExternalSecretExfiltrationFindings(
23681
+ code,
23682
+ graph.ir.meta.file,
23683
+ language
23684
+ );
23685
+ for (const finding of exfilFindings) {
23686
+ ctx.addFinding(finding);
23687
+ }
23612
23688
  }
23613
23689
  attachSourceLineCode(additionalSources, additionalSinks, code);
23614
23690
  return { additionalSources, additionalSinks, additionalSanitizers, pyTaintedVars, pySanitizedVars, jsTaintedVars };
@@ -24166,6 +24242,44 @@ function findPythonReturnXSSSinks(sourceCode, taintedVars) {
24166
24242
  }
24167
24243
  return sinks;
24168
24244
  }
24245
+ function findPythonReflectionInvocationSinks(sourceCode, taintedVars) {
24246
+ if (taintedVars.size === 0) return [];
24247
+ const sinks = [];
24248
+ const lines = sourceCode.split("\n");
24249
+ const directRe = /\bgetattr\s*\(\s*[^,()]+\s*,\s*([A-Za-z_][\w]*)\s*\)\s*\(/;
24250
+ const bindRe = /^\s*([A-Za-z_][\w]*)\s*=\s*getattr\s*\(\s*[^,()]+\s*,\s*([A-Za-z_][\w]*)\s*\)\s*$/;
24251
+ const aliases = [];
24252
+ for (let i2 = 0; i2 < lines.length; i2++) {
24253
+ const line = lines[i2];
24254
+ if (line.trimStart().startsWith("#")) continue;
24255
+ const dm = line.match(directRe);
24256
+ if (dm && taintedVars.has(dm[1])) {
24257
+ sinks.push({ sinkLine: i2 + 1, method: "getattr" });
24258
+ continue;
24259
+ }
24260
+ const bm = line.match(bindRe);
24261
+ if (bm && taintedVars.has(bm[2])) {
24262
+ aliases.push({ name: bm[1], bindLine: i2 + 1 });
24263
+ }
24264
+ }
24265
+ if (aliases.length === 0) return sinks;
24266
+ const firedBindLines = /* @__PURE__ */ new Set();
24267
+ for (let i2 = 0; i2 < lines.length; i2++) {
24268
+ const line = lines[i2];
24269
+ if (line.trimStart().startsWith("#")) continue;
24270
+ const lineNum = i2 + 1;
24271
+ for (const a of aliases) {
24272
+ if (lineNum <= a.bindLine) continue;
24273
+ if (firedBindLines.has(a.bindLine)) continue;
24274
+ const invokeRe = new RegExp(`(?<![\\w.])${a.name}\\s*\\(`);
24275
+ if (invokeRe.test(line)) {
24276
+ sinks.push({ sinkLine: a.bindLine, method: "getattr" });
24277
+ firedBindLines.add(a.bindLine);
24278
+ }
24279
+ }
24280
+ }
24281
+ return sinks;
24282
+ }
24169
24283
  function findJavaScriptDOMSinks(sourceCode, language) {
24170
24284
  if (!["javascript", "typescript"].includes(language)) return [];
24171
24285
  const sinks = [];
@@ -24383,6 +24497,7 @@ function findBashPatternFindings(sourceCode, file) {
24383
24497
  const findings = [];
24384
24498
  const lines = sourceCode.split("\n");
24385
24499
  const checksumVerifiedTmpPaths = collectChecksumVerifiedTmpPaths(lines);
24500
+ const scriptHasVerifier = hasIntegrityVerifierAnywhere(lines);
24386
24501
  for (let i2 = 0; i2 < lines.length; i2++) {
24387
24502
  const line = lines[i2];
24388
24503
  const trimmed = line.trim();
@@ -24475,9 +24590,78 @@ function findBashPatternFindings(sourceCode, file) {
24475
24590
  snippet: trimmed.substring(0, 80)
24476
24591
  });
24477
24592
  }
24593
+ if (!scriptHasVerifier) {
24594
+ const installer = matchUnverifiedPackageInstall(trimmed);
24595
+ if (installer) {
24596
+ findings.push({
24597
+ id: `unverified-package-install-${file}-${lineNumber}`,
24598
+ pass: "language-sources",
24599
+ category: "security",
24600
+ rule_id: "unverified-package-install",
24601
+ cwe: "CWE-494",
24602
+ severity: "high",
24603
+ level: "error",
24604
+ message: `Unverified package install via ${installer}: package contents are not integrity-checked (no gpg/sha256sum verify in script)`,
24605
+ file,
24606
+ line: lineNumber,
24607
+ snippet: trimmed.substring(0, 80)
24608
+ });
24609
+ }
24610
+ }
24611
+ const weakHashAlg = matchBashWeakHashCommand(trimmed);
24612
+ if (weakHashAlg) {
24613
+ findings.push({
24614
+ id: `weak-hash-${file}-${lineNumber}`,
24615
+ pass: "language-sources",
24616
+ category: "security",
24617
+ rule_id: "weak-hash",
24618
+ cwe: "CWE-328",
24619
+ severity: "medium",
24620
+ level: "warning",
24621
+ message: `Weak hash algorithm: ${weakHashAlg} is cryptographically broken. Use sha256sum or sha512sum`,
24622
+ file,
24623
+ line: lineNumber,
24624
+ snippet: trimmed.substring(0, 80)
24625
+ });
24626
+ }
24478
24627
  }
24479
24628
  return findings;
24480
24629
  }
24630
+ function matchBashWeakHashCommand(line) {
24631
+ const re = /(?:^|[|;]|&&|\|\|)\s*(md5sum|sha1sum|md5|sha1)\b(?!\s*=)/;
24632
+ const m = line.match(re);
24633
+ if (!m) return null;
24634
+ return m[1];
24635
+ }
24636
+ function matchUnverifiedPackageInstall(line) {
24637
+ if (/\bdpkg\b/.test(line) && /(?:^|\s)(?:-[a-zA-Z]*[iIU][a-zA-Z]*|--install)\b/.test(line)) {
24638
+ return "dpkg";
24639
+ }
24640
+ if (/\brpm\b/.test(line) && /(?:^|\s)(?:-[a-zA-Z]*[iU][a-zA-Z]*|--install|--upgrade)\b/.test(line) && !/--(?:verify|checksig|erase|query)\b/.test(line)) {
24641
+ return "rpm";
24642
+ }
24643
+ const aptMatch = line.match(/\b(apt-get|apt|aptitude)\s+install\b/);
24644
+ if (aptMatch && /\.deb\b/.test(line)) {
24645
+ return aptMatch[1];
24646
+ }
24647
+ const yumMatch = line.match(/\b(yum|dnf|zypper)\s+install\b/);
24648
+ if (yumMatch && /\.rpm\b/.test(line)) {
24649
+ return yumMatch[1];
24650
+ }
24651
+ return null;
24652
+ }
24653
+ function hasIntegrityVerifierAnywhere(lines) {
24654
+ const sigRe = /\b(?:gpg(?:v|2)?|gpg)\s+(?:[^|]*\s)?--verify\b/;
24655
+ const rpmSigRe = /\brpm\s+(?:[^|]*\s)?--checksig\b/;
24656
+ const dpkgSigRe = /\bdpkg\s+(?:[^|]*\s)?--verify\b/;
24657
+ const sumRe = /\b(?:sha(?:1|224|256|384|512)sum|md5sum|cksum|b2sum)\s+(?:[^|]*\s)?(?:-c|--check)\b/;
24658
+ for (const line of lines) {
24659
+ if (sigRe.test(line) || rpmSigRe.test(line) || dpkgSigRe.test(line) || sumRe.test(line)) {
24660
+ return true;
24661
+ }
24662
+ }
24663
+ return false;
24664
+ }
24481
24665
  function findBashRegexAllowlistSanitizers(code) {
24482
24666
  const sanitizers = [];
24483
24667
  const lines = code.split("\n");
@@ -24830,6 +25014,491 @@ function findRustCanonicalizeGuardSanitizers(code) {
24830
25014
  }
24831
25015
  return sanitizers;
24832
25016
  }
25017
+ function findJavaSafeJsonParseSanitizers(code) {
25018
+ const sanitizers = [];
25019
+ const lines = code.split("\n");
25020
+ const safeParse = /\.(?:readValue|fromJson)\s*\(/;
25021
+ for (let i2 = 0; i2 < lines.length; i2++) {
25022
+ if (!safeParse.test(lines[i2])) continue;
25023
+ sanitizers.push({
25024
+ type: "java_safe_json_parse",
25025
+ method: "readValue",
25026
+ line: i2 + 1,
25027
+ sanitizes: ["external_taint_escape"]
25028
+ });
25029
+ }
25030
+ return sanitizers;
25031
+ }
25032
+ function findJsSafeJsonParseSanitizers(code) {
25033
+ const sanitizers = [];
25034
+ const lines = code.split("\n");
25035
+ const safeParse = /\bJSON\.parse\s*\(/;
25036
+ for (let i2 = 0; i2 < lines.length; i2++) {
25037
+ if (!safeParse.test(lines[i2])) continue;
25038
+ sanitizers.push({
25039
+ type: "js_safe_json_parse",
25040
+ method: "JSON.parse",
25041
+ line: i2 + 1,
25042
+ sanitizes: ["external_taint_escape"]
25043
+ });
25044
+ }
25045
+ return sanitizers;
25046
+ }
25047
+ function findJsCryptoHashSanitizers(code) {
25048
+ const sanitizers = [];
25049
+ const lines = code.split("\n");
25050
+ const hashCall = /\b(?:bcrypt|argon2)\s*\.\s*hash(?:Sync)?\s*\(|\bcrypto\s*\.\s*(?:scrypt(?:Sync)?|createHash)\s*\(|\.digest\s*\(/;
25051
+ for (let i2 = 0; i2 < lines.length; i2++) {
25052
+ if (!hashCall.test(lines[i2])) continue;
25053
+ sanitizers.push({
25054
+ type: "js_crypto_hash",
25055
+ method: "hash",
25056
+ line: i2 + 1,
25057
+ sanitizes: ["external_taint_escape"]
25058
+ });
25059
+ }
25060
+ return sanitizers;
25061
+ }
25062
+ function findJsCsvFormulaPrefixSanitizers(code) {
25063
+ const sanitizers = [];
25064
+ const lines = code.split("\n");
25065
+ const tickPrefix = /`'\$\{/;
25066
+ const concatPrefix = /["']\\?'["']\s*\+\s*[A-Za-z_]/;
25067
+ for (let i2 = 0; i2 < lines.length; i2++) {
25068
+ if (tickPrefix.test(lines[i2]) || concatPrefix.test(lines[i2])) {
25069
+ sanitizers.push({
25070
+ type: "js_csv_formula_prefix",
25071
+ method: "'-prefix",
25072
+ line: i2 + 1,
25073
+ sanitizes: ["external_taint_escape"]
25074
+ });
25075
+ }
25076
+ }
25077
+ return sanitizers;
25078
+ }
25079
+ function findJsWrapperFunctionSanitizers(code) {
25080
+ const sanitizers = [];
25081
+ const lines = code.split("\n");
25082
+ const wrappers = /* @__PURE__ */ new Map();
25083
+ const fnDeclRe = /\bfunction\s+([A-Za-z_]\w*)\s*\(/;
25084
+ const arrowDeclRe = /\b(?:const|let|var)\s+([A-Za-z_]\w*)\s*=\s*(?:\([^)]*\)|[A-Za-z_]\w*)\s*=>/;
25085
+ const xssCharClass = /\.replace\s*\(\s*\/[^/]*[&<>"'][^/]*\//;
25086
+ const crlfCharClass = /\.replace\s*\(\s*\/[^/]*(?:\\r|\\n|\\t)[^/]*\//;
25087
+ for (let i2 = 0; i2 < lines.length; i2++) {
25088
+ const m = fnDeclRe.exec(lines[i2]) ?? arrowDeclRe.exec(lines[i2]);
25089
+ if (!m) continue;
25090
+ const name2 = m[1];
25091
+ const body2 = lines.slice(i2, Math.min(lines.length, i2 + 8)).join("\n");
25092
+ const kinds = /* @__PURE__ */ new Set();
25093
+ if (xssCharClass.test(body2)) {
25094
+ kinds.add("xss");
25095
+ kinds.add("external_taint_escape");
25096
+ }
25097
+ if (crlfCharClass.test(body2)) {
25098
+ kinds.add("log_injection");
25099
+ kinds.add("external_taint_escape");
25100
+ }
25101
+ if (kinds.size > 0) wrappers.set(name2, kinds);
25102
+ }
25103
+ if (wrappers.size === 0) return sanitizers;
25104
+ for (let i2 = 0; i2 < lines.length; i2++) {
25105
+ for (const [name2, kinds] of wrappers) {
25106
+ const declSelf = new RegExp(`\\b(?:function|const|let|var)\\s+${name2}\\b`);
25107
+ if (declSelf.test(lines[i2])) continue;
25108
+ const callRe = new RegExp(`\\b${name2}\\s*\\(`);
25109
+ if (!callRe.test(lines[i2])) continue;
25110
+ sanitizers.push({
25111
+ type: "js_wrapper_function",
25112
+ method: name2,
25113
+ line: i2 + 1,
25114
+ sanitizes: [...kinds]
25115
+ });
25116
+ }
25117
+ }
25118
+ return sanitizers;
25119
+ }
25120
+ function collectEnvSecretVars(lines, language) {
25121
+ const out2 = /* @__PURE__ */ new Map();
25122
+ for (let i2 = 0; i2 < lines.length; i2++) {
25123
+ const line = lines[i2];
25124
+ if (language === "python") {
25125
+ const m = line.match(/^\s*(\w+)\s*=\s*os\.(?:environ\s*[\[.]|getenv\b)/);
25126
+ if (m) out2.set(m[1], i2 + 1);
25127
+ } else if (language === "javascript" || language === "typescript") {
25128
+ const m = line.match(/(?:^|\s|;)(?:const|let|var)\s+(\w+)\s*=\s*process\.env\b/);
25129
+ if (m) out2.set(m[1], i2 + 1);
25130
+ } else if (language === "go") {
25131
+ const m = line.match(/^\s*(\w+)\s*:?=\s*os\.Getenv\s*\(/);
25132
+ if (m) out2.set(m[1], i2 + 1);
25133
+ }
25134
+ }
25135
+ return out2;
25136
+ }
25137
+ function isExternalHostUrl(url) {
25138
+ const lower = url.toLowerCase();
25139
+ if (!/^https?:\/\//.test(lower)) return false;
25140
+ const host = lower.replace(/^https?:\/\//, "").split(/[/?#:]/)[0];
25141
+ if (!host) return false;
25142
+ if (host === "localhost" || host === "127.0.0.1" || host === "0.0.0.0" || host === "::1") return false;
25143
+ if (/^10\./.test(host)) return false;
25144
+ if (/^192\.168\./.test(host)) return false;
25145
+ if (/^172\.(1[6-9]|2\d|3[01])\./.test(host)) return false;
25146
+ if (host.includes(".internal.") || host.endsWith(".internal") || host.startsWith("internal.")) return false;
25147
+ if (host.endsWith(".local") || host.endsWith(".lan") || host.endsWith(".corp")) return false;
25148
+ if (!host.includes(".")) return false;
25149
+ return true;
25150
+ }
25151
+ function findBalancedCallEnd(code, start2) {
25152
+ let depth = 1;
25153
+ let i2 = start2;
25154
+ let inStr = null;
25155
+ while (i2 < code.length) {
25156
+ const ch = code[i2];
25157
+ if (inStr) {
25158
+ if (ch === "\\") {
25159
+ i2 += 2;
25160
+ continue;
25161
+ }
25162
+ if (ch === inStr) inStr = null;
25163
+ } else {
25164
+ if (ch === '"' || ch === "'" || ch === "`") inStr = ch;
25165
+ else if (ch === "(") depth++;
25166
+ else if (ch === ")") {
25167
+ depth--;
25168
+ if (depth === 0) return i2;
25169
+ }
25170
+ }
25171
+ i2++;
25172
+ }
25173
+ return -1;
25174
+ }
25175
+ function findKwargValueEnd(s, start2) {
25176
+ let depth = 0;
25177
+ let inStr = null;
25178
+ for (let i2 = start2; i2 < s.length; i2++) {
25179
+ const ch = s[i2];
25180
+ if (inStr) {
25181
+ if (ch === "\\") {
25182
+ i2++;
25183
+ continue;
25184
+ }
25185
+ if (ch === inStr) inStr = null;
25186
+ continue;
25187
+ }
25188
+ if (ch === '"' || ch === "'" || ch === "`") {
25189
+ inStr = ch;
25190
+ continue;
25191
+ }
25192
+ if (ch === "(" || ch === "[" || ch === "{") depth++;
25193
+ else if (ch === ")" || ch === "]" || ch === "}") {
25194
+ if (depth === 0) return i2;
25195
+ depth--;
25196
+ } else if (ch === "," && depth === 0) {
25197
+ return i2;
25198
+ }
25199
+ }
25200
+ return s.length;
25201
+ }
25202
+ function lineOfCharIndex(code, charIdx) {
25203
+ let line = 1;
25204
+ for (let i2 = 0; i2 < charIdx && i2 < code.length; i2++) {
25205
+ if (code[i2] === "\n") line++;
25206
+ }
25207
+ return line;
25208
+ }
25209
+ function makeExfilFinding(file, line, snippet, fired, receiver) {
25210
+ return {
25211
+ id: `external-secret-exfiltration-${file}-${line}`,
25212
+ pass: "language-sources",
25213
+ category: "security",
25214
+ rule_id: "external-secret-exfiltration",
25215
+ cwe: "CWE-200",
25216
+ severity: "high",
25217
+ level: "error",
25218
+ message: `Environment secret(s) ${fired.join(", ")} transmitted in request body to external host via ${receiver}`,
25219
+ file,
25220
+ line,
25221
+ snippet: snippet.substring(0, 120)
25222
+ };
25223
+ }
25224
+ function findExternalSecretExfiltrationFindings(code, file, language) {
25225
+ const out2 = [];
25226
+ const lines = code.split("\n");
25227
+ const secretVars = collectEnvSecretVars(lines, language);
25228
+ if (secretVars.size === 0) return out2;
25229
+ if (language === "python") {
25230
+ out2.push(...findPythonExfilCalls(code, file, secretVars));
25231
+ } else if (language === "javascript" || language === "typescript") {
25232
+ out2.push(...findJavaScriptExfilCalls(code, file, secretVars));
25233
+ } else if (language === "go") {
25234
+ out2.push(...findGoExfilCalls(code, file, secretVars));
25235
+ }
25236
+ return out2;
25237
+ }
25238
+ function findPythonExfilCalls(code, file, secretVars) {
25239
+ const out2 = [];
25240
+ const callRe = /\b(requests|httpx)\.(post|put|patch|delete|request)\s*\(/g;
25241
+ let m;
25242
+ while (m = callRe.exec(code)) {
25243
+ const argStart = m.index + m[0].length;
25244
+ const argEnd = findBalancedCallEnd(code, argStart);
25245
+ if (argEnd < 0) continue;
25246
+ const args2 = code.slice(argStart, argEnd);
25247
+ const urlMatch = args2.match(/^\s*["']([^"']+)["']/);
25248
+ if (!urlMatch) continue;
25249
+ if (!isExternalHostUrl(urlMatch[1])) continue;
25250
+ const headersIdx = args2.search(/\bheaders\s*=/);
25251
+ let headersStr = "";
25252
+ let bodyStr = args2;
25253
+ if (headersIdx >= 0) {
25254
+ const eqIdx = args2.indexOf("=", headersIdx);
25255
+ const valueStart = eqIdx + 1;
25256
+ const headersEnd = findKwargValueEnd(args2, valueStart);
25257
+ headersStr = args2.slice(valueStart, headersEnd);
25258
+ bodyStr = args2.slice(0, headersIdx) + " " + args2.slice(headersEnd);
25259
+ }
25260
+ const fired = [];
25261
+ for (const v of secretVars.keys()) {
25262
+ const re = new RegExp(`\\b${v}\\b`);
25263
+ if (re.test(bodyStr) && !re.test(headersStr)) fired.push(v);
25264
+ }
25265
+ if (fired.length > 0) {
25266
+ const line = lineOfCharIndex(code, m.index);
25267
+ out2.push(
25268
+ makeExfilFinding(file, line, code.slice(m.index, Math.min(argEnd + 1, m.index + 120)), fired, `${m[1]}.${m[2]}`)
25269
+ );
25270
+ }
25271
+ }
25272
+ return out2;
25273
+ }
25274
+ function findJavaScriptExfilCalls(code, file, secretVars) {
25275
+ const out2 = [];
25276
+ const allLines = code.split("\n");
25277
+ const carriers = /* @__PURE__ */ new Set();
25278
+ const carrierRe = /(?:^|[\s;{])(?:const|let|var)\s+(\w+)\s*=\s*([^;\n]+)/g;
25279
+ let cm;
25280
+ while (cm = carrierRe.exec(code)) {
25281
+ const name2 = cm[1];
25282
+ const rhs = cm[2];
25283
+ for (const v of secretVars.keys()) {
25284
+ if (new RegExp(`\\b${v}\\b`).test(rhs)) {
25285
+ carriers.add(name2);
25286
+ break;
25287
+ }
25288
+ }
25289
+ }
25290
+ const taintedRefs = /* @__PURE__ */ new Set([...secretVars.keys(), ...carriers]);
25291
+ const networkRe = /\b(https?\.(?:request|get)|fetch|axios\.(?:post|put|patch|request|delete))\s*\(/g;
25292
+ let m;
25293
+ while (m = networkRe.exec(code)) {
25294
+ const argStart = m.index + m[0].length;
25295
+ const argEnd = findBalancedCallEnd(code, argStart);
25296
+ if (argEnd < 0) continue;
25297
+ const args2 = code.slice(argStart, argEnd);
25298
+ const urlMatch = args2.match(/^\s*(?:["']([^"']+)["']|`([^`$]+)`)/);
25299
+ if (!urlMatch) continue;
25300
+ const url = urlMatch[1] || urlMatch[2];
25301
+ if (!isExternalHostUrl(url)) continue;
25302
+ const headersIdx = args2.search(/\bheaders\s*:/);
25303
+ let headersStr = "";
25304
+ let restStr = args2;
25305
+ if (headersIdx >= 0) {
25306
+ const valueStart = args2.indexOf(":", headersIdx) + 1;
25307
+ const headersEnd = findKwargValueEnd(args2, valueStart);
25308
+ headersStr = args2.slice(valueStart, headersEnd);
25309
+ restStr = args2.slice(0, headersIdx) + " " + args2.slice(headersEnd);
25310
+ }
25311
+ const fired = /* @__PURE__ */ new Set();
25312
+ for (const v of taintedRefs) {
25313
+ const re = new RegExp(`\\b${v}\\b`);
25314
+ if (re.test(restStr) && !re.test(headersStr)) fired.add(v);
25315
+ }
25316
+ const callLine = lineOfCharIndex(code, m.index);
25317
+ const windowEnd = Math.min(allLines.length, callLine + 20);
25318
+ const writeWindow = allLines.slice(callLine, windowEnd).join("\n");
25319
+ const writeRe = /\b\w+\.(?:write|end)\s*\(\s*(\w+)/g;
25320
+ let wm;
25321
+ while (wm = writeRe.exec(writeWindow)) {
25322
+ if (taintedRefs.has(wm[1])) fired.add(wm[1]);
25323
+ }
25324
+ if (fired.size > 0) {
25325
+ out2.push(
25326
+ makeExfilFinding(
25327
+ file,
25328
+ callLine,
25329
+ code.slice(m.index, Math.min(argEnd + 1, m.index + 120)),
25330
+ [...fired],
25331
+ m[1]
25332
+ )
25333
+ );
25334
+ }
25335
+ }
25336
+ return out2;
25337
+ }
25338
+ function findGoExfilCalls(code, file, secretVars) {
25339
+ const out2 = [];
25340
+ const patterns = [
25341
+ { re: /\bhttp\.PostForm\s*\(/g, urlArgIndex: 0, receiver: "http.PostForm" },
25342
+ { re: /\bhttp\.Post\s*\(/g, urlArgIndex: 0, receiver: "http.Post" },
25343
+ { re: /\bhttp\.NewRequest\s*\(/g, urlArgIndex: 1, receiver: "http.NewRequest" }
25344
+ ];
25345
+ for (const { re, urlArgIndex, receiver } of patterns) {
25346
+ let m;
25347
+ while (m = re.exec(code)) {
25348
+ const argStart = m.index + m[0].length;
25349
+ const argEnd = findBalancedCallEnd(code, argStart);
25350
+ if (argEnd < 0) continue;
25351
+ const args2 = code.slice(argStart, argEnd);
25352
+ let urlSearchSlice = args2;
25353
+ if (urlArgIndex === 1) {
25354
+ const firstComma = findKwargValueEnd(args2, 0);
25355
+ if (firstComma >= args2.length) continue;
25356
+ urlSearchSlice = args2.slice(firstComma + 1);
25357
+ }
25358
+ const urlMatch = urlSearchSlice.match(/^\s*["`]([^"`]+)["`]/);
25359
+ if (!urlMatch) continue;
25360
+ if (!isExternalHostUrl(urlMatch[1])) continue;
25361
+ const fired = [];
25362
+ for (const v of secretVars.keys()) {
25363
+ if (new RegExp(`\\b${v}\\b`).test(args2)) fired.push(v);
25364
+ }
25365
+ if (fired.length > 0) {
25366
+ const line = lineOfCharIndex(code, m.index);
25367
+ out2.push(
25368
+ makeExfilFinding(file, line, code.slice(m.index, Math.min(argEnd + 1, m.index + 120)), fired, receiver)
25369
+ );
25370
+ }
25371
+ }
25372
+ }
25373
+ return out2;
25374
+ }
25375
+ function findPythonPatternFindings(code, file) {
25376
+ const out2 = [];
25377
+ const lines = code.split("\n");
25378
+ const subscriptHeaderRe = /\.headers\s*\[\s*['"]([^'"]+)['"]\s*\]\s*=\s*(.+)$/;
25379
+ const xfoHits = [];
25380
+ const cspHits = [];
25381
+ for (let i2 = 0; i2 < lines.length; i2++) {
25382
+ const raw = lines[i2];
25383
+ const trimmed = raw.trim();
25384
+ if (!trimmed || trimmed.startsWith("#")) continue;
25385
+ const lineNumber = i2 + 1;
25386
+ const sm = trimmed.match(subscriptHeaderRe);
25387
+ if (sm) {
25388
+ const headerName = sm[1];
25389
+ const rhs = sm[2].trim();
25390
+ const headerLower = headerName.toLowerCase();
25391
+ if (headerLower === "access-control-allow-origin") {
25392
+ const valMatch = rhs.match(/^['"]\*['"]/);
25393
+ if (valMatch) {
25394
+ out2.push({
25395
+ id: `cors-wildcard-origin-${file}-${lineNumber}`,
25396
+ pass: "language-sources",
25397
+ category: "security",
25398
+ rule_id: "cors-wildcard-origin",
25399
+ cwe: "CWE-942",
25400
+ severity: "medium",
25401
+ level: "warning",
25402
+ message: "CORS Access-Control-Allow-Origin set to wildcard '*': any origin may read responses",
25403
+ file,
25404
+ line: lineNumber,
25405
+ snippet: trimmed.substring(0, 100)
25406
+ });
25407
+ }
25408
+ }
25409
+ if (headerLower === "x-frame-options") {
25410
+ xfoHits.push({ line: lineNumber, value: rhs });
25411
+ } else if (headerLower === "content-security-policy") {
25412
+ cspHits.push({ line: lineNumber, value: rhs });
25413
+ }
25414
+ }
25415
+ if (/\bverify_mode\s*=\s*ssl\.CERT_NONE\b/.test(trimmed)) {
25416
+ out2.push({
25417
+ id: `tls-verify-disabled-${file}-${lineNumber}`,
25418
+ pass: "language-sources",
25419
+ category: "security",
25420
+ rule_id: "tls-verify-disabled",
25421
+ cwe: "CWE-295",
25422
+ severity: "high",
25423
+ level: "error",
25424
+ message: "TLS certificate verification disabled: ssl context verify_mode set to CERT_NONE",
25425
+ file,
25426
+ line: lineNumber,
25427
+ snippet: trimmed.substring(0, 100)
25428
+ });
25429
+ } else if (/\bcheck_hostname\s*=\s*False\b/.test(trimmed)) {
25430
+ out2.push({
25431
+ id: `tls-verify-disabled-${file}-${lineNumber}`,
25432
+ pass: "language-sources",
25433
+ category: "security",
25434
+ rule_id: "tls-verify-disabled",
25435
+ cwe: "CWE-295",
25436
+ severity: "high",
25437
+ level: "error",
25438
+ message: "TLS hostname verification disabled: ssl context check_hostname set to False",
25439
+ file,
25440
+ line: lineNumber,
25441
+ snippet: trimmed.substring(0, 100)
25442
+ });
25443
+ }
25444
+ }
25445
+ const restrictiveXfo = xfoHits.find(
25446
+ (h) => /['"](?:DENY|SAMEORIGIN)['"]/i.test(h.value)
25447
+ );
25448
+ const permissiveCsp = cspHits.find((h) => {
25449
+ const pm = h.value.match(/['"]([^'"]+)['"]/);
25450
+ if (!pm) return false;
25451
+ const policy = pm[1];
25452
+ const faMatch = policy.match(/frame-ancestors\s+([^;]+)/i);
25453
+ if (!faMatch) return false;
25454
+ const directive = faMatch[1].trim();
25455
+ return /(^|\s)\*(\s|$)/.test(directive) || /\bhttps?:\/\//i.test(directive);
25456
+ });
25457
+ if (restrictiveXfo && permissiveCsp) {
25458
+ out2.push({
25459
+ id: `xfo-csp-mismatch-${file}-${restrictiveXfo.line}`,
25460
+ pass: "language-sources",
25461
+ category: "security",
25462
+ rule_id: "xfo-csp-mismatch",
25463
+ cwe: "CWE-1021",
25464
+ severity: "medium",
25465
+ level: "warning",
25466
+ message: "X-Frame-Options/CSP frame-ancestors mismatch: XFO restricts framing but CSP frame-ancestors is permissive (CSP overrides on modern browsers)",
25467
+ file,
25468
+ line: restrictiveXfo.line,
25469
+ snippet: lines[restrictiveXfo.line - 1].trim().substring(0, 100)
25470
+ });
25471
+ }
25472
+ return out2;
25473
+ }
25474
+ function findRustPatternFindings(code, file) {
25475
+ const out2 = [];
25476
+ const lines = code.split("\n");
25477
+ const re = /\.\s*(danger_accept_invalid_certs|danger_accept_invalid_hostnames)\s*\(\s*true\s*\)/;
25478
+ for (let i2 = 0; i2 < lines.length; i2++) {
25479
+ const raw = lines[i2];
25480
+ const trimmed = raw.trim();
25481
+ if (!trimmed || trimmed.startsWith("//")) continue;
25482
+ const m = trimmed.match(re);
25483
+ if (!m) continue;
25484
+ const method = m[1];
25485
+ const what = method === "danger_accept_invalid_certs" ? "certificate" : "hostname";
25486
+ out2.push({
25487
+ id: `tls-verify-disabled-${file}-${i2 + 1}`,
25488
+ pass: "language-sources",
25489
+ category: "security",
25490
+ rule_id: "tls-verify-disabled",
25491
+ cwe: "CWE-295",
25492
+ severity: "high",
25493
+ level: "error",
25494
+ message: `TLS ${what} verification disabled: reqwest builder ${method}(true)`,
25495
+ file,
25496
+ line: i2 + 1,
25497
+ snippet: trimmed.substring(0, 100)
25498
+ });
25499
+ }
25500
+ return out2;
25501
+ }
24833
25502
 
24834
25503
  // src/analysis/passes/sink-filter-pass.ts
24835
25504
  var JS_XSS_SANITIZERS = [