circle-ir 3.119.0 → 3.123.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}`,
@@ -23106,6 +23135,113 @@ function truncate(s, maxLen) {
23106
23135
  return s.length > maxLen ? s.slice(0, maxLen) + "..." : s;
23107
23136
  }
23108
23137
 
23138
+ // src/analysis/html/vue-template-xss-pass.ts
23139
+ var DANGEROUS_BINDINGS = /* @__PURE__ */ new Set([
23140
+ "v-html",
23141
+ "v-bind:innerhtml",
23142
+ ":innerhtml",
23143
+ "v-bind:outerhtml",
23144
+ ":outerhtml"
23145
+ ]);
23146
+ var ID_RE = /\b([A-Za-z_$][A-Za-z0-9_$]*)\b/g;
23147
+ var RESERVED = /* @__PURE__ */ new Set([
23148
+ "true",
23149
+ "false",
23150
+ "null",
23151
+ "undefined",
23152
+ "this",
23153
+ "new",
23154
+ "typeof",
23155
+ "void",
23156
+ "delete",
23157
+ "instanceof",
23158
+ "in",
23159
+ "of",
23160
+ "Math",
23161
+ "Number",
23162
+ "String",
23163
+ "Boolean",
23164
+ "Array",
23165
+ "Object",
23166
+ "JSON"
23167
+ ]);
23168
+ function runVueTemplateXssChecks(rootNode, filePath, scriptResults) {
23169
+ const taintedNames = collectTaintedNames(scriptResults);
23170
+ if (taintedNames.size === 0) return [];
23171
+ const findings = [];
23172
+ const stack = [rootNode];
23173
+ while (stack.length > 0) {
23174
+ const node = stack.pop();
23175
+ if (node.type === "start_tag" || node.type === "self_closing_tag") {
23176
+ checkElementBindings(node, filePath, taintedNames, findings);
23177
+ }
23178
+ for (let i2 = node.childCount - 1; i2 >= 0; i2--) {
23179
+ const c = node.child(i2);
23180
+ if (c) stack.push(c);
23181
+ }
23182
+ }
23183
+ return findings;
23184
+ }
23185
+ function checkElementBindings(tag, filePath, tainted, out2) {
23186
+ for (let i2 = 0; i2 < tag.childCount; i2++) {
23187
+ const attr = tag.child(i2);
23188
+ if (!attr || attr.type !== "attribute") continue;
23189
+ const nameNode = findChildByType2(attr, "attribute_name");
23190
+ if (!nameNode) continue;
23191
+ const lcName = nameNode.text.toLowerCase();
23192
+ if (!DANGEROUS_BINDINGS.has(lcName)) continue;
23193
+ const valueNode = findChildByType2(attr, "quoted_attribute_value") ?? findChildByType2(attr, "attribute_value");
23194
+ if (!valueNode) continue;
23195
+ const rhs = stripQuotes2(valueNode.text);
23196
+ if (!rhs.trim()) continue;
23197
+ const matched = matchTaint(rhs, tainted);
23198
+ if (!matched) continue;
23199
+ const line = nameNode.startPosition.row + 1;
23200
+ out2.push({
23201
+ id: `vue-template-xss-${filePath}-${line}-${lcName}`,
23202
+ pass: "vue-template-xss",
23203
+ category: "security",
23204
+ rule_id: "vue-template-xss",
23205
+ cwe: "CWE-79",
23206
+ severity: "high",
23207
+ level: "error",
23208
+ message: `Vue template attribute "${nameNode.text}" binds tainted identifier "${matched}" \u2014 writes raw HTML (XSS risk).`,
23209
+ file: filePath,
23210
+ line,
23211
+ snippet: `${nameNode.text}="${rhs}"`
23212
+ });
23213
+ }
23214
+ }
23215
+ function matchTaint(rhs, tainted) {
23216
+ ID_RE.lastIndex = 0;
23217
+ let m;
23218
+ while ((m = ID_RE.exec(rhs)) !== null) {
23219
+ const id = m[1];
23220
+ if (RESERVED.has(id)) continue;
23221
+ if (tainted.has(id)) return id;
23222
+ }
23223
+ return void 0;
23224
+ }
23225
+ function collectTaintedNames(blocks) {
23226
+ const names = /* @__PURE__ */ new Set();
23227
+ for (const { ir } of blocks) {
23228
+ const sourceLines = /* @__PURE__ */ new Set();
23229
+ for (const source of ir.taint.sources) {
23230
+ sourceLines.add(source.line);
23231
+ if (source.variable) names.add(source.variable);
23232
+ }
23233
+ for (const def of ir.dfg.defs) {
23234
+ if (sourceLines.has(def.line) && def.variable) names.add(def.variable);
23235
+ }
23236
+ for (const flow of ir.taint.flows ?? []) {
23237
+ for (const step of flow.path ?? []) {
23238
+ if (step.variable) names.add(step.variable);
23239
+ }
23240
+ }
23241
+ }
23242
+ return names;
23243
+ }
23244
+
23109
23245
  // src/analysis/html/html-merge.ts
23110
23246
  function mergeHtmlResults(htmlMeta, scriptResults, attributeFindings) {
23111
23247
  const allTypes = [];
@@ -23421,7 +23557,12 @@ var PYTHON_TAINTED_PATTERNS2 = [
23421
23557
  { pattern: /\bget_form_parameter\s*\(/, type: "http_body" },
23422
23558
  { pattern: /\bget_query_parameter\s*\(/, type: "http_param" },
23423
23559
  { pattern: /\bget_header_value\s*\(/, type: "http_header" },
23424
- { 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" }
23425
23566
  ];
23426
23567
  var LanguageSourcesPass = class {
23427
23568
  name = "language-sources";
@@ -23480,6 +23621,21 @@ var LanguageSourcesPass = class {
23480
23621
  });
23481
23622
  }
23482
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
+ }
23483
23639
  }
23484
23640
  const jsTaintedVars = buildJavaScriptTaintedVars(code, language);
23485
23641
  if (language === "bash") {
@@ -23498,10 +23654,28 @@ var LanguageSourcesPass = class {
23498
23654
  if (language === "python") {
23499
23655
  additionalSanitizers.push(...findPythonNetlocAllowlistGuardSanitizers(code));
23500
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
+ }
23501
23661
  }
23502
23662
  if (language === "rust") {
23503
23663
  additionalSanitizers.push(...findRustSetAllowlistGuardSanitizers(code));
23504
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 === "python" || language === "javascript" || language === "typescript" || language === "go") {
23671
+ const exfilFindings = findExternalSecretExfiltrationFindings(
23672
+ code,
23673
+ graph.ir.meta.file,
23674
+ language
23675
+ );
23676
+ for (const finding of exfilFindings) {
23677
+ ctx.addFinding(finding);
23678
+ }
23505
23679
  }
23506
23680
  attachSourceLineCode(additionalSources, additionalSinks, code);
23507
23681
  return { additionalSources, additionalSinks, additionalSanitizers, pyTaintedVars, pySanitizedVars, jsTaintedVars };
@@ -24059,6 +24233,44 @@ function findPythonReturnXSSSinks(sourceCode, taintedVars) {
24059
24233
  }
24060
24234
  return sinks;
24061
24235
  }
24236
+ function findPythonReflectionInvocationSinks(sourceCode, taintedVars) {
24237
+ if (taintedVars.size === 0) return [];
24238
+ const sinks = [];
24239
+ const lines = sourceCode.split("\n");
24240
+ const directRe = /\bgetattr\s*\(\s*[^,()]+\s*,\s*([A-Za-z_][\w]*)\s*\)\s*\(/;
24241
+ const bindRe = /^\s*([A-Za-z_][\w]*)\s*=\s*getattr\s*\(\s*[^,()]+\s*,\s*([A-Za-z_][\w]*)\s*\)\s*$/;
24242
+ const aliases = [];
24243
+ for (let i2 = 0; i2 < lines.length; i2++) {
24244
+ const line = lines[i2];
24245
+ if (line.trimStart().startsWith("#")) continue;
24246
+ const dm = line.match(directRe);
24247
+ if (dm && taintedVars.has(dm[1])) {
24248
+ sinks.push({ sinkLine: i2 + 1, method: "getattr" });
24249
+ continue;
24250
+ }
24251
+ const bm = line.match(bindRe);
24252
+ if (bm && taintedVars.has(bm[2])) {
24253
+ aliases.push({ name: bm[1], bindLine: i2 + 1 });
24254
+ }
24255
+ }
24256
+ if (aliases.length === 0) return sinks;
24257
+ const firedBindLines = /* @__PURE__ */ new Set();
24258
+ for (let i2 = 0; i2 < lines.length; i2++) {
24259
+ const line = lines[i2];
24260
+ if (line.trimStart().startsWith("#")) continue;
24261
+ const lineNum = i2 + 1;
24262
+ for (const a of aliases) {
24263
+ if (lineNum <= a.bindLine) continue;
24264
+ if (firedBindLines.has(a.bindLine)) continue;
24265
+ const invokeRe = new RegExp(`(?<![\\w.])${a.name}\\s*\\(`);
24266
+ if (invokeRe.test(line)) {
24267
+ sinks.push({ sinkLine: a.bindLine, method: "getattr" });
24268
+ firedBindLines.add(a.bindLine);
24269
+ }
24270
+ }
24271
+ }
24272
+ return sinks;
24273
+ }
24062
24274
  function findJavaScriptDOMSinks(sourceCode, language) {
24063
24275
  if (!["javascript", "typescript"].includes(language)) return [];
24064
24276
  const sinks = [];
@@ -24250,9 +24462,33 @@ function findBashTaintSources(sourceCode, dfg) {
24250
24462
  return sources;
24251
24463
  }
24252
24464
  var BASH_CREDENTIAL_PATTERN = /^(.*?)(password|passwd|secret|api_?key|token|auth_token|private_key|access_key)\s*=\s*["']?([^"'\s$][^"'\s]*)["']?\s*$/i;
24465
+ var BASH_CHECKSUM_VERIFY_PATTERN = /\b(?:sha(?:1|224|256|384|512)sum|md5sum|cksum|b2sum)\s+(?:-c\b|--check\b)/;
24466
+ function collectChecksumVerifiedTmpPaths(lines) {
24467
+ const verified = /* @__PURE__ */ new Set();
24468
+ for (const line of lines) {
24469
+ if (!BASH_CHECKSUM_VERIFY_PATTERN.test(line)) continue;
24470
+ const matches = line.match(/\/tmp\/[^\s"'$|`]+/g);
24471
+ if (!matches) continue;
24472
+ for (const p of matches) verified.add(p);
24473
+ }
24474
+ return verified;
24475
+ }
24476
+ var BASH_ARCHIVE_EXT_PATTERN = /\.(?:tgz|tar\.gz|tar\.bz2|tar\.xz|tar|tbz2|txz|zip|gz|bz2|xz|7z)$/i;
24477
+ function isArchiveOutputContext(line, tmpRel) {
24478
+ if (!BASH_ARCHIVE_EXT_PATTERN.test(tmpRel)) return false;
24479
+ if (/\btar\b/.test(line) && /(?:^|\s)-?[A-Za-z]*c[A-Za-z]*\b/.test(line)) return true;
24480
+ if (/\bzip\b/.test(line) && !/\bunzip\b/.test(line)) return true;
24481
+ if (/\bgzip\b/.test(line) && /(?:-c\b|--stdout\b|>)/.test(line)) return true;
24482
+ if (/\bbzip2\b/.test(line) && /(?:-c\b|--stdout\b|>)/.test(line)) return true;
24483
+ if (/\bxz\b/.test(line) && /(?:-c\b|--stdout\b|>)/.test(line)) return true;
24484
+ if (/\b7z\s+a\b/.test(line)) return true;
24485
+ return false;
24486
+ }
24253
24487
  function findBashPatternFindings(sourceCode, file) {
24254
24488
  const findings = [];
24255
24489
  const lines = sourceCode.split("\n");
24490
+ const checksumVerifiedTmpPaths = collectChecksumVerifiedTmpPaths(lines);
24491
+ const scriptHasVerifier = hasIntegrityVerifierAnywhere(lines);
24256
24492
  for (let i2 = 0; i2 < lines.length; i2++) {
24257
24493
  const line = lines[i2];
24258
24494
  const trimmed = line.trim();
@@ -24294,19 +24530,25 @@ function findBashPatternFindings(sourceCode, file) {
24294
24530
  }
24295
24531
  const tmpMatch = trimmed.match(/\/tmp\/([^\s"'$]+)/);
24296
24532
  if (tmpMatch && !/mktemp/.test(trimmed)) {
24297
- findings.push({
24298
- id: `predictable-temp-file-${file}-${lineNumber}`,
24299
- pass: "language-sources",
24300
- category: "security",
24301
- rule_id: "predictable-temp-file",
24302
- cwe: "CWE-377",
24303
- severity: "medium",
24304
- level: "warning",
24305
- message: `Predictable temp file: /tmp/${tmpMatch[1]}. Use mktemp instead`,
24306
- file,
24307
- line: lineNumber,
24308
- snippet: trimmed.substring(0, 80)
24309
- });
24533
+ const tmpRel = tmpMatch[1];
24534
+ const tmpPath = `/tmp/${tmpRel}`;
24535
+ const isChecksumVerified = checksumVerifiedTmpPaths.has(tmpPath);
24536
+ const isArchiveOutput = isArchiveOutputContext(trimmed, tmpRel);
24537
+ if (!isChecksumVerified && !isArchiveOutput) {
24538
+ findings.push({
24539
+ id: `predictable-temp-file-${file}-${lineNumber}`,
24540
+ pass: "language-sources",
24541
+ category: "security",
24542
+ rule_id: "predictable-temp-file",
24543
+ cwe: "CWE-377",
24544
+ severity: "medium",
24545
+ level: "warning",
24546
+ message: `Predictable temp file: /tmp/${tmpRel}. Use mktemp instead`,
24547
+ file,
24548
+ line: lineNumber,
24549
+ snippet: trimmed.substring(0, 80)
24550
+ });
24551
+ }
24310
24552
  }
24311
24553
  if (/\bchmod\b/.test(trimmed) && /\b(777|666)\b/.test(trimmed)) {
24312
24554
  const mode = trimmed.match(/\b(777|666)\b/)[1];
@@ -24339,9 +24581,78 @@ function findBashPatternFindings(sourceCode, file) {
24339
24581
  snippet: trimmed.substring(0, 80)
24340
24582
  });
24341
24583
  }
24584
+ if (!scriptHasVerifier) {
24585
+ const installer = matchUnverifiedPackageInstall(trimmed);
24586
+ if (installer) {
24587
+ findings.push({
24588
+ id: `unverified-package-install-${file}-${lineNumber}`,
24589
+ pass: "language-sources",
24590
+ category: "security",
24591
+ rule_id: "unverified-package-install",
24592
+ cwe: "CWE-494",
24593
+ severity: "high",
24594
+ level: "error",
24595
+ message: `Unverified package install via ${installer}: package contents are not integrity-checked (no gpg/sha256sum verify in script)`,
24596
+ file,
24597
+ line: lineNumber,
24598
+ snippet: trimmed.substring(0, 80)
24599
+ });
24600
+ }
24601
+ }
24602
+ const weakHashAlg = matchBashWeakHashCommand(trimmed);
24603
+ if (weakHashAlg) {
24604
+ findings.push({
24605
+ id: `weak-hash-${file}-${lineNumber}`,
24606
+ pass: "language-sources",
24607
+ category: "security",
24608
+ rule_id: "weak-hash",
24609
+ cwe: "CWE-328",
24610
+ severity: "medium",
24611
+ level: "warning",
24612
+ message: `Weak hash algorithm: ${weakHashAlg} is cryptographically broken. Use sha256sum or sha512sum`,
24613
+ file,
24614
+ line: lineNumber,
24615
+ snippet: trimmed.substring(0, 80)
24616
+ });
24617
+ }
24342
24618
  }
24343
24619
  return findings;
24344
24620
  }
24621
+ function matchBashWeakHashCommand(line) {
24622
+ const re = /(?:^|[|;]|&&|\|\|)\s*(md5sum|sha1sum|md5|sha1)\b(?!\s*=)/;
24623
+ const m = line.match(re);
24624
+ if (!m) return null;
24625
+ return m[1];
24626
+ }
24627
+ function matchUnverifiedPackageInstall(line) {
24628
+ if (/\bdpkg\b/.test(line) && /(?:^|\s)(?:-[a-zA-Z]*[iIU][a-zA-Z]*|--install)\b/.test(line)) {
24629
+ return "dpkg";
24630
+ }
24631
+ 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)) {
24632
+ return "rpm";
24633
+ }
24634
+ const aptMatch = line.match(/\b(apt-get|apt|aptitude)\s+install\b/);
24635
+ if (aptMatch && /\.deb\b/.test(line)) {
24636
+ return aptMatch[1];
24637
+ }
24638
+ const yumMatch = line.match(/\b(yum|dnf|zypper)\s+install\b/);
24639
+ if (yumMatch && /\.rpm\b/.test(line)) {
24640
+ return yumMatch[1];
24641
+ }
24642
+ return null;
24643
+ }
24644
+ function hasIntegrityVerifierAnywhere(lines) {
24645
+ const sigRe = /\b(?:gpg(?:v|2)?|gpg)\s+(?:[^|]*\s)?--verify\b/;
24646
+ const rpmSigRe = /\brpm\s+(?:[^|]*\s)?--checksig\b/;
24647
+ const dpkgSigRe = /\bdpkg\s+(?:[^|]*\s)?--verify\b/;
24648
+ const sumRe = /\b(?:sha(?:1|224|256|384|512)sum|md5sum|cksum|b2sum)\s+(?:[^|]*\s)?(?:-c|--check)\b/;
24649
+ for (const line of lines) {
24650
+ if (sigRe.test(line) || rpmSigRe.test(line) || dpkgSigRe.test(line) || sumRe.test(line)) {
24651
+ return true;
24652
+ }
24653
+ }
24654
+ return false;
24655
+ }
24345
24656
  function findBashRegexAllowlistSanitizers(code) {
24346
24657
  const sanitizers = [];
24347
24658
  const lines = code.split("\n");
@@ -24694,6 +25005,388 @@ function findRustCanonicalizeGuardSanitizers(code) {
24694
25005
  }
24695
25006
  return sanitizers;
24696
25007
  }
