circle-ir 3.129.0 → 3.131.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
  for (const finding of goMisconfigFindings) {
23655
23655
  ctx.addFinding(finding);
23656
23656
  }
23657
+ for (const finding of findGoXssFindings(code, graph.ir.meta.file)) {
23658
+ ctx.addFinding(finding);
23659
+ }
23657
23660
  }
23658
23661
  if (language === "python") {
23659
23662
  additionalSanitizers.push(...findPythonNetlocAllowlistGuardSanitizers(code));
@@ -23666,6 +23669,12 @@ var LanguageSourcesPass = class {
23666
23669
  for (const finding of pyMisconfigFindings) {
23667
23670
  ctx.addFinding(finding);
23668
23671
  }
23672
+ for (const finding of findPythonFlaskStringConcatXssFindings(code, graph.ir.meta.file)) {
23673
+ ctx.addFinding(finding);
23674
+ }
23675
+ for (const finding of findPythonJinjaMarkupXssFindings(code, graph.ir.meta.file)) {
23676
+ ctx.addFinding(finding);
23677
+ }
23669
23678
  }
23670
23679
  if (language === "rust") {
23671
23680
  additionalSanitizers.push(...findRustSetAllowlistGuardSanitizers(code));
@@ -23694,9 +23703,17 @@ var LanguageSourcesPass = class {
23694
23703
  additionalSanitizers.push(...findJsCsvFormulaPrefixSanitizers(code));
23695
23704
  additionalSanitizers.push(...findJsWrapperFunctionSanitizers(code));
23696
23705
  additionalSanitizers.push(...findJsSsrfAllowlistGuardSanitizers(code));
23706
+ additionalSanitizers.push(...findJsArgvFormExecSanitizers(code));
23707
+ additionalSanitizers.push(...findJsParameterizedSqlSanitizers(code));
23697
23708
  for (const finding of findJsPatternFindings(code, graph.ir.meta.file)) {
23698
23709
  ctx.addFinding(finding);
23699
23710
  }
23711
+ for (const finding of findJsVueVHtmlXssFindings(code, graph.ir.meta.file)) {
23712
+ ctx.addFinding(finding);
23713
+ }
23714
+ for (const finding of findTsAngularBypassXssFindings(code, graph.ir.meta.file)) {
23715
+ ctx.addFinding(finding);
23716
+ }
23700
23717
  }
23701
23718
  if (language === "java") {
23702
23719
  additionalSanitizers.push(...findJavaSafeJsonParseSanitizers(code));
@@ -23708,6 +23725,9 @@ var LanguageSourcesPass = class {
23708
23725
  for (const finding of findJavaPatternFindings(code, graph.ir.meta.file)) {
23709
23726
  ctx.addFinding(finding);
23710
23727
  }
23728
+ for (const finding of findJavaResponseWriterXssFindings(code, graph.ir.meta.file)) {
23729
+ ctx.addFinding(finding);
23730
+ }
23711
23731
  }
23712
23732
  if (language === "python" || language === "javascript" || language === "typescript" || language === "go") {
23713
23733
  const exfilFindings = findExternalSecretExfiltrationFindings(
@@ -25454,6 +25474,55 @@ function findJsSsrfAllowlistGuardSanitizers(code) {
25454
25474
  }
25455
25475
  return sanitizers;
25456
25476
  }
25477
+ function findJsArgvFormExecSanitizers(code) {
25478
+ const sanitizers = [];
25479
+ const lines = code.split("\n");
25480
+ const argvExecRe = /\b(?:execFile|spawn)(?:Sync)?\s*\(\s*(?:'[^']*'|"[^"]*"|`[^`]*`)\s*,\s*\[/;
25481
+ const shellArgvRe = /\b(?:execFile|spawn)(?:Sync)?\s*\(\s*['"`](?:[\w./-]*\/)?(?:sh|bash|zsh|ksh|dash|cmd(?:\.exe)?|powershell|pwsh)['"`]\s*,\s*\[\s*['"`]-c['"`]/;
25482
+ for (let i2 = 0; i2 < lines.length; i2++) {
25483
+ const text = lines[i2];
25484
+ if (!argvExecRe.test(text)) continue;
25485
+ if (shellArgvRe.test(text)) continue;
25486
+ sanitizers.push({
25487
+ type: "js_argv_form_exec",
25488
+ method: "execFile",
25489
+ line: i2 + 1,
25490
+ sanitizes: ["command_injection", "external_taint_escape"]
25491
+ });
25492
+ }
25493
+ return sanitizers;
25494
+ }
25495
+ function findJsParameterizedSqlSanitizers(code) {
25496
+ const sanitizers = [];
25497
+ const lines = code.split("\n");
25498
+ const singleRe = /\.\s*(?:query|execute)\s*\(\s*'([^'\\]*(?:\\.[^'\\]*)*)'\s*,\s*\[/;
25499
+ const doubleRe = /\.\s*(?:query|execute)\s*\(\s*"([^"\\]*(?:\\.[^"\\]*)*)"\s*,\s*\[/;
25500
+ const tickRe = /\.\s*(?:query|execute)\s*\(\s*`([^`]*)`\s*,\s*\[/;
25501
+ const placeholderRe = /(?:\$\d+|\?)/;
25502
+ for (let i2 = 0; i2 < lines.length; i2++) {
25503
+ const text = lines[i2];
25504
+ let sql = null;
25505
+ const s = singleRe.exec(text);
25506
+ if (s) sql = s[1];
25507
+ if (sql == null) {
25508
+ const d = doubleRe.exec(text);
25509
+ if (d) sql = d[1];
25510
+ }
25511
+ if (sql == null) {
25512
+ const t = tickRe.exec(text);
25513
+ if (t && !/\$\{/.test(t[1])) sql = t[1];
25514
+ }
25515
+ if (sql == null) continue;
25516
+ if (!placeholderRe.test(sql)) continue;
25517
+ sanitizers.push({
25518
+ type: "js_parameterized_sql",
25519
+ method: "query",
25520
+ line: i2 + 1,
25521
+ sanitizes: ["sql_injection", "external_taint_escape"]
25522
+ });
25523
+ }
25524
+ return sanitizers;
25525
+ }
25457
25526
  function findJavaPathNormalizeStartsWithGuardSanitizers(code) {
25458
25527
  const sanitizers = [];
25459
25528
  const lines = code.split("\n");
@@ -26212,6 +26281,341 @@ function findJsPatternFindings(code, file) {
26212
26281
  }
26213
26282
  return out2;
26214
26283
  }
26284
+ function findGoXssFindings(code, file) {
26285
+ const out2 = [];
26286
+ const lines = code.split("\n");
26287
+ const rwNames = /* @__PURE__ */ new Set();
26288
+ const sigRe = /\(\s*([A-Za-z_]\w*)\s+http\.ResponseWriter\b/g;
26289
+ for (const line of lines) {
26290
+ let m;
26291
+ sigRe.lastIndex = 0;
26292
+ while ((m = sigRe.exec(line)) !== null) rwNames.add(m[1]);
26293
+ }
26294
+ if (rwNames.size === 0) return out2;
26295
+ const escaperRe = /\b(?:html|template)\.(?:EscapeString|HTMLEscapeString|HTMLEscaper|JSEscapeString|URLQueryEscaper)\s*\(/;
26296
+ for (let i2 = 0; i2 < lines.length; i2++) {
26297
+ const raw = lines[i2];
26298
+ const trimmed = raw.trim();
26299
+ if (!trimmed || trimmed.startsWith("//")) continue;
26300
+ const callRe = /\bfmt\.Fprint(?:f|ln)?\s*\(\s*([A-Za-z_]\w*)\s*,\s*([\s\S]*)\)\s*(?:\/\/.*)?$/;
26301
+ const m = trimmed.match(callRe);
26302
+ if (!m) continue;
26303
+ if (!rwNames.has(m[1])) continue;
26304
+ const tail = m[2];
26305
+ if (escaperRe.test(tail)) continue;
26306
+ if (!/[A-Za-z_]\w*/.test(tail.replace(/"[^"]*"|`[^`]*`/g, ""))) continue;
26307
+ out2.push({
26308
+ id: `xss-${file}-${i2 + 1}`,
26309
+ pass: "language-sources",
26310
+ category: "security",
26311
+ rule_id: "xss",
26312
+ cwe: "CWE-79",
26313
+ severity: "high",
26314
+ level: "error",
26315
+ message: "Reflected XSS: fmt.Fprint writes data to http.ResponseWriter without HTML escaping. Wrap user-controlled args with html.EscapeString / template.HTMLEscapeString.",
26316
+ file,
26317
+ line: i2 + 1,
26318
+ snippet: trimmed.substring(0, 100)
26319
+ });
26320
+ }
26321
+ return out2;
26322
+ }
26323
+ function findJavaResponseWriterXssFindings(code, file) {
26324
+ const out2 = [];
26325
+ const lines = code.split("\n");
26326
+ const respNames = /* @__PURE__ */ new Set();
26327
+ const sigRe = /\bHttpServletResponse\s+([A-Za-z_]\w*)\b/g;
26328
+ for (const line of lines) {
26329
+ let m;
26330
+ sigRe.lastIndex = 0;
26331
+ while ((m = sigRe.exec(line)) !== null) respNames.add(m[1]);
26332
+ }
26333
+ if (respNames.size === 0) return out2;
26334
+ const safeWrapRe = /\b(?:Encode\.(?:forHtml|forHtmlAttribute|forHtmlContent|forJavaScript)|StringEscapeUtils\.escape(?:Html3|Html4|EcmaScript|Xml)|HtmlUtils\.htmlEscape(?:Decimal|Hex)?|Escaper\.escapeHtml|HtmlEscapers\.(?:escapeHtml|htmlEscaper)|Encoder\.encodeForHTML(?:Attribute)?|Jsoup\.clean)\s*\(/;
26335
+ for (let i2 = 0; i2 < lines.length; i2++) {
26336
+ const raw = lines[i2];
26337
+ const trimmed = raw.trim();
26338
+ if (!trimmed || trimmed.startsWith("//") || trimmed.startsWith("*")) continue;
26339
+ const chainRe = /\b([A-Za-z_]\w*)\s*\.\s*getWriter\s*\(\s*\)\s*\.\s*(print|println|write|printf|format|append)\s*\(([\s\S]*)\)\s*;?\s*(?:\/\/.*)?$/;
26340
+ const m = trimmed.match(chainRe);
26341
+ if (!m) continue;
26342
+ const recv = m[1];
26343
+ if (!respNames.has(recv)) continue;
26344
+ const args2 = m[3];
26345
+ if (safeWrapRe.test(args2)) continue;
26346
+ const argsTrim = args2.trim();
26347
+ if (/^"(?:\\.|[^"\\])*"$/.test(argsTrim)) continue;
26348
+ if (!/[A-Za-z_]\w*/.test(argsTrim)) continue;
26349
+ out2.push({
26350
+ id: `xss-${file}-${i2 + 1}`,
26351
+ pass: "language-sources",
26352
+ category: "security",
26353
+ rule_id: "xss",
26354
+ cwe: "CWE-79",
26355
+ severity: "high",
26356
+ level: "error",
26357
+ message: "Reflected XSS: HttpServletResponse.getWriter()." + m[2] + "(...) writes data to the response without HTML escaping. Wrap user-controlled values with OWASP Encode.forHtml / StringEscapeUtils.escapeHtml4 / Jsoup.clean.",
26358
+ file,
26359
+ line: i2 + 1,
26360
+ snippet: trimmed.substring(0, 100)
26361
+ });
26362
+ }
26363
+ return out2;
26364
+ }
26365
+ function findJsVueVHtmlXssFindings(code, file) {
26366
+ const out2 = [];
26367
+ const lines = code.split("\n");
26368
+ const sourceRe = /\b(?:URLSearchParams|location\s*\.\s*(?:search|hash|href|pathname)|window\s*\.\s*location|route\s*\.\s*(?:query|params)|router\s*\.\s*(?:currentRoute|query)|\$route\s*\.\s*(?:query|params)|fetch\s*\(|axios\s*\.\s*(?:get|post)|XMLHttpRequest|document\s*\.\s*location)\b/;
26369
+ const tplRe = /\btemplate\s*:\s*(['"`])([\s\S]*?)\1/g;
26370
+ let tm;
26371
+ const boundVars = /* @__PURE__ */ new Set();
26372
+ while ((tm = tplRe.exec(code)) !== null) {
26373
+ const tpl = tm[2];
26374
+ const vhRe = /\bv-html\s*=\s*"([^"]+)"/g;
26375
+ let vm;
26376
+ while ((vm = vhRe.exec(tpl)) !== null) {
26377
+ const expr = vm[1].trim();
26378
+ const idMatch = expr.match(/^([A-Za-z_$][\w$]*)/);
26379
+ if (idMatch) boundVars.add(idMatch[1]);
26380
+ }
26381
+ }
26382
+ if (boundVars.size === 0) return out2;
26383
+ const taintedSet = /* @__PURE__ */ new Set();
26384
+ const assignRe = /\b(?:const|let|var)\s+([A-Za-z_$][\w$]*)\s*=\s*([^;\n]+)/g;
26385
+ let am;
26386
+ assignRe.lastIndex = 0;
26387
+ while ((am = assignRe.exec(code)) !== null) {
26388
+ if (sourceRe.test(am[2])) taintedSet.add(am[1]);
26389
+ }
26390
+ for (let pass = 0; pass < 3; pass++) {
26391
+ assignRe.lastIndex = 0;
26392
+ const before = taintedSet.size;
26393
+ while ((am = assignRe.exec(code)) !== null) {
26394
+ if (taintedSet.has(am[1])) continue;
26395
+ for (const tv of taintedSet) {
26396
+ if (new RegExp(`\\b${tv}\\b`).test(am[2])) {
26397
+ taintedSet.add(am[1]);
26398
+ break;
26399
+ }
26400
+ }
26401
+ }
26402
+ if (taintedSet.size === before) break;
26403
+ }
26404
+ const taintedBindings = /* @__PURE__ */ new Set();
26405
+ for (const v of boundVars) {
26406
+ const bindRe = new RegExp(`\\b${v}\\s*[:=]\\s*([^,;\\n]+)`);
26407
+ for (const line of lines) {
26408
+ const m = line.match(bindRe);
26409
+ if (!m) continue;
26410
+ const val = m[1];
26411
+ if (sourceRe.test(val)) {
26412
+ taintedBindings.add(v);
26413
+ break;
26414
+ }
26415
+ let found = false;
26416
+ for (const tv of taintedSet) {
26417
+ if (new RegExp(`\\b${tv}\\b`).test(val)) {
26418
+ taintedBindings.add(v);
26419
+ found = true;
26420
+ break;
26421
+ }
26422
+ }
26423
+ if (found) break;
26424
+ }
26425
+ const propsRe = new RegExp(
26426
+ `\\bprops\\s*:\\s*\\[[^\\]]*['"\`]${v}['"\`][^\\]]*\\]`
26427
+ );
26428
+ if (propsRe.test(code)) taintedBindings.add(v);
26429
+ }
26430
+ if (taintedBindings.size === 0) return out2;
26431
+ for (let i2 = 0; i2 < lines.length; i2++) {
26432
+ const raw = lines[i2];
26433
+ if (!/v-html\s*=/.test(raw)) continue;
26434
+ const vm = raw.match(/v-html\s*=\s*"([^"]+)"/);
26435
+ if (!vm) continue;
26436
+ const idMatch = vm[1].trim().match(/^([A-Za-z_$][\w$]*)/);
26437
+ if (!idMatch || !taintedBindings.has(idMatch[1])) continue;
26438
+ out2.push({
26439
+ id: `xss-${file}-${i2 + 1}`,
26440
+ pass: "language-sources",
26441
+ category: "security",
26442
+ rule_id: "xss",
26443
+ cwe: "CWE-79",
26444
+ severity: "high",
26445
+ level: "error",
26446
+ message: 'Vue v-html XSS: directive binds to "' + idMatch[1] + '" which is sourced from user-controlled input. Use {{ }} interpolation (auto-escapes) or sanitize the HTML with DOMPurify before binding.',
26447
+ file,
26448
+ line: i2 + 1,
26449
+ snippet: raw.trim().substring(0, 100)
26450
+ });
26451
+ }
26452
+ return out2;
26453
+ }
26454
+ function findTsAngularBypassXssFindings(code, file) {
26455
+ const out2 = [];
26456
+ const lines = code.split("\n");
26457
+ const sanitizerNames = /* @__PURE__ */ new Set();
26458
+ const declRe = /\b(?:public|private|protected|readonly|\s)?\s*([A-Za-z_$][\w$]*)\s*:\s*DomSanitizer\b/g;
26459
+ let m;
26460
+ declRe.lastIndex = 0;
26461
+ while ((m = declRe.exec(code)) !== null) sanitizerNames.add(m[1]);
26462
+ if (sanitizerNames.size === 0) return out2;
26463
+ const assignRe = /\bthis\s*\.\s*([A-Za-z_$][\w$]*)\s*=\s*([A-Za-z_$][\w$]*)\b/g;
26464
+ let am;
26465
+ assignRe.lastIndex = 0;
26466
+ while ((am = assignRe.exec(code)) !== null) {
26467
+ if (sanitizerNames.has(am[2])) sanitizerNames.add(am[1]);
26468
+ }
26469
+ for (let i2 = 0; i2 < lines.length; i2++) {
26470
+ const raw = lines[i2];
26471
+ const trimmed = raw.trim();
26472
+ if (!trimmed || trimmed.startsWith("//") || trimmed.startsWith("*")) continue;
26473
+ const callRe = /\b(?:this\s*\.\s*)?([A-Za-z_$][\w$]*)\s*\.\s*bypassSecurityTrust(Html|Script|Url|ResourceUrl|Style)\s*\(([\s\S]*?)\)/;
26474
+ const cm = trimmed.match(callRe);
26475
+ if (!cm) continue;
26476
+ const recv = cm[1];
26477
+ if (!sanitizerNames.has(recv)) continue;
26478
+ const arg = cm[3].trim();
26479
+ if (/^(['"`])[^'"`]*\1$/.test(arg)) continue;
26480
+ out2.push({
26481
+ id: `xss-${file}-${i2 + 1}`,
26482
+ pass: "language-sources",
26483
+ category: "security",
26484
+ rule_id: "xss",
26485
+ cwe: "CWE-79",
26486
+ severity: "high",
26487
+ level: "error",
26488
+ message: "Angular XSS: DomSanitizer.bypassSecurityTrust" + cm[2] + "() opts out of Angular's built-in sanitization for a non-literal argument. Prefer leaving Angular's default sanitizer in place, or sanitize the input with DOMPurify before bypassing.",
26489
+ file,
26490
+ line: i2 + 1,
26491
+ snippet: trimmed.substring(0, 100)
26492
+ });
26493
+ }
26494
+ return out2;
26495
+ }
26496
+ function findPythonFlaskStringConcatXssFindings(code, file) {
26497
+ const out2 = [];
26498
+ const lines = code.split("\n");
26499
+ const fns = [];
26500
+ const defRe = /^(\s*)def\s+([A-Za-z_]\w*)\s*\(/;
26501
+ for (let i2 = 0; i2 < lines.length; i2++) {
26502
+ const raw = lines[i2];
26503
+ const dm = raw.match(defRe);
26504
+ if (!dm) continue;
26505
+ const indent = dm[1].length;
26506
+ let hasRoute = false;
26507
+ for (let j = i2 - 1; j >= 0; j--) {
26508
+ const prev = lines[j].trim();
26509
+ if (prev === "" || prev.startsWith("#")) continue;
26510
+ if (!prev.startsWith("@")) break;
26511
+ if (/@\s*[A-Za-z_]\w*\s*\.\s*route\s*\(/.test(prev) || /@\s*route\s*\(/.test(prev)) {
26512
+ hasRoute = true;
26513
+ }
26514
+ }
26515
+ if (!hasRoute) continue;
26516
+ let end = lines.length;
26517
+ for (let k = i2 + 1; k < lines.length; k++) {
26518
+ const ln = lines[k];
26519
+ if (!ln.trim()) continue;
26520
+ const ind = ln.match(/^(\s*)/)?.[1].length ?? 0;
26521
+ if (ind <= indent) {
26522
+ end = k;
26523
+ break;
26524
+ }
26525
+ }
26526
+ fns.push({ name: dm[2], startLine: i2 + 1, endLine: end });
26527
+ }
26528
+ if (fns.length === 0) return out2;
26529
+ const requestSourceRe = /\b([A-Za-z_]\w*)\s*=\s*request\s*\.\s*(?:args|form|values|files|json|cookies|headers)(?:\s*\.\s*get\s*\(|\s*\[)/;
26530
+ const escapeWrapRe = /\b(?:html\.escape|markupsafe\.escape|escape|markupsafe\.Markup\s*\.\s*escape|werkzeug\.utils\.escape)\s*\(/;
26531
+ for (const fn of fns) {
26532
+ const taintedVars = /* @__PURE__ */ new Set();
26533
+ for (let li = fn.startLine; li < fn.endLine; li++) {
26534
+ const raw = lines[li];
26535
+ const rm = raw.match(requestSourceRe);
26536
+ if (rm) taintedVars.add(rm[1]);
26537
+ }
26538
+ for (let li = fn.startLine; li < fn.endLine; li++) {
26539
+ const raw = lines[li];
26540
+ const trimmed = raw.trim();
26541
+ if (!trimmed || trimmed.startsWith("#")) continue;
26542
+ const retMatch = trimmed.match(/^(?:return|yield)\s+(.+)$/);
26543
+ if (!retMatch) continue;
26544
+ const expr = retMatch[1];
26545
+ const inlineSource = /\brequest\s*\.\s*(?:args|form|values|files|cookies|headers)(?:\s*\.\s*get\s*\(|\s*\[)/.test(
26546
+ expr
26547
+ );
26548
+ const hasTaintedVar = [...taintedVars].some(
26549
+ (v) => new RegExp(`\\b${v}\\b`).test(expr)
26550
+ );
26551
+ if (!hasTaintedVar && !inlineSource) continue;
26552
+ if (escapeWrapRe.test(expr)) continue;
26553
+ const buildsHtml = /['"`]\s*\+/.test(expr) || /\+\s*['"`]/.test(expr) || // f-string with interpolation. Allow any non-`{` char between the
26554
+ // opening quote and the first `{` (Python f-strings may embed `"`
26555
+ // when single-quoted and vice-versa, e.g. f'<a href="{u}">').
26556
+ /\bf['"`][^{\n]*\{[^}]+\}/.test(expr) || /['"`]\s*%\s*[^=]/.test(expr) || /['"`]\.\s*format\s*\(/.test(expr);
26557
+ if (!buildsHtml) continue;
26558
+ out2.push({
26559
+ id: `xss-${file}-${li + 1}`,
26560
+ pass: "language-sources",
26561
+ category: "security",
26562
+ rule_id: "xss",
26563
+ cwe: "CWE-79",
26564
+ severity: "high",
26565
+ level: "error",
26566
+ message: "Reflected XSS: Flask route returns HTML built from request input via string concatenation / f-string / format. Wrap user input with markupsafe.escape() or render via a Jinja2 template (autoescape on by default).",
26567
+ file,
26568
+ line: li + 1,
26569
+ snippet: trimmed.substring(0, 100)
26570
+ });
26571
+ break;
26572
+ }
26573
+ }
26574
+ return out2;
26575
+ }
26576
+ function findPythonJinjaMarkupXssFindings(code, file) {
26577
+ const out2 = [];
26578
+ const lines = code.split("\n");
26579
+ const importRe = /^\s*from\s+(?:markupsafe|flask)\s+import\s+(?:[^#\n]*\b)?Markup\b/m;
26580
+ const classDefRe = /^\s*class\s+Markup\b/m;
26581
+ if (!importRe.test(code)) return out2;
26582
+ if (classDefRe.test(code)) return out2;
26583
+ const requestSourceRe = /\b([A-Za-z_]\w*)\s*=\s*request\s*\.\s*(?:args|form|values|files|json|cookies|headers)(?:\s*\.\s*get\s*\(|\s*\[)/;
26584
+ const taintedVars = /* @__PURE__ */ new Set();
26585
+ for (const line of lines) {
26586
+ const m = line.match(requestSourceRe);
26587
+ if (m) taintedVars.add(m[1]);
26588
+ }
26589
+ if (taintedVars.size === 0) return out2;
26590
+ if (!/\.\s*render\s*\(/.test(code)) return out2;
26591
+ const markupCallRe = /\bMarkup\s*\(\s*([A-Za-z_]\w*)\s*[,)]/g;
26592
+ for (let i2 = 0; i2 < lines.length; i2++) {
26593
+ const raw = lines[i2];
26594
+ const trimmed = raw.trim();
26595
+ if (!trimmed || trimmed.startsWith("#")) continue;
26596
+ let mm;
26597
+ markupCallRe.lastIndex = 0;
26598
+ while ((mm = markupCallRe.exec(trimmed)) !== null) {
26599
+ const argName = mm[1];
26600
+ if (!taintedVars.has(argName)) continue;
26601
+ out2.push({
26602
+ id: `xss-${file}-${i2 + 1}`,
26603
+ pass: "language-sources",
26604
+ category: "security",
26605
+ rule_id: "xss",
26606
+ cwe: "CWE-79",
26607
+ severity: "high",
26608
+ level: "error",
26609
+ message: "Jinja2 autoescape bypass: Markup(" + argName + ") wraps a request-derived value, disabling autoescape when rendered. Pass the raw value to render() and let Jinja2 auto-escape, or sanitize with bleach.clean first.",
26610
+ file,
26611
+ line: i2 + 1,
26612
+ snippet: trimmed.substring(0, 100)
26613
+ });
26614
+ break;
26615
+ }
26616
+ }
26617
+ return out2;
26618
+ }
26215
26619
 
26216
26620
  // src/analysis/passes/sink-filter-pass.ts
26217
26621
  var JS_XSS_SANITIZERS = [
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "circle-ir",
3
- "version": "3.129.0",
3
+ "version": "3.131.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",