circle-ir 3.137.0 → 3.138.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.
@@ -233,6 +233,16 @@ export class LanguageSourcesPass {
233
233
  for (const finding of findGoLdapInjectionFindings(code, graph.ir.meta.file)) {
234
234
  ctx.addFinding(finding);
235
235
  }
236
+ // Sprint 89 (#189): Go insecure_deserialization — encoding/gob
237
+ // NewDecoder(r.Body).Decode(&v).
238
+ for (const finding of findGoGobDeserializationFindings(code, graph.ir.meta.file)) {
239
+ ctx.addFinding(finding);
240
+ }
241
+ // Sprint 90 (#189): Go XXE — encoding/xml NewDecoder(r.Body) with
242
+ // Strict=false or custom Entity map.
243
+ for (const finding of findGoXmlDecoderXxeFindings(code, graph.ir.meta.file)) {
244
+ ctx.addFinding(finding);
245
+ }
236
246
  }
237
247
  // -- Python: safe-handler sanitizer detectors (cognium-dev #114 Sprint 31) --
238
248
  if (language === 'python') {
@@ -287,6 +297,10 @@ export class LanguageSourcesPass {
287
297
  for (const finding of findPythonHeaderCrlfInjectionFindings(code, graph.ir.meta.file)) {
288
298
  ctx.addFinding(finding);
289
299
  }
300
+ // Sprint 90 (#189): Python SSTI — jinja2.Template(<tainted>).render(...).
301
+ for (const finding of findPythonJinjaTemplateSstiFindings(code, graph.ir.meta.file)) {
302
+ ctx.addFinding(finding);
303
+ }
290
304
  }
291
305
  // -- Rust: safe-handler sanitizer detectors (cognium-dev #115 Sprint 31) --
292
306
  if (language === 'rust') {
@@ -383,6 +397,21 @@ export class LanguageSourcesPass {
383
397
  for (const finding of findJsLdapInjectionFindings(code, graph.ir.meta.file)) {
384
398
  ctx.addFinding(finding);
385
399
  }
400
+ // Sprint 89 (#189): JS insecure_deserialization —
401
+ // JSON.parse(req.body) on raw / text bodies.
402
+ for (const finding of findJsJsonParseBodyFindings(code, graph.ir.meta.file)) {
403
+ ctx.addFinding(finding);
404
+ }
405
+ // Sprint 89 (#189): JS xpath_injection — DOM
406
+ // document.evaluate(<tainted>, ...).
407
+ for (const finding of findJsDomXpathInjectionFindings(code, graph.ir.meta.file)) {
408
+ ctx.addFinding(finding);
409
+ }
410
+ // Sprint 90 (#189): JS template_injection — Handlebars.compile /
411
+ // ejs.render / ejs.compile with a tainted source.
412
+ for (const finding of findJsTemplateInjectionSstiFindings(code, graph.ir.meta.file)) {
413
+ ctx.addFinding(finding);
414
+ }
386
415
  }
387
416
  // -- Java: Sprint 73 (#216 Pattern A) — Jackson readValue / Gson
388
417
  // fromJson recognized as ETE terminator (does not affect
@@ -7131,4 +7160,555 @@ export function findRustLogInjectionFindings(code, file) {
7131
7160
  }
7132
7161
  return findings;
7133
7162
  }
7163
+ /**
7164
+ * Sprint 89 detector A (#189) — Go insecure deserialization via
7165
+ * `encoding/gob` `gob.NewDecoder(<req-body>).Decode(&v)`.
7166
+ *
7167
+ * The `gob` package will reconstruct arbitrary Go values, and any
7168
+ * registered concrete type can be instantiated by the attacker through
7169
+ * `gob.Register(...)` side effects. Decoding directly from
7170
+ * `r.Body` (`*http.Request`) without authenticated framing is unsafe.
7171
+ * CWE-502.
7172
+ */
7173
+ export function findGoGobDeserializationFindings(code, file) {
7174
+ const findings = [];
7175
+ if (typeof code !== 'string' || code.length === 0)
7176
+ return findings;
7177
+ if (!/\bgob\s*\.\s*NewDecoder\s*\(/.test(code))
7178
+ return findings;
7179
+ if (!/\*http\.Request\b/.test(code) && !/\bhttp\.HandlerFunc\b/.test(code)) {
7180
+ return findings;
7181
+ }
7182
+ const lines = code.split('\n');
7183
+ // Track variables holding a *gob.Decoder constructed from req body.
7184
+ const decoderFromBody = new Set();
7185
+ const newDecoderAssignRe = /^(\w+)\s*(?::=|=)\s*gob\s*\.\s*NewDecoder\s*\(\s*([^)]+)\)/;
7186
+ for (const raw of lines) {
7187
+ const t = raw.trim();
7188
+ if (t.startsWith('//'))
7189
+ continue;
7190
+ const m = t.match(newDecoderAssignRe);
7191
+ if (!m)
7192
+ continue;
7193
+ const lhs = m[1];
7194
+ const arg = m[2];
7195
+ if (/\b\w+\s*\.\s*Body\b/.test(arg)) {
7196
+ decoderFromBody.add(lhs);
7197
+ }
7198
+ }
7199
+ const seen = new Set();
7200
+ if (decoderFromBody.size > 0) {
7201
+ for (let i = 0; i < lines.length; i++) {
7202
+ const line = lines[i];
7203
+ if (line.trim().startsWith('//'))
7204
+ continue;
7205
+ const m = line.match(/\b(\w+)\s*\.\s*Decode\s*\(/);
7206
+ if (!m)
7207
+ continue;
7208
+ if (!decoderFromBody.has(m[1]))
7209
+ continue;
7210
+ if (seen.has(i + 1))
7211
+ continue;
7212
+ seen.add(i + 1);
7213
+ findings.push({
7214
+ id: `insecure_deserialization-${file}-${i + 1}-go-gob`,
7215
+ pass: 'language-sources',
7216
+ category: 'security',
7217
+ rule_id: 'insecure_deserialization',
7218
+ cwe: 'CWE-502',
7219
+ severity: 'critical',
7220
+ level: 'error',
7221
+ message: 'Insecure deserialization: `gob.NewDecoder(req.Body).Decode(...)` ' +
7222
+ 'reconstructs arbitrary registered Go types from attacker-controlled ' +
7223
+ 'bytes. Use an authenticated framing (signed/MAC payloads), avoid ' +
7224
+ 'decoding interface{} values, or switch to a schema-bound format ' +
7225
+ '(JSON with explicit types, Protocol Buffers).',
7226
+ file,
7227
+ line: i + 1,
7228
+ snippet: line.trim(),
7229
+ });
7230
+ }
7231
+ }
7232
+ // Inline form — `gob.NewDecoder(req.Body).Decode(&v)`.
7233
+ for (let i = 0; i < lines.length; i++) {
7234
+ const line = lines[i];
7235
+ if (line.trim().startsWith('//'))
7236
+ continue;
7237
+ if (!/\bgob\s*\.\s*NewDecoder\s*\(\s*\w+\s*\.\s*Body\s*\)\s*\.\s*Decode\s*\(/.test(line)) {
7238
+ continue;
7239
+ }
7240
+ if (seen.has(i + 1))
7241
+ continue;
7242
+ seen.add(i + 1);
7243
+ findings.push({
7244
+ id: `insecure_deserialization-${file}-${i + 1}-go-gob-inline`,
7245
+ pass: 'language-sources',
7246
+ category: 'security',
7247
+ rule_id: 'insecure_deserialization',
7248
+ cwe: 'CWE-502',
7249
+ severity: 'critical',
7250
+ level: 'error',
7251
+ message: 'Insecure deserialization: `gob.NewDecoder(req.Body).Decode(...)` ' +
7252
+ 'reconstructs arbitrary registered Go types from attacker-controlled ' +
7253
+ 'bytes. Use an authenticated framing (signed/MAC payloads), avoid ' +
7254
+ 'decoding interface{} values, or switch to a schema-bound format ' +
7255
+ '(JSON with explicit types, Protocol Buffers).',
7256
+ file,
7257
+ line: i + 1,
7258
+ snippet: line.trim(),
7259
+ });
7260
+ }
7261
+ return findings;
7262
+ }
7263
+ /**
7264
+ * Sprint 89 detector B (#189) — JS insecure deserialization via
7265
+ * `JSON.parse(req.body)`.
7266
+ *
7267
+ * Express applications that register `express.text(...)` or
7268
+ * `bodyParser.raw(...)` middleware leave `req.body` as an
7269
+ * attacker-controlled string. Passing it through `JSON.parse(...)`
7270
+ * exposes prototype-pollution paths if the parsed object is then
7271
+ * merged into trusted state or used as a property bag. CWE-502 /
7272
+ * CWE-1321 (prototype-pollution adjacent).
7273
+ *
7274
+ * Conservative gate: only fire on `JSON.parse(<expr>)` where
7275
+ * `<expr>` resolves to `req.body` (the only express extractor that
7276
+ * is genuinely a raw string when text/raw bodyparsing is used).
7277
+ */
7278
+ export function findJsJsonParseBodyFindings(code, file) {
7279
+ const findings = [];
7280
+ if (typeof code !== 'string' || code.length === 0)
7281
+ return findings;
7282
+ if (!/\bJSON\s*\.\s*parse\s*\(/.test(code))
7283
+ return findings;
7284
+ if (!/\breq\s*\.\s*body\b/.test(code))
7285
+ return findings;
7286
+ const lines = code.split('\n');
7287
+ const reqBodyRe = /\breq\s*\.\s*body\b/;
7288
+ const taintedVars = new Set();
7289
+ for (let pass = 0; pass < 3; pass++) {
7290
+ const before = taintedVars.size;
7291
+ for (let i = 0; i < lines.length; i++) {
7292
+ const t = lines[i].trim();
7293
+ if (t.startsWith('//'))
7294
+ continue;
7295
+ const m = t.match(/^(?:const|let|var)\s+(\w+)(?:\s*:\s*[^=]+)?\s*=\s*(.+?);?$/);
7296
+ if (!m)
7297
+ continue;
7298
+ const lhs = m[1];
7299
+ const rhs = m[2];
7300
+ if (taintedVars.has(lhs))
7301
+ continue;
7302
+ if (reqBodyRe.test(rhs)) {
7303
+ taintedVars.add(lhs);
7304
+ continue;
7305
+ }
7306
+ for (const v of taintedVars) {
7307
+ if (new RegExp(`\\b${v}\\b`).test(rhs)) {
7308
+ taintedVars.add(lhs);
7309
+ break;
7310
+ }
7311
+ }
7312
+ }
7313
+ if (taintedVars.size === before)
7314
+ break;
7315
+ }
7316
+ const callRe = /\bJSON\s*\.\s*parse\s*\(\s*([^)]+?)\s*\)/g;
7317
+ const seen = new Set();
7318
+ for (let i = 0; i < lines.length; i++) {
7319
+ const line = lines[i];
7320
+ if (line.trim().startsWith('//'))
7321
+ continue;
7322
+ callRe.lastIndex = 0;
7323
+ let m;
7324
+ while ((m = callRe.exec(line)) !== null) {
7325
+ const arg = m[1];
7326
+ let tainted = reqBodyRe.test(arg);
7327
+ if (!tainted) {
7328
+ for (const v of taintedVars) {
7329
+ if (new RegExp(`\\b${v}\\b`).test(arg)) {
7330
+ tainted = true;
7331
+ break;
7332
+ }
7333
+ }
7334
+ }
7335
+ if (!tainted)
7336
+ continue;
7337
+ if (seen.has(i + 1))
7338
+ continue;
7339
+ seen.add(i + 1);
7340
+ findings.push({
7341
+ id: `insecure_deserialization-${file}-${i + 1}-js-jsonparse-body`,
7342
+ pass: 'language-sources',
7343
+ category: 'security',
7344
+ rule_id: 'insecure_deserialization',
7345
+ cwe: 'CWE-502',
7346
+ severity: 'high',
7347
+ level: 'warning',
7348
+ message: '`JSON.parse(req.body)` deserializes attacker-controlled bytes ' +
7349
+ 'directly. With `express.text()` / `bodyParser.raw()` middleware ' +
7350
+ '`req.body` is an unvetted string; the parsed object can carry a ' +
7351
+ '`__proto__` payload that pollutes downstream property lookups. ' +
7352
+ 'Validate against a schema (zod/ajv) before consuming the value.',
7353
+ file,
7354
+ line: i + 1,
7355
+ snippet: line.trim(),
7356
+ });
7357
+ }
7358
+ }
7359
+ return findings;
7360
+ }
7361
+ /**
7362
+ * Sprint 89 detector C (#189) — JS DOM XPath injection via
7363
+ * `document.evaluate(<tainted>, ...)`.
7364
+ *
7365
+ * The DOM `XPathEvaluator.evaluate()` API takes a string XPath
7366
+ * expression as its first argument. When that string is built from
7367
+ * `location.search` / `location.hash` / URLSearchParams without
7368
+ * escaping, the attacker can rewrite the query (e.g. injecting
7369
+ * `' or '1'='1`-style predicates) and exfiltrate other nodes. CWE-643.
7370
+ *
7371
+ * Gate: file must reference `XPathResult` (strong DOM-XPath signal)
7372
+ * AND call `.evaluate(`, AND contain a browser taint source
7373
+ * (`location.search`, `location.hash`, `URLSearchParams`).
7374
+ */
7375
+ export function findJsDomXpathInjectionFindings(code, file) {
7376
+ const findings = [];
7377
+ if (typeof code !== 'string' || code.length === 0)
7378
+ return findings;
7379
+ if (!/\.\s*evaluate\s*\(/.test(code))
7380
+ return findings;
7381
+ if (!/\bXPathResult\b/.test(code))
7382
+ return findings;
7383
+ const browserSourceRe = /\b(?:location\s*\.\s*(?:search|hash|href)|URLSearchParams|window\s*\.\s*name|document\s*\.\s*cookie)\b/;
7384
+ if (!browserSourceRe.test(code))
7385
+ return findings;
7386
+ const lines = code.split('\n');
7387
+ const taintedVars = new Set();
7388
+ for (let pass = 0; pass < 3; pass++) {
7389
+ const before = taintedVars.size;
7390
+ for (let i = 0; i < lines.length; i++) {
7391
+ const t = lines[i].trim();
7392
+ if (t.startsWith('//'))
7393
+ continue;
7394
+ const m = t.match(/^(?:const|let|var)\s+(\w+)(?:\s*:\s*[^=]+)?\s*=\s*(.+?);?$/);
7395
+ if (!m)
7396
+ continue;
7397
+ const lhs = m[1];
7398
+ const rhs = m[2];
7399
+ if (taintedVars.has(lhs))
7400
+ continue;
7401
+ if (browserSourceRe.test(rhs)) {
7402
+ taintedVars.add(lhs);
7403
+ continue;
7404
+ }
7405
+ for (const v of taintedVars) {
7406
+ if (new RegExp(`\\b${v}\\b`).test(rhs)) {
7407
+ taintedVars.add(lhs);
7408
+ break;
7409
+ }
7410
+ }
7411
+ }
7412
+ if (taintedVars.size === before)
7413
+ break;
7414
+ }
7415
+ if (taintedVars.size === 0)
7416
+ return findings;
7417
+ const evalRe = /\.\s*evaluate\s*\(\s*([^,)]+)/;
7418
+ const seen = new Set();
7419
+ for (let i = 0; i < lines.length; i++) {
7420
+ const line = lines[i];
7421
+ if (line.trim().startsWith('//'))
7422
+ continue;
7423
+ const m = line.match(evalRe);
7424
+ if (!m)
7425
+ continue;
7426
+ const arg = m[1].trim();
7427
+ let tainted = false;
7428
+ for (const v of taintedVars) {
7429
+ if (new RegExp(`\\b${v}\\b`).test(arg)) {
7430
+ tainted = true;
7431
+ break;
7432
+ }
7433
+ }
7434
+ if (!tainted)
7435
+ continue;
7436
+ if (seen.has(i + 1))
7437
+ continue;
7438
+ seen.add(i + 1);
7439
+ findings.push({
7440
+ id: `xpath_injection-${file}-${i + 1}-js-dom-evaluate`,
7441
+ pass: 'language-sources',
7442
+ category: 'security',
7443
+ rule_id: 'xpath_injection',
7444
+ cwe: 'CWE-643',
7445
+ severity: 'high',
7446
+ level: 'warning',
7447
+ message: 'XPath injection: `document.evaluate(<tainted>, ...)` lets the ' +
7448
+ 'attacker break out of the XPath expression and read sibling ' +
7449
+ 'nodes. Bind user input through XPath variables / parameterized ' +
7450
+ 'expressions, or escape with an allowlist before concatenation.',
7451
+ file,
7452
+ line: i + 1,
7453
+ snippet: line.trim(),
7454
+ });
7455
+ }
7456
+ return findings;
7457
+ }
7458
+ /**
7459
+ * Sprint 90 detector A (#189) — Go XXE via `encoding/xml` decoder with
7460
+ * `d.Strict = false` (allows DTD-like constructs and custom Entity
7461
+ * resolution to be configured on the decoder). When the source stream
7462
+ * is `*http.Request.Body`, the attacker can submit a payload that
7463
+ * triggers entity-expansion / external-entity reads through the
7464
+ * `Entity` map. CWE-611 / CWE-776.
7465
+ */
7466
+ export function findGoXmlDecoderXxeFindings(code, file) {
7467
+ const findings = [];
7468
+ if (typeof code !== 'string' || code.length === 0)
7469
+ return findings;
7470
+ if (!/\bxml\s*\.\s*NewDecoder\s*\(/.test(code))
7471
+ return findings;
7472
+ if (!/\*http\.Request\b/.test(code) && !/\bhttp\.HandlerFunc\b/.test(code)) {
7473
+ return findings;
7474
+ }
7475
+ const lines = code.split('\n');
7476
+ // Track decoder vars constructed from req.Body.
7477
+ const decoderFromBody = new Map(); // var -> line idx
7478
+ for (let i = 0; i < lines.length; i++) {
7479
+ const t = lines[i].trim();
7480
+ if (t.startsWith('//'))
7481
+ continue;
7482
+ const m = t.match(/^(\w+)\s*(?::=|=)\s*xml\s*\.\s*NewDecoder\s*\(\s*([^)]+)\)/);
7483
+ if (!m)
7484
+ continue;
7485
+ if (/\b\w+\s*\.\s*Body\b/.test(m[2]))
7486
+ decoderFromBody.set(m[1], i);
7487
+ }
7488
+ if (decoderFromBody.size === 0)
7489
+ return findings;
7490
+ // Only fire if downstream we see `<dec>.Strict = false` or
7491
+ // `<dec>.Entity = ...` (entity-map override).
7492
+ const seen = new Set();
7493
+ for (let i = 0; i < lines.length; i++) {
7494
+ const line = lines[i];
7495
+ if (line.trim().startsWith('//'))
7496
+ continue;
7497
+ const m = line.match(/\b(\w+)\s*\.\s*(?:Strict\s*=\s*false|Entity\s*=)\b/);
7498
+ if (!m)
7499
+ continue;
7500
+ if (!decoderFromBody.has(m[1]))
7501
+ continue;
7502
+ if (seen.has(i + 1))
7503
+ continue;
7504
+ seen.add(i + 1);
7505
+ findings.push({
7506
+ id: `xml_entity_expansion-${file}-${i + 1}-go-xml-decoder`,
7507
+ pass: 'language-sources',
7508
+ category: 'security',
7509
+ rule_id: 'xml_entity_expansion',
7510
+ cwe: 'CWE-611',
7511
+ severity: 'high',
7512
+ level: 'warning',
7513
+ message: 'XXE: Go `xml.NewDecoder(req.Body)` with `Strict = false` (or a ' +
7514
+ 'custom `Entity` map) allows entity references that the standard ' +
7515
+ 'library normally rejects. Keep `Strict = true` and avoid setting ' +
7516
+ '`Entity` from attacker-controlled inputs.',
7517
+ file,
7518
+ line: i + 1,
7519
+ snippet: line.trim(),
7520
+ });
7521
+ }
7522
+ return findings;
7523
+ }
7524
+ /**
7525
+ * Sprint 90 detector B (#189) — Python SSTI via Jinja2
7526
+ * `Template(<tainted>).render(...)`.
7527
+ *
7528
+ * Constructing a `jinja2.Template` from attacker-controlled source
7529
+ * gives the attacker the entire Jinja sandbox-escape surface
7530
+ * (`{{ ''.__class__.__mro__[1].__subclasses__() ... }}`). CWE-1336.
7531
+ *
7532
+ * Gate: file must import `jinja2.Template` AND construct it from a
7533
+ * Flask/FastAPI request extractor (request.args/form/values/json/data).
7534
+ */
7535
+ export function findPythonJinjaTemplateSstiFindings(code, file) {
7536
+ const findings = [];
7537
+ if (typeof code !== 'string' || code.length === 0)
7538
+ return findings;
7539
+ if (!/\bfrom\s+jinja2\s+import\s+[^#\n]*\bTemplate\b/.test(code) &&
7540
+ !/\bjinja2\s*\.\s*Template\b/.test(code)) {
7541
+ return findings;
7542
+ }
7543
+ const reqExtractRe = /\brequest\s*\.\s*(?:args|form|values|json|data|cookies|headers)\b/;
7544
+ if (!reqExtractRe.test(code))
7545
+ return findings;
7546
+ const lines = code.split('\n');
7547
+ const taintedVars = new Set();
7548
+ for (let pass = 0; pass < 3; pass++) {
7549
+ const before = taintedVars.size;
7550
+ for (let i = 0; i < lines.length; i++) {
7551
+ const t = lines[i].trim();
7552
+ if (t.startsWith('#'))
7553
+ continue;
7554
+ const m = t.match(/^(\w+)\s*=\s*(.+?)$/);
7555
+ if (!m)
7556
+ continue;
7557
+ const lhs = m[1];
7558
+ const rhs = m[2];
7559
+ if (taintedVars.has(lhs))
7560
+ continue;
7561
+ if (reqExtractRe.test(rhs)) {
7562
+ taintedVars.add(lhs);
7563
+ continue;
7564
+ }
7565
+ for (const v of taintedVars) {
7566
+ if (new RegExp(`\\b${v}\\b`).test(rhs)) {
7567
+ taintedVars.add(lhs);
7568
+ break;
7569
+ }
7570
+ }
7571
+ }
7572
+ if (taintedVars.size === before)
7573
+ break;
7574
+ }
7575
+ if (taintedVars.size === 0)
7576
+ return findings;
7577
+ const ctorRe = /\b(?:jinja2\s*\.\s*)?Template\s*\(\s*([^)]+?)\s*\)/;
7578
+ const seen = new Set();
7579
+ for (let i = 0; i < lines.length; i++) {
7580
+ const line = lines[i];
7581
+ if (line.trim().startsWith('#'))
7582
+ continue;
7583
+ const m = line.match(ctorRe);
7584
+ if (!m)
7585
+ continue;
7586
+ const arg = m[1].trim();
7587
+ let tainted = reqExtractRe.test(arg);
7588
+ if (!tainted) {
7589
+ for (const v of taintedVars) {
7590
+ if (new RegExp(`\\b${v}\\b`).test(arg)) {
7591
+ tainted = true;
7592
+ break;
7593
+ }
7594
+ }
7595
+ }
7596
+ if (!tainted)
7597
+ continue;
7598
+ if (seen.has(i + 1))
7599
+ continue;
7600
+ seen.add(i + 1);
7601
+ findings.push({
7602
+ id: `template_injection-${file}-${i + 1}-py-jinja-template`,
7603
+ pass: 'language-sources',
7604
+ category: 'security',
7605
+ rule_id: 'template_injection',
7606
+ cwe: 'CWE-1336',
7607
+ severity: 'critical',
7608
+ level: 'error',
7609
+ message: 'Server-side template injection: `jinja2.Template(<tainted>).render()` ' +
7610
+ 'compiles attacker-controlled source. Sandbox-escape gadgets such as ' +
7611
+ '`{{ ().__class__.__bases__[0].__subclasses__() }}` lead to RCE. ' +
7612
+ 'Render fixed templates with user data passed as context variables.',
7613
+ file,
7614
+ line: i + 1,
7615
+ snippet: line.trim(),
7616
+ });
7617
+ }
7618
+ return findings;
7619
+ }
7620
+ /**
7621
+ * Sprint 90 detector C (#189) — JS SSTI via `Handlebars.compile(<tainted>)`
7622
+ * or `ejs.render(<tainted>, ...)` / `ejs.compile(<tainted>)`.
7623
+ *
7624
+ * Compiling an attacker-controlled template lets the attacker execute
7625
+ * arbitrary code through helper-shadowing / prototype gadgets
7626
+ * (Handlebars CVE-2019-19919 chains, EJS render-options escape). CWE-1336.
7627
+ */
7628
+ export function findJsTemplateInjectionSstiFindings(code, file) {
7629
+ const findings = [];
7630
+ if (typeof code !== 'string' || code.length === 0)
7631
+ return findings;
7632
+ if (!/\bHandlebars\s*\.\s*compile\s*\(/.test(code) &&
7633
+ !/\bejs\s*\.\s*(?:render|compile)\s*\(/.test(code)) {
7634
+ return findings;
7635
+ }
7636
+ if (!/\breq\s*\.\s*(?:query|body|params|headers|cookies)\b/.test(code)) {
7637
+ return findings;
7638
+ }
7639
+ const lines = code.split('\n');
7640
+ const reqExtractRe = /\breq\s*\.\s*(?:query|body|params|headers|cookies)\b/;
7641
+ const taintedVars = new Set();
7642
+ for (let pass = 0; pass < 3; pass++) {
7643
+ const before = taintedVars.size;
7644
+ for (let i = 0; i < lines.length; i++) {
7645
+ const t = lines[i].trim();
7646
+ if (t.startsWith('//'))
7647
+ continue;
7648
+ const m = t.match(/^(?:const|let|var)\s+(\w+)(?:\s*:\s*[^=]+)?\s*=\s*(.+?);?$/);
7649
+ if (!m)
7650
+ continue;
7651
+ const lhs = m[1];
7652
+ const rhs = m[2];
7653
+ if (taintedVars.has(lhs))
7654
+ continue;
7655
+ if (reqExtractRe.test(rhs)) {
7656
+ taintedVars.add(lhs);
7657
+ continue;
7658
+ }
7659
+ for (const v of taintedVars) {
7660
+ if (new RegExp(`\\b${v}\\b`).test(rhs)) {
7661
+ taintedVars.add(lhs);
7662
+ break;
7663
+ }
7664
+ }
7665
+ }
7666
+ if (taintedVars.size === before)
7667
+ break;
7668
+ }
7669
+ if (taintedVars.size === 0)
7670
+ return findings;
7671
+ const callRe = /\b(Handlebars\s*\.\s*compile|ejs\s*\.\s*(?:render|compile))\s*\(\s*([^,)]+)/;
7672
+ const seen = new Set();
7673
+ for (let i = 0; i < lines.length; i++) {
7674
+ const line = lines[i];
7675
+ if (line.trim().startsWith('//'))
7676
+ continue;
7677
+ const m = line.match(callRe);
7678
+ if (!m)
7679
+ continue;
7680
+ const arg = m[2].trim();
7681
+ let tainted = reqExtractRe.test(arg);
7682
+ if (!tainted) {
7683
+ for (const v of taintedVars) {
7684
+ if (new RegExp(`\\b${v}\\b`).test(arg)) {
7685
+ tainted = true;
7686
+ break;
7687
+ }
7688
+ }
7689
+ }
7690
+ if (!tainted)
7691
+ continue;
7692
+ if (seen.has(i + 1))
7693
+ continue;
7694
+ seen.add(i + 1);
7695
+ findings.push({
7696
+ id: `template_injection-${file}-${i + 1}-js-${m[1].replace(/[\s.]/g, '').toLowerCase()}`,
7697
+ pass: 'language-sources',
7698
+ category: 'security',
7699
+ rule_id: 'template_injection',
7700
+ cwe: 'CWE-1336',
7701
+ severity: 'critical',
7702
+ level: 'error',
7703
+ message: 'Server-side template injection: compiling/rendering a template ' +
7704
+ 'whose source is attacker-controlled (`Handlebars.compile(...)` / ' +
7705
+ '`ejs.render(...)`) opens helper-shadowing and prototype-gadget RCE ' +
7706
+ 'paths. Use a fixed template and pass user data as context.',
7707
+ file,
7708
+ line: i + 1,
7709
+ snippet: line.trim(),
7710
+ });
7711
+ }
7712
+ return findings;
7713
+ }
7134
7714
  //# sourceMappingURL=language-sources-pass.js.map