25008
+ function collectEnvSecretVars(lines, language) {
25009
+ const out2 = /* @__PURE__ */ new Map();
25010
+ for (let i2 = 0; i2 < lines.length; i2++) {
25011
+ const line = lines[i2];
25012
+ if (language === "python") {
25013
+ const m = line.match(/^\s*(\w+)\s*=\s*os\.(?:environ\s*[\[.]|getenv\b)/);
25014
+ if (m) out2.set(m[1], i2 + 1);
25015
+ } else if (language === "javascript" || language === "typescript") {
25016
+ const m = line.match(/(?:^|\s|;)(?:const|let|var)\s+(\w+)\s*=\s*process\.env\b/);
25017
+ if (m) out2.set(m[1], i2 + 1);
25018
+ } else if (language === "go") {
25019
+ const m = line.match(/^\s*(\w+)\s*:?=\s*os\.Getenv\s*\(/);
25020
+ if (m) out2.set(m[1], i2 + 1);
25021
+ }
25022
+ }
25023
+ return out2;
25024
+ }
25025
+ function isExternalHostUrl(url) {
25026
+ const lower = url.toLowerCase();
25027
+ if (!/^https?:\/\//.test(lower)) return false;
25028
+ const host = lower.replace(/^https?:\/\//, "").split(/[/?#:]/)[0];
25029
+ if (!host) return false;
25030
+ if (host === "localhost" || host === "127.0.0.1" || host === "0.0.0.0" || host === "::1") return false;
25031
+ if (/^10\./.test(host)) return false;
25032
+ if (/^192\.168\./.test(host)) return false;
25033
+ if (/^172\.(1[6-9]|2\d|3[01])\./.test(host)) return false;
25034
+ if (host.includes(".internal.") || host.endsWith(".internal") || host.startsWith("internal.")) return false;
25035
+ if (host.endsWith(".local") || host.endsWith(".lan") || host.endsWith(".corp")) return false;
25036
+ if (!host.includes(".")) return false;
25037
+ return true;
25038
+ }
25039
+ function findBalancedCallEnd(code, start2) {
25040
+ let depth = 1;
25041
+ let i2 = start2;
25042
+ let inStr = null;
25043
+ while (i2 < code.length) {
25044
+ const ch = code[i2];
25045
+ if (inStr) {
25046
+ if (ch === "\\") {
25047
+ i2 += 2;
25048
+ continue;
25049
+ }
25050
+ if (ch === inStr) inStr = null;
25051
+ } else {
25052
+ if (ch === '"' || ch === "'" || ch === "`") inStr = ch;
25053
+ else if (ch === "(") depth++;
25054
+ else if (ch === ")") {
25055
+ depth--;
25056
+ if (depth === 0) return i2;
25057
+ }
25058
+ }
25059
+ i2++;
25060
+ }
25061
+ return -1;
25062
+ }
25063
+ function findKwargValueEnd(s, start2) {
25064
+ let depth = 0;
25065
+ let inStr = null;
25066
+ for (let i2 = start2; i2 < s.length; i2++) {
25067
+ const ch = s[i2];
25068
+ if (inStr) {
25069
+ if (ch === "\\") {
25070
+ i2++;
25071
+ continue;
25072
+ }
25073
+ if (ch === inStr) inStr = null;
25074
+ continue;
25075
+ }
25076
+ if (ch === '"' || ch === "'" || ch === "`") {
25077
+ inStr = ch;
25078
+ continue;
25079
+ }
25080
+ if (ch === "(" || ch === "[" || ch === "{") depth++;
25081
+ else if (ch === ")" || ch === "]" || ch === "}") {
25082
+ if (depth === 0) return i2;
25083
+ depth--;
25084
+ } else if (ch === "," && depth === 0) {
25085
+ return i2;
25086
+ }
25087
+ }
25088
+ return s.length;
25089
+ }
25090
+ function lineOfCharIndex(code, charIdx) {
25091
+ let line = 1;
25092
+ for (let i2 = 0; i2 < charIdx && i2 < code.length; i2++) {
25093
+ if (code[i2] === "\n") line++;
25094
+ }
25095
+ return line;
25096
+ }
25097
+ function makeExfilFinding(file, line, snippet, fired, receiver) {
25098
+ return {
25099
+ id: `external-secret-exfiltration-${file}-${line}`,
25100
+ pass: "language-sources",
25101
+ category: "security",
25102
+ rule_id: "external-secret-exfiltration",
25103
+ cwe: "CWE-200",
25104
+ severity: "high",
25105
+ level: "error",
25106
+ message: `Environment secret(s) ${fired.join(", ")} transmitted in request body to external host via ${receiver}`,
25107
+ file,
25108
+ line,
25109
+ snippet: snippet.substring(0, 120)
25110
+ };
25111
+ }
25112
+ function findExternalSecretExfiltrationFindings(code, file, language) {
25113
+ const out2 = [];
25114
+ const lines = code.split("\n");
25115
+ const secretVars = collectEnvSecretVars(lines, language);
25116
+ if (secretVars.size === 0) return out2;
25117
+ if (language === "python") {
25118
+ out2.push(...findPythonExfilCalls(code, file, secretVars));
25119
+ } else if (language === "javascript" || language === "typescript") {
25120
+ out2.push(...findJavaScriptExfilCalls(code, file, secretVars));
25121
+ } else if (language === "go") {
25122
+ out2.push(...findGoExfilCalls(code, file, secretVars));
25123
+ }
25124
+ return out2;
25125
+ }
25126
+ function findPythonExfilCalls(code, file, secretVars) {
25127
+ const out2 = [];
25128
+ const callRe = /\b(requests|httpx)\.(post|put|patch|delete|request)\s*\(/g;
25129
+ let m;
25130
+ while (m = callRe.exec(code)) {
25131
+ const argStart = m.index + m[0].length;
25132
+ const argEnd = findBalancedCallEnd(code, argStart);
25133
+ if (argEnd < 0) continue;
25134
+ const args2 = code.slice(argStart, argEnd);
25135
+ const urlMatch = args2.match(/^\s*["']([^"']+)["']/);
25136
+ if (!urlMatch) continue;
25137
+ if (!isExternalHostUrl(urlMatch[1])) continue;
25138
+ const headersIdx = args2.search(/\bheaders\s*=/);
25139
+ let headersStr = "";
25140
+ let bodyStr = args2;
25141
+ if (headersIdx >= 0) {
25142
+ const eqIdx = args2.indexOf("=", headersIdx);
25143
+ const valueStart = eqIdx + 1;
25144
+ const headersEnd = findKwargValueEnd(args2, valueStart);
25145
+ headersStr = args2.slice(valueStart, headersEnd);
25146
+ bodyStr = args2.slice(0, headersIdx) + " " + args2.slice(headersEnd);
25147
+ }
25148
+ const fired = [];
25149
+ for (const v of secretVars.keys()) {
25150
+ const re = new RegExp(`\\b${v}\\b`);
25151
+ if (re.test(bodyStr) && !re.test(headersStr)) fired.push(v);
25152
+ }
25153
+ if (fired.length > 0) {
25154
+ const line = lineOfCharIndex(code, m.index);
25155
+ out2.push(
25156
+ makeExfilFinding(file, line, code.slice(m.index, Math.min(argEnd + 1, m.index + 120)), fired, `${m[1]}.${m[2]}`)
25157
+ );
25158
+ }
25159
+ }
25160
+ return out2;
25161
+ }
25162
+ function findJavaScriptExfilCalls(code, file, secretVars) {
25163
+ const out2 = [];
25164
+ const allLines = code.split("\n");
25165
+ const carriers = /* @__PURE__ */ new Set();
25166
+ const carrierRe = /(?:^|[\s;{])(?:const|let|var)\s+(\w+)\s*=\s*([^;\n]+)/g;
25167
+ let cm;
25168
+ while (cm = carrierRe.exec(code)) {
25169
+ const name2 = cm[1];
25170
+ const rhs = cm[2];
25171
+ for (const v of secretVars.keys()) {
25172
+ if (new RegExp(`\\b${v}\\b`).test(rhs)) {
25173
+ carriers.add(name2);
25174
+ break;
25175
+ }
25176
+ }
25177
+ }
25178
+ const taintedRefs = /* @__PURE__ */ new Set([...secretVars.keys(), ...carriers]);
25179
+ const networkRe = /\b(https?\.(?:request|get)|fetch|axios\.(?:post|put|patch|request|delete))\s*\(/g;
25180
+ let m;
25181
+ while (m = networkRe.exec(code)) {
25182
+ const argStart = m.index + m[0].length;
25183
+ const argEnd = findBalancedCallEnd(code, argStart);
25184
+ if (argEnd < 0) continue;
25185
+ const args2 = code.slice(argStart, argEnd);
25186
+ const urlMatch = args2.match(/^\s*(?:["']([^"']+)["']|`([^`$]+)`)/);
25187
+ if (!urlMatch) continue;
25188
+ const url = urlMatch[1] || urlMatch[2];
25189
+ if (!isExternalHostUrl(url)) continue;
25190
+ const headersIdx = args2.search(/\bheaders\s*:/);
25191
+ let headersStr = "";
25192
+ let restStr = args2;
25193
+ if (headersIdx >= 0) {
25194
+ const valueStart = args2.indexOf(":", headersIdx) + 1;
25195
+ const headersEnd = findKwargValueEnd(args2, valueStart);
25196
+ headersStr = args2.slice(valueStart, headersEnd);
25197
+ restStr = args2.slice(0, headersIdx) + " " + args2.slice(headersEnd);
25198
+ }
25199
+ const fired = /* @__PURE__ */ new Set();
25200
+ for (const v of taintedRefs) {
25201
+ const re = new RegExp(`\\b${v}\\b`);
25202
+ if (re.test(restStr) && !re.test(headersStr)) fired.add(v);
25203
+ }
25204
+ const callLine = lineOfCharIndex(code, m.index);
25205
+ const windowEnd = Math.min(allLines.length, callLine + 20);
25206
+ const writeWindow = allLines.slice(callLine, windowEnd).join("\n");
25207
+ const writeRe = /\b\w+\.(?:write|end)\s*\(\s*(\w+)/g;
25208
+ let wm;
25209
+ while (wm = writeRe.exec(writeWindow)) {
25210
+ if (taintedRefs.has(wm[1])) fired.add(wm[1]);
25211
+ }
25212
+ if (fired.size > 0) {
25213
+ out2.push(
25214
+ makeExfilFinding(
25215
+ file,
25216
+ callLine,
25217
+ code.slice(m.index, Math.min(argEnd + 1, m.index + 120)),
25218
+ [...fired],
25219
+ m[1]
25220
+ )
25221
+ );
25222
+ }
25223
+ }
25224
+ return out2;
25225
+ }
25226
+ function findGoExfilCalls(code, file, secretVars) {
25227
+ const out2 = [];
25228
+ const patterns = [
25229
+ { re: /\bhttp\.PostForm\s*\(/g, urlArgIndex: 0, receiver: "http.PostForm" },
25230
+ { re: /\bhttp\.Post\s*\(/g, urlArgIndex: 0, receiver: "http.Post" },
25231
+ { re: /\bhttp\.NewRequest\s*\(/g, urlArgIndex: 1, receiver: "http.NewRequest" }
25232
+ ];
25233
+ for (const { re, urlArgIndex, receiver } of patterns) {
25234
+ let m;
25235
+ while (m = re.exec(code)) {
25236
+ const argStart = m.index + m[0].length;
25237
+ const argEnd = findBalancedCallEnd(code, argStart);
25238
+ if (argEnd < 0) continue;
25239
+ const args2 = code.slice(argStart, argEnd);
25240
+ let urlSearchSlice = args2;
25241
+ if (urlArgIndex === 1) {
25242
+ const firstComma = findKwargValueEnd(args2, 0);
25243
+ if (firstComma >= args2.length) continue;
25244
+ urlSearchSlice = args2.slice(firstComma + 1);
25245
+ }
25246
+ const urlMatch = urlSearchSlice.match(/^\s*["`]([^"`]+)["`]/);
25247
+ if (!urlMatch) continue;
25248
+ if (!isExternalHostUrl(urlMatch[1])) continue;
25249
+ const fired = [];
25250
+ for (const v of secretVars.keys()) {
25251
+ if (new RegExp(`\\b${v}\\b`).test(args2)) fired.push(v);
25252
+ }
25253
+ if (fired.length > 0) {
25254
+ const line = lineOfCharIndex(code, m.index);
25255
+ out2.push(
25256
+ makeExfilFinding(file, line, code.slice(m.index, Math.min(argEnd + 1, m.index + 120)), fired, receiver)
25257
+ );
25258
+ }
25259
+ }
25260
+ }
25261
+ return out2;
25262
+ }
25263
+ function findPythonPatternFindings(code, file) {
25264
+ const out2 = [];
25265
+ const lines = code.split("\n");
25266
+ const subscriptHeaderRe = /\.headers\s*\[\s*['"]([^'"]+)['"]\s*\]\s*=\s*(.+)$/;
25267
+ const xfoHits = [];
25268
+ const cspHits = [];
25269
+ for (let i2 = 0; i2 < lines.length; i2++) {
25270
+ const raw = lines[i2];
25271
+ const trimmed = raw.trim();
25272
+ if (!trimmed || trimmed.startsWith("#")) continue;
25273
+ const lineNumber = i2 + 1;
25274
+ const sm = trimmed.match(subscriptHeaderRe);
25275
+ if (sm) {
25276
+ const headerName = sm[1];
25277
+ const rhs = sm[2].trim();
25278
+ const headerLower = headerName.toLowerCase();
25279
+ if (headerLower === "access-control-allow-origin") {
25280
+ const valMatch = rhs.match(/^['"]\*['"]/);
25281
+ if (valMatch) {
25282
+ out2.push({
25283
+ id: `cors-wildcard-origin-${file}-${lineNumber}`,
25284
+ pass: "language-sources",
25285
+ category: "security",
25286
+ rule_id: "cors-wildcard-origin",
25287
+ cwe: "CWE-942",
25288
+ severity: "medium",
25289
+ level: "warning",
25290
+ message: "CORS Access-Control-Allow-Origin set to wildcard '*': any origin may read responses",
25291
+ file,
25292
+ line: lineNumber,
25293
+ snippet: trimmed.substring(0, 100)
25294
+ });
25295
+ }
25296
+ }
25297
+ if (headerLower === "x-frame-options") {
25298
+ xfoHits.push({ line: lineNumber, value: rhs });
25299
+ } else if (headerLower === "content-security-policy") {
25300
+ cspHits.push({ line: lineNumber, value: rhs });
25301
+ }
25302
+ }
25303
+ if (/\bverify_mode\s*=\s*ssl\.CERT_NONE\b/.test(trimmed)) {
25304
+ out2.push({
25305
+ id: `tls-verify-disabled-${file}-${lineNumber}`,
25306
+ pass: "language-sources",
25307
+ category: "security",
25308
+ rule_id: "tls-verify-disabled",
25309
+ cwe: "CWE-295",
25310
+ severity: "high",
25311
+ level: "error",
25312
+ message: "TLS certificate verification disabled: ssl context verify_mode set to CERT_NONE",
25313
+ file,
25314
+ line: lineNumber,
25315
+ snippet: trimmed.substring(0, 100)
25316
+ });
25317
+ } else if (/\bcheck_hostname\s*=\s*False\b/.test(trimmed)) {
25318
+ out2.push({
25319
+ id: `tls-verify-disabled-${file}-${lineNumber}`,
25320
+ pass: "language-sources",
25321
+ category: "security",
25322
+ rule_id: "tls-verify-disabled",
25323
+ cwe: "CWE-295",
25324
+ severity: "high",
25325
+ level: "error",
25326
+ message: "TLS hostname verification disabled: ssl context check_hostname set to False",
25327
+ file,
25328
+ line: lineNumber,
25329
+ snippet: trimmed.substring(0, 100)
25330
+ });
25331
+ }
25332
+ }
25333
+ const restrictiveXfo = xfoHits.find(
25334
+ (h) => /['"](?:DENY|SAMEORIGIN)['"]/i.test(h.value)
25335
+ );
25336
+ const permissiveCsp = cspHits.find((h) => {
25337
+ const pm = h.value.match(/['"]([^'"]+)['"]/);
25338
+ if (!pm) return false;
25339
+ const policy = pm[1];
25340
+ const faMatch = policy.match(/frame-ancestors\s+([^;]+)/i);
25341
+ if (!faMatch) return false;
25342
+ const directive = faMatch[1].trim();
25343
+ return /(^|\s)\*(\s|$)/.test(directive) || /\bhttps?:\/\//i.test(directive);
25344
+ });
25345
+ if (restrictiveXfo && permissiveCsp) {
25346
+ out2.push({
25347
+ id: `xfo-csp-mismatch-${file}-${restrictiveXfo.line}`,
25348
+ pass: "language-sources",
25349
+ category: "security",
25350
+ rule_id: "xfo-csp-mismatch",
25351
+ cwe: "CWE-1021",
25352
+ severity: "medium",
25353
+ level: "warning",
25354
+ message: "X-Frame-Options/CSP frame-ancestors mismatch: XFO restricts framing but CSP frame-ancestors is permissive (CSP overrides on modern browsers)",
25355
+ file,
25356
+ line: restrictiveXfo.line,
25357
+ snippet: lines[restrictiveXfo.line - 1].trim().substring(0, 100)
25358
+ });
25359
+ }
25360
+ return out2;
25361
+ }
25362
+ function findRustPatternFindings(code, file) {
25363
+ const out2 = [];
25364
+ const lines = code.split("\n");
25365
+ const re = /\.\s*(danger_accept_invalid_certs|danger_accept_invalid_hostnames)\s*\(\s*true\s*\)/;
25366
+ for (let i2 = 0; i2 < lines.length; i2++) {
25367
+ const raw = lines[i2];
25368
+ const trimmed = raw.trim();
25369
+ if (!trimmed || trimmed.startsWith("//")) continue;
25370
+ const m = trimmed.match(re);
25371
+ if (!m) continue;
25372
+ const method = m[1];
25373
+ const what = method === "danger_accept_invalid_certs" ? "certificate" : "hostname";
25374
+ out2.push({
25375
+ id: `tls-verify-disabled-${file}-${i2 + 1}`,
25376
+ pass: "language-sources",
25377
+ category: "security",
25378
+ rule_id: "tls-verify-disabled",
25379
+ cwe: "CWE-295",
25380
+ severity: "high",
25381
+ level: "error",
25382
+ message: `TLS ${what} verification disabled: reqwest builder ${method}(true)`,
25383
+ file,
25384
+ line: i2 + 1,
25385
+ snippet: trimmed.substring(0, 100)
25386
+ });
25387
+ }
25388
+ return out2;
25389
+ }
24697
25390
 
24698
25391
  // src/analysis/passes/sink-filter-pass.ts
24699
25392
  var JS_XSS_SANITIZERS = [
@@ -35512,6 +36205,10 @@ async function analyzeMarkupFile(code, filePath, options, language) {
35512
36205
  }
35513
36206
  }
35514
36207
  const attributeFindings = runHtmlAttributeSecurityChecks(tree.rootNode, filePath);
36208
+ if (language === "vue") {
36209
+ const vueXss = runVueTemplateXssChecks(tree.rootNode, filePath, scriptResults);
36210
+ if (vueXss.length > 0) attributeFindings.push(...vueXss);
36211
+ }
35515
36212
  const result = mergeHtmlResults(meta, scriptResults, attributeFindings);
35516
36213
  result.parse_status = htmlParseStatus;
35517
36214
  logger.debug("HTML analysis complete", {