circle-ir 3.128.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.
@@ -23650,6 +23650,13 @@ var LanguageSourcesPass = class {
23650
23650
  if (language === "go") {
23651
23651
  additionalSanitizers.push(...findGoMapAllowlistGuardSanitizers(code));
23652
23652
  additionalSanitizers.push(...findGoHtmlTemplateImportSanitizers(code));
23653
+ const goMisconfigFindings = findGoPatternFindings(code, graph.ir.meta.file);
23654
+ for (const finding of goMisconfigFindings) {
23655
+ ctx.addFinding(finding);
23656
+ }
23657
+ for (const finding of findGoXssFindings(code, graph.ir.meta.file)) {
23658
+ ctx.addFinding(finding);
23659
+ }
23653
23660
  }
23654
23661
  if (language === "python") {
23655
23662
  additionalSanitizers.push(...findPythonNetlocAllowlistGuardSanitizers(code));
@@ -23662,6 +23669,12 @@ var LanguageSourcesPass = class {
23662
23669
  for (const finding of pyMisconfigFindings) {
23663
23670
  ctx.addFinding(finding);
23664
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
+ }
23665
23678
  }
23666
23679
  if (language === "rust") {
23667
23680
  additionalSanitizers.push(...findRustSetAllowlistGuardSanitizers(code));
@@ -23671,6 +23684,18 @@ var LanguageSourcesPass = class {
23671
23684
  for (const finding of rustMisconfigFindings) {
23672
23685
  ctx.addFinding(finding);
23673
23686
  }
23687
+ for (const finding of findRustHardcodedCredentialFindings(code, graph.ir.meta.file)) {
23688
+ ctx.addFinding(finding);
23689
+ }
23690
+ for (const finding of findRustInsecureCookieFindings(code, graph.ir.meta.file)) {
23691
+ ctx.addFinding(finding);
23692
+ }
23693
+ for (const finding of findRustJwtVerifyDisabledFindings(code, graph.ir.meta.file)) {
23694
+ ctx.addFinding(finding);
23695
+ }
23696
+ for (const finding of findRustWeakCryptoEcbFindings(code, graph.ir.meta.file)) {
23697
+ ctx.addFinding(finding);
23698
+ }
23674
23699
  }
23675
23700
  if (language === "javascript" || language === "typescript" || language === "htmljs") {
23676
23701
  additionalSanitizers.push(...findJsSafeJsonParseSanitizers(code));
@@ -23678,6 +23703,17 @@ var LanguageSourcesPass = class {
23678
23703
  additionalSanitizers.push(...findJsCsvFormulaPrefixSanitizers(code));
23679
23704
  additionalSanitizers.push(...findJsWrapperFunctionSanitizers(code));
23680
23705
  additionalSanitizers.push(...findJsSsrfAllowlistGuardSanitizers(code));
23706
+ additionalSanitizers.push(...findJsArgvFormExecSanitizers(code));
23707
+ additionalSanitizers.push(...findJsParameterizedSqlSanitizers(code));
23708
+ for (const finding of findJsPatternFindings(code, graph.ir.meta.file)) {
23709
+ ctx.addFinding(finding);
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
+ }
23681
23717
  }
23682
23718
  if (language === "java") {
23683
23719
  additionalSanitizers.push(...findJavaSafeJsonParseSanitizers(code));
@@ -23686,6 +23722,12 @@ var LanguageSourcesPass = class {
23686
23722
  );
23687
23723
  additionalSanitizers.push(...findJavaInlineCrlfStripLogSanitizers(code));
23688
23724
  additionalSanitizers.push(...findJavaArgvFormExecSanitizers(code));
23725
+ for (const finding of findJavaPatternFindings(code, graph.ir.meta.file)) {
23726
+ ctx.addFinding(finding);
23727
+ }
23728
+ for (const finding of findJavaResponseWriterXssFindings(code, graph.ir.meta.file)) {
23729
+ ctx.addFinding(finding);
23730
+ }
23689
23731
  }
23690
23732
  if (language === "python" || language === "javascript" || language === "typescript" || language === "go") {
23691
23733
  const exfilFindings = findExternalSecretExfiltrationFindings(
@@ -25432,6 +25474,55 @@ function findJsSsrfAllowlistGuardSanitizers(code) {
25432
25474
  }
25433
25475
  return sanitizers;
25434
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
+ }
25435
25526
  function findJavaPathNormalizeStartsWithGuardSanitizers(code) {
25436
25527
  const sanitizers = [];
25437
25528
  const lines = code.split("\n");
@@ -25944,6 +26035,587 @@ function findRustPatternFindings(code, file) {
25944
26035
  }
25945
26036
  return out2;
25946
26037
  }
26038
+ function findRustHardcodedCredentialFindings(code, file) {
26039
+ const out2 = [];
26040
+ const lines = code.split("\n");
26041
+ const re = /\b(?:pub\s+)?(?:const|static)\s+([A-Z][A-Z0-9_]*)\s*:\s*&\s*'?[a-z_]*\s*str\s*=\s*"([^"]+)"/;
26042
+ const nameRe = /(?:^|_)(?:API[_]?KEY|SECRET|TOKEN|PASSWORD|PASSWD|PWD|AUTH)(?:_|$)/i;
26043
+ for (let i2 = 0; i2 < lines.length; i2++) {
26044
+ const raw = lines[i2];
26045
+ const trimmed = raw.trim();
26046
+ if (!trimmed || trimmed.startsWith("//")) continue;
26047
+ const m = trimmed.match(re);
26048
+ if (!m) continue;
26049
+ const name2 = m[1];
26050
+ const value = m[2];
26051
+ if (!nameRe.test(name2)) continue;
26052
+ if (value.length < 8) continue;
26053
+ if (/^(?:xxx|todo|fixme|placeholder|changeme)/i.test(value)) continue;
26054
+ out2.push({
26055
+ id: `hardcoded-credential-${file}-${i2 + 1}`,
26056
+ pass: "language-sources",
26057
+ category: "security",
26058
+ rule_id: "hardcoded-credential",
26059
+ cwe: "CWE-798",
26060
+ severity: "high",
26061
+ level: "error",
26062
+ message: `Hardcoded credential: const ${name2} contains a literal secret value`,
26063
+ file,
26064
+ line: i2 + 1,
26065
+ snippet: trimmed.substring(0, 100)
26066
+ });
26067
+ }
26068
+ return out2;
26069
+ }
26070
+ function findRustInsecureCookieFindings(code, file) {
26071
+ const out2 = [];
26072
+ const lines = code.split("\n");
26073
+ const builderRe = /\bCookie\s*::\s*build\s*\(/;
26074
+ const insecureFlagRe = /\.\s*(?:secure|http_only)\s*\(\s*false\s*\)/;
26075
+ for (let i2 = 0; i2 < lines.length; i2++) {
26076
+ const raw = lines[i2];
26077
+ const trimmed = raw.trim();
26078
+ if (!trimmed || trimmed.startsWith("//")) continue;
26079
+ if (!builderRe.test(trimmed)) continue;
26080
+ if (!insecureFlagRe.test(trimmed)) continue;
26081
+ out2.push({
26082
+ id: `insecure-cookie-${file}-${i2 + 1}`,
26083
+ pass: "language-sources",
26084
+ category: "security",
26085
+ rule_id: "insecure-cookie",
26086
+ cwe: "CWE-1004",
26087
+ severity: "medium",
26088
+ level: "warning",
26089
+ message: "Insecure cookie: Cookie::build chain disables Secure / HttpOnly flag(s)",
26090
+ file,
26091
+ line: i2 + 1,
26092
+ snippet: trimmed.substring(0, 100)
26093
+ });
26094
+ }
26095
+ return out2;
26096
+ }
26097
+ function findRustJwtVerifyDisabledFindings(code, file) {
26098
+ const out2 = [];
26099
+ const lines = code.split("\n");
26100
+ const re = /\.\s*insecure_disable_signature_validation\s*\(/;
26101
+ for (let i2 = 0; i2 < lines.length; i2++) {
26102
+ const raw = lines[i2];
26103
+ const trimmed = raw.trim();
26104
+ if (!trimmed || trimmed.startsWith("//")) continue;
26105
+ if (!re.test(trimmed)) continue;
26106
+ out2.push({
26107
+ id: `jwt-verify-disabled-${file}-${i2 + 1}`,
26108
+ pass: "language-sources",
26109
+ category: "security",
26110
+ rule_id: "jwt-verify-disabled",
26111
+ cwe: "CWE-347",
26112
+ severity: "critical",
26113
+ level: "error",
26114
+ message: "JWT signature verification disabled: Validation::insecure_disable_signature_validation() forfeits signature enforcement",
26115
+ file,
26116
+ line: i2 + 1,
26117
+ snippet: trimmed.substring(0, 100)
26118
+ });
26119
+ }
26120
+ return out2;
26121
+ }
26122
+ function findRustWeakCryptoEcbFindings(code, file) {
26123
+ const out2 = [];
26124
+ const lines = code.split("\n");
26125
+ const ctorRe = /\bAes(?:128|192|256)(?:Ecb)?\s*::\s*new\s*\(/;
26126
+ const blockOpRe = /\.\s*(encrypt_block|decrypt_block)\s*\(/;
26127
+ let sawCtor = false;
26128
+ for (const line of lines) {
26129
+ if (ctorRe.test(line)) {
26130
+ sawCtor = true;
26131
+ break;
26132
+ }
26133
+ }
26134
+ if (!sawCtor) return out2;
26135
+ for (let i2 = 0; i2 < lines.length; i2++) {
26136
+ const raw = lines[i2];
26137
+ const trimmed = raw.trim();
26138
+ if (!trimmed || trimmed.startsWith("//")) continue;
26139
+ if (!blockOpRe.test(trimmed)) continue;
26140
+ out2.push({
26141
+ id: `weak-crypto-${file}-${i2 + 1}`,
26142
+ pass: "language-sources",
26143
+ category: "security",
26144
+ rule_id: "weak-crypto",
26145
+ cwe: "CWE-327",
26146
+ severity: "high",
26147
+ level: "error",
26148
+ message: "Weak crypto (ECB mode): raw Aes::encrypt_block/decrypt_block leaks repeating-block patterns. Use AES-GCM, AES-CTR, or AES-CBC with an HMAC.",
26149
+ file,
26150
+ line: i2 + 1,
26151
+ snippet: trimmed.substring(0, 100)
26152
+ });
26153
+ }
26154
+ return out2;
26155
+ }
26156
+ function findJavaPatternFindings(code, file) {
26157
+ const out2 = [];
26158
+ const lines = code.split("\n");
26159
+ const jwtDecodeRe = /\bJWT\s*\.\s*decode\s*\(/;
26160
+ for (let i2 = 0; i2 < lines.length; i2++) {
26161
+ const raw = lines[i2];
26162
+ const trimmed = raw.trim();
26163
+ if (!trimmed || trimmed.startsWith("//") || trimmed.startsWith("*")) continue;
26164
+ if (!jwtDecodeRe.test(trimmed)) continue;
26165
+ if (/\.\s*verify\s*\(/.test(trimmed)) continue;
26166
+ out2.push({
26167
+ id: `jwt-verify-disabled-${file}-${i2 + 1}-decode`,
26168
+ pass: "language-sources",
26169
+ category: "security",
26170
+ rule_id: "jwt-verify-disabled",
26171
+ cwe: "CWE-347",
26172
+ severity: "critical",
26173
+ level: "error",
26174
+ message: "JWT signature not verified: auth0 `JWT.decode(token)` parses without checking the signature. Use `JWT.require(<algorithm>).build().verify(token)` to enforce verification.",
26175
+ file,
26176
+ line: i2 + 1,
26177
+ snippet: trimmed.substring(0, 100)
26178
+ });
26179
+ }
26180
+ const anonStartRe = /\bnew\s+X509TrustManager\s*\(\s*\)\s*\{/;
26181
+ const checkServerSig = /\bcheckServerTrusted\s*\([^)]*\)\s*(?:throws\s+[^\{]*)?\{\s*\}/;
26182
+ for (let i2 = 0; i2 < lines.length; i2++) {
26183
+ const raw = lines[i2];
26184
+ if (!anonStartRe.test(raw)) continue;
26185
+ const end = Math.min(lines.length, i2 + 16);
26186
+ let foundAt = -1;
26187
+ for (let j = i2; j < end; j++) {
26188
+ if (checkServerSig.test(lines[j])) {
26189
+ foundAt = j;
26190
+ break;
26191
+ }
26192
+ }
26193
+ if (foundAt < 0) continue;
26194
+ out2.push({
26195
+ id: `tls-verify-disabled-${file}-${foundAt + 1}`,
26196
+ pass: "language-sources",
26197
+ category: "security",
26198
+ rule_id: "tls-verify-disabled",
26199
+ cwe: "CWE-295",
26200
+ severity: "high",
26201
+ level: "error",
26202
+ message: "TLS certificate verification disabled: anonymous X509TrustManager with empty checkServerTrusted body accepts every certificate.",
26203
+ file,
26204
+ line: foundAt + 1,
26205
+ snippet: lines[foundAt].trim().substring(0, 100)
26206
+ });
26207
+ }
26208
+ return out2;
26209
+ }
26210
+ function findGoPatternFindings(code, file) {
26211
+ const out2 = [];
26212
+ const lines = code.split("\n");
26213
+ const cipherVars = /* @__PURE__ */ new Set();
26214
+ const ctorRe = /\b([a-zA-Z_]\w*)\s*(?:,\s*[a-zA-Z_]\w*)?\s*:?=\s*aes\.NewCipher\s*\(/;
26215
+ for (const line of lines) {
26216
+ const m = line.match(ctorRe);
26217
+ if (m) cipherVars.add(m[1]);
26218
+ }
26219
+ if (cipherVars.size === 0) return out2;
26220
+ for (const v of Array.from(cipherVars)) {
26221
+ const wrapRe = new RegExp(
26222
+ `\\bcipher\\.New(?:GCM|CBCEncrypter|CBCDecrypter|CTR|OFB|CFBEncrypter|CFBDecrypter)\\s*\\(\\s*${v}\\b`
26223
+ );
26224
+ for (const line of lines) {
26225
+ if (wrapRe.test(line)) {
26226
+ cipherVars.delete(v);
26227
+ break;
26228
+ }
26229
+ }
26230
+ }
26231
+ if (cipherVars.size === 0) return out2;
26232
+ for (let i2 = 0; i2 < lines.length; i2++) {
26233
+ const raw = lines[i2];
26234
+ const trimmed = raw.trim();
26235
+ if (!trimmed || trimmed.startsWith("//")) continue;
26236
+ for (const v of cipherVars) {
26237
+ const opRe = new RegExp(`\\b${v}\\s*\\.\\s*(?:Encrypt|Decrypt)\\s*\\(`);
26238
+ if (!opRe.test(trimmed)) continue;
26239
+ out2.push({
26240
+ id: `weak-crypto-${file}-${i2 + 1}`,
26241
+ pass: "language-sources",
26242
+ category: "security",
26243
+ rule_id: "weak-crypto",
26244
+ cwe: "CWE-327",
26245
+ severity: "high",
26246
+ level: "error",
26247
+ message: "Weak crypto (ECB mode): raw aes.Cipher.Encrypt/Decrypt on a block leaks repeating-block patterns. Wrap with cipher.NewGCM, cipher.NewCTR, or cipher.NewCBCEncrypter + HMAC.",
26248
+ file,
26249
+ line: i2 + 1,
26250
+ snippet: trimmed.substring(0, 100)
26251
+ });
26252
+ break;
26253
+ }
26254
+ }
26255
+ return out2;
26256
+ }
26257
+ function findJsPatternFindings(code, file) {
26258
+ const out2 = [];
26259
+ const lines = code.split("\n");
26260
+ const parseRe = /\blibxml(?:js)?\s*\.\s*parseXml(?:String)?\s*\(/;
26261
+ const noentTrueRe = /\bnoent\s*:\s*true\b/;
26262
+ for (let i2 = 0; i2 < lines.length; i2++) {
26263
+ const raw = lines[i2];
26264
+ const trimmed = raw.trim();
26265
+ if (!trimmed || trimmed.startsWith("//") || trimmed.startsWith("*")) continue;
26266
+ if (!parseRe.test(trimmed)) continue;
26267
+ if (!noentTrueRe.test(trimmed)) continue;
26268
+ out2.push({
26269
+ id: `xml-entity-expansion-${file}-${i2 + 1}`,
26270
+ pass: "language-sources",
26271
+ category: "security",
26272
+ rule_id: "xml-entity-expansion",
26273
+ cwe: "CWE-611",
26274
+ severity: "high",
26275
+ level: "error",
26276
+ message: "XML external entity resolution enabled: libxmljs parseXml called with `noent: true` resolves external entities (XXE / billion-laughs). Omit the flag or set `noent: false`.",
26277
+ file,
26278
+ line: i2 + 1,
26279
+ snippet: trimmed.substring(0, 100)
26280
+ });
26281
+ }
26282
+ return out2;
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
+ }
25947
26619
 
25948
26620
  // src/analysis/passes/sink-filter-pass.ts
25949
26621
  var JS_XSS_SANITIZERS = [