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.
- package/dist/analysis/passes/language-sources-pass.d.ts +72 -0
- package/dist/analysis/passes/language-sources-pass.d.ts.map +1 -1
- package/dist/analysis/passes/language-sources-pass.js +580 -0
- package/dist/analysis/passes/language-sources-pass.js.map +1 -1
- package/dist/browser/circle-ir.js +432 -0
- package/package.json +1 -1
|
@@ -23690,6 +23690,18 @@ var LanguageSourcesPass = class {
|
|
|
23690
23690
|
)) {
|
|
23691
23691
|
ctx.addFinding(finding);
|
|
23692
23692
|
}
|
|
23693
|
+
for (const finding of findGoGobDeserializationFindings(
|
|
23694
|
+
code,
|
|
23695
|
+
graph.ir.meta.file
|
|
23696
|
+
)) {
|
|
23697
|
+
ctx.addFinding(finding);
|
|
23698
|
+
}
|
|
23699
|
+
for (const finding of findGoXmlDecoderXxeFindings(
|
|
23700
|
+
code,
|
|
23701
|
+
graph.ir.meta.file
|
|
23702
|
+
)) {
|
|
23703
|
+
ctx.addFinding(finding);
|
|
23704
|
+
}
|
|
23693
23705
|
}
|
|
23694
23706
|
if (language === "python") {
|
|
23695
23707
|
additionalSanitizers.push(...findPythonNetlocAllowlistGuardSanitizers(code));
|
|
@@ -23738,6 +23750,12 @@ var LanguageSourcesPass = class {
|
|
|
23738
23750
|
)) {
|
|
23739
23751
|
ctx.addFinding(finding);
|
|
23740
23752
|
}
|
|
23753
|
+
for (const finding of findPythonJinjaTemplateSstiFindings(
|
|
23754
|
+
code,
|
|
23755
|
+
graph.ir.meta.file
|
|
23756
|
+
)) {
|
|
23757
|
+
ctx.addFinding(finding);
|
|
23758
|
+
}
|
|
23741
23759
|
}
|
|
23742
23760
|
if (language === "rust") {
|
|
23743
23761
|
additionalSanitizers.push(...findRustSetAllowlistGuardSanitizers(code));
|
|
@@ -23825,6 +23843,24 @@ var LanguageSourcesPass = class {
|
|
|
23825
23843
|
)) {
|
|
23826
23844
|
ctx.addFinding(finding);
|
|
23827
23845
|
}
|
|
23846
|
+
for (const finding of findJsJsonParseBodyFindings(
|
|
23847
|
+
code,
|
|
23848
|
+
graph.ir.meta.file
|
|
23849
|
+
)) {
|
|
23850
|
+
ctx.addFinding(finding);
|
|
23851
|
+
}
|
|
23852
|
+
for (const finding of findJsDomXpathInjectionFindings(
|
|
23853
|
+
code,
|
|
23854
|
+
graph.ir.meta.file
|
|
23855
|
+
)) {
|
|
23856
|
+
ctx.addFinding(finding);
|
|
23857
|
+
}
|
|
23858
|
+
for (const finding of findJsTemplateInjectionSstiFindings(
|
|
23859
|
+
code,
|
|
23860
|
+
graph.ir.meta.file
|
|
23861
|
+
)) {
|
|
23862
|
+
ctx.addFinding(finding);
|
|
23863
|
+
}
|
|
23828
23864
|
}
|
|
23829
23865
|
if (language === "java") {
|
|
23830
23866
|
additionalSanitizers.push(...findJavaSafeJsonParseSanitizers(code));
|
|
@@ -28454,6 +28490,402 @@ function findRustLogInjectionFindings(code, file) {
|
|
|
28454
28490
|
}
|
|
28455
28491
|
return findings;
|
|
28456
28492
|
}
|
|
28493
|
+
function findGoGobDeserializationFindings(code, file) {
|
|
28494
|
+
const findings = [];
|
|
28495
|
+
if (typeof code !== "string" || code.length === 0) return findings;
|
|
28496
|
+
if (!/\bgob\s*\.\s*NewDecoder\s*\(/.test(code)) return findings;
|
|
28497
|
+
if (!/\*http\.Request\b/.test(code) && !/\bhttp\.HandlerFunc\b/.test(code)) {
|
|
28498
|
+
return findings;
|
|
28499
|
+
}
|
|
28500
|
+
const lines = code.split("\n");
|
|
28501
|
+
const decoderFromBody = /* @__PURE__ */ new Set();
|
|
28502
|
+
const newDecoderAssignRe = /^(\w+)\s*(?::=|=)\s*gob\s*\.\s*NewDecoder\s*\(\s*([^)]+)\)/;
|
|
28503
|
+
for (const raw of lines) {
|
|
28504
|
+
const t = raw.trim();
|
|
28505
|
+
if (t.startsWith("//")) continue;
|
|
28506
|
+
const m = t.match(newDecoderAssignRe);
|
|
28507
|
+
if (!m) continue;
|
|
28508
|
+
const lhs = m[1];
|
|
28509
|
+
const arg = m[2];
|
|
28510
|
+
if (/\b\w+\s*\.\s*Body\b/.test(arg)) {
|
|
28511
|
+
decoderFromBody.add(lhs);
|
|
28512
|
+
}
|
|
28513
|
+
}
|
|
28514
|
+
const seen = /* @__PURE__ */ new Set();
|
|
28515
|
+
if (decoderFromBody.size > 0) {
|
|
28516
|
+
for (let i2 = 0; i2 < lines.length; i2++) {
|
|
28517
|
+
const line = lines[i2];
|
|
28518
|
+
if (line.trim().startsWith("//")) continue;
|
|
28519
|
+
const m = line.match(/\b(\w+)\s*\.\s*Decode\s*\(/);
|
|
28520
|
+
if (!m) continue;
|
|
28521
|
+
if (!decoderFromBody.has(m[1])) continue;
|
|
28522
|
+
if (seen.has(i2 + 1)) continue;
|
|
28523
|
+
seen.add(i2 + 1);
|
|
28524
|
+
findings.push({
|
|
28525
|
+
id: `insecure_deserialization-${file}-${i2 + 1}-go-gob`,
|
|
28526
|
+
pass: "language-sources",
|
|
28527
|
+
category: "security",
|
|
28528
|
+
rule_id: "insecure_deserialization",
|
|
28529
|
+
cwe: "CWE-502",
|
|
28530
|
+
severity: "critical",
|
|
28531
|
+
level: "error",
|
|
28532
|
+
message: "Insecure deserialization: `gob.NewDecoder(req.Body).Decode(...)` reconstructs arbitrary registered Go types from attacker-controlled bytes. Use an authenticated framing (signed/MAC payloads), avoid decoding interface{} values, or switch to a schema-bound format (JSON with explicit types, Protocol Buffers).",
|
|
28533
|
+
file,
|
|
28534
|
+
line: i2 + 1,
|
|
28535
|
+
snippet: line.trim()
|
|
28536
|
+
});
|
|
28537
|
+
}
|
|
28538
|
+
}
|
|
28539
|
+
for (let i2 = 0; i2 < lines.length; i2++) {
|
|
28540
|
+
const line = lines[i2];
|
|
28541
|
+
if (line.trim().startsWith("//")) continue;
|
|
28542
|
+
if (!/\bgob\s*\.\s*NewDecoder\s*\(\s*\w+\s*\.\s*Body\s*\)\s*\.\s*Decode\s*\(/.test(
|
|
28543
|
+
line
|
|
28544
|
+
)) {
|
|
28545
|
+
continue;
|
|
28546
|
+
}
|
|
28547
|
+
if (seen.has(i2 + 1)) continue;
|
|
28548
|
+
seen.add(i2 + 1);
|
|
28549
|
+
findings.push({
|
|
28550
|
+
id: `insecure_deserialization-${file}-${i2 + 1}-go-gob-inline`,
|
|
28551
|
+
pass: "language-sources",
|
|
28552
|
+
category: "security",
|
|
28553
|
+
rule_id: "insecure_deserialization",
|
|
28554
|
+
cwe: "CWE-502",
|
|
28555
|
+
severity: "critical",
|
|
28556
|
+
level: "error",
|
|
28557
|
+
message: "Insecure deserialization: `gob.NewDecoder(req.Body).Decode(...)` reconstructs arbitrary registered Go types from attacker-controlled bytes. Use an authenticated framing (signed/MAC payloads), avoid decoding interface{} values, or switch to a schema-bound format (JSON with explicit types, Protocol Buffers).",
|
|
28558
|
+
file,
|
|
28559
|
+
line: i2 + 1,
|
|
28560
|
+
snippet: line.trim()
|
|
28561
|
+
});
|
|
28562
|
+
}
|
|
28563
|
+
return findings;
|
|
28564
|
+
}
|
|
28565
|
+
function findJsJsonParseBodyFindings(code, file) {
|
|
28566
|
+
const findings = [];
|
|
28567
|
+
if (typeof code !== "string" || code.length === 0) return findings;
|
|
28568
|
+
if (!/\bJSON\s*\.\s*parse\s*\(/.test(code)) return findings;
|
|
28569
|
+
if (!/\breq\s*\.\s*body\b/.test(code)) return findings;
|
|
28570
|
+
const lines = code.split("\n");
|
|
28571
|
+
const reqBodyRe = /\breq\s*\.\s*body\b/;
|
|
28572
|
+
const taintedVars = /* @__PURE__ */ new Set();
|
|
28573
|
+
for (let pass = 0; pass < 3; pass++) {
|
|
28574
|
+
const before = taintedVars.size;
|
|
28575
|
+
for (let i2 = 0; i2 < lines.length; i2++) {
|
|
28576
|
+
const t = lines[i2].trim();
|
|
28577
|
+
if (t.startsWith("//")) continue;
|
|
28578
|
+
const m = t.match(/^(?:const|let|var)\s+(\w+)(?:\s*:\s*[^=]+)?\s*=\s*(.+?);?$/);
|
|
28579
|
+
if (!m) continue;
|
|
28580
|
+
const lhs = m[1];
|
|
28581
|
+
const rhs = m[2];
|
|
28582
|
+
if (taintedVars.has(lhs)) continue;
|
|
28583
|
+
if (reqBodyRe.test(rhs)) {
|
|
28584
|
+
taintedVars.add(lhs);
|
|
28585
|
+
continue;
|
|
28586
|
+
}
|
|
28587
|
+
for (const v of taintedVars) {
|
|
28588
|
+
if (new RegExp(`\\b${v}\\b`).test(rhs)) {
|
|
28589
|
+
taintedVars.add(lhs);
|
|
28590
|
+
break;
|
|
28591
|
+
}
|
|
28592
|
+
}
|
|
28593
|
+
}
|
|
28594
|
+
if (taintedVars.size === before) break;
|
|
28595
|
+
}
|
|
28596
|
+
const callRe = /\bJSON\s*\.\s*parse\s*\(\s*([^)]+?)\s*\)/g;
|
|
28597
|
+
const seen = /* @__PURE__ */ new Set();
|
|
28598
|
+
for (let i2 = 0; i2 < lines.length; i2++) {
|
|
28599
|
+
const line = lines[i2];
|
|
28600
|
+
if (line.trim().startsWith("//")) continue;
|
|
28601
|
+
callRe.lastIndex = 0;
|
|
28602
|
+
let m;
|
|
28603
|
+
while ((m = callRe.exec(line)) !== null) {
|
|
28604
|
+
const arg = m[1];
|
|
28605
|
+
let tainted = reqBodyRe.test(arg);
|
|
28606
|
+
if (!tainted) {
|
|
28607
|
+
for (const v of taintedVars) {
|
|
28608
|
+
if (new RegExp(`\\b${v}\\b`).test(arg)) {
|
|
28609
|
+
tainted = true;
|
|
28610
|
+
break;
|
|
28611
|
+
}
|
|
28612
|
+
}
|
|
28613
|
+
}
|
|
28614
|
+
if (!tainted) continue;
|
|
28615
|
+
if (seen.has(i2 + 1)) continue;
|
|
28616
|
+
seen.add(i2 + 1);
|
|
28617
|
+
findings.push({
|
|
28618
|
+
id: `insecure_deserialization-${file}-${i2 + 1}-js-jsonparse-body`,
|
|
28619
|
+
pass: "language-sources",
|
|
28620
|
+
category: "security",
|
|
28621
|
+
rule_id: "insecure_deserialization",
|
|
28622
|
+
cwe: "CWE-502",
|
|
28623
|
+
severity: "high",
|
|
28624
|
+
level: "warning",
|
|
28625
|
+
message: "`JSON.parse(req.body)` deserializes attacker-controlled bytes directly. With `express.text()` / `bodyParser.raw()` middleware `req.body` is an unvetted string; the parsed object can carry a `__proto__` payload that pollutes downstream property lookups. Validate against a schema (zod/ajv) before consuming the value.",
|
|
28626
|
+
file,
|
|
28627
|
+
line: i2 + 1,
|
|
28628
|
+
snippet: line.trim()
|
|
28629
|
+
});
|
|
28630
|
+
}
|
|
28631
|
+
}
|
|
28632
|
+
return findings;
|
|
28633
|
+
}
|
|
28634
|
+
function findJsDomXpathInjectionFindings(code, file) {
|
|
28635
|
+
const findings = [];
|
|
28636
|
+
if (typeof code !== "string" || code.length === 0) return findings;
|
|
28637
|
+
if (!/\.\s*evaluate\s*\(/.test(code)) return findings;
|
|
28638
|
+
if (!/\bXPathResult\b/.test(code)) return findings;
|
|
28639
|
+
const browserSourceRe = /\b(?:location\s*\.\s*(?:search|hash|href)|URLSearchParams|window\s*\.\s*name|document\s*\.\s*cookie)\b/;
|
|
28640
|
+
if (!browserSourceRe.test(code)) return findings;
|
|
28641
|
+
const lines = code.split("\n");
|
|
28642
|
+
const taintedVars = /* @__PURE__ */ new Set();
|
|
28643
|
+
for (let pass = 0; pass < 3; pass++) {
|
|
28644
|
+
const before = taintedVars.size;
|
|
28645
|
+
for (let i2 = 0; i2 < lines.length; i2++) {
|
|
28646
|
+
const t = lines[i2].trim();
|
|
28647
|
+
if (t.startsWith("//")) continue;
|
|
28648
|
+
const m = t.match(/^(?:const|let|var)\s+(\w+)(?:\s*:\s*[^=]+)?\s*=\s*(.+?);?$/);
|
|
28649
|
+
if (!m) continue;
|
|
28650
|
+
const lhs = m[1];
|
|
28651
|
+
const rhs = m[2];
|
|
28652
|
+
if (taintedVars.has(lhs)) continue;
|
|
28653
|
+
if (browserSourceRe.test(rhs)) {
|
|
28654
|
+
taintedVars.add(lhs);
|
|
28655
|
+
continue;
|
|
28656
|
+
}
|
|
28657
|
+
for (const v of taintedVars) {
|
|
28658
|
+
if (new RegExp(`\\b${v}\\b`).test(rhs)) {
|
|
28659
|
+
taintedVars.add(lhs);
|
|
28660
|
+
break;
|
|
28661
|
+
}
|
|
28662
|
+
}
|
|
28663
|
+
}
|
|
28664
|
+
if (taintedVars.size === before) break;
|
|
28665
|
+
}
|
|
28666
|
+
if (taintedVars.size === 0) return findings;
|
|
28667
|
+
const evalRe = /\.\s*evaluate\s*\(\s*([^,)]+)/;
|
|
28668
|
+
const seen = /* @__PURE__ */ new Set();
|
|
28669
|
+
for (let i2 = 0; i2 < lines.length; i2++) {
|
|
28670
|
+
const line = lines[i2];
|
|
28671
|
+
if (line.trim().startsWith("//")) continue;
|
|
28672
|
+
const m = line.match(evalRe);
|
|
28673
|
+
if (!m) continue;
|
|
28674
|
+
const arg = m[1].trim();
|
|
28675
|
+
let tainted = false;
|
|
28676
|
+
for (const v of taintedVars) {
|
|
28677
|
+
if (new RegExp(`\\b${v}\\b`).test(arg)) {
|
|
28678
|
+
tainted = true;
|
|
28679
|
+
break;
|
|
28680
|
+
}
|
|
28681
|
+
}
|
|
28682
|
+
if (!tainted) continue;
|
|
28683
|
+
if (seen.has(i2 + 1)) continue;
|
|
28684
|
+
seen.add(i2 + 1);
|
|
28685
|
+
findings.push({
|
|
28686
|
+
id: `xpath_injection-${file}-${i2 + 1}-js-dom-evaluate`,
|
|
28687
|
+
pass: "language-sources",
|
|
28688
|
+
category: "security",
|
|
28689
|
+
rule_id: "xpath_injection",
|
|
28690
|
+
cwe: "CWE-643",
|
|
28691
|
+
severity: "high",
|
|
28692
|
+
level: "warning",
|
|
28693
|
+
message: "XPath injection: `document.evaluate(<tainted>, ...)` lets the attacker break out of the XPath expression and read sibling nodes. Bind user input through XPath variables / parameterized expressions, or escape with an allowlist before concatenation.",
|
|
28694
|
+
file,
|
|
28695
|
+
line: i2 + 1,
|
|
28696
|
+
snippet: line.trim()
|
|
28697
|
+
});
|
|
28698
|
+
}
|
|
28699
|
+
return findings;
|
|
28700
|
+
}
|
|
28701
|
+
function findGoXmlDecoderXxeFindings(code, file) {
|
|
28702
|
+
const findings = [];
|
|
28703
|
+
if (typeof code !== "string" || code.length === 0) return findings;
|
|
28704
|
+
if (!/\bxml\s*\.\s*NewDecoder\s*\(/.test(code)) return findings;
|
|
28705
|
+
if (!/\*http\.Request\b/.test(code) && !/\bhttp\.HandlerFunc\b/.test(code)) {
|
|
28706
|
+
return findings;
|
|
28707
|
+
}
|
|
28708
|
+
const lines = code.split("\n");
|
|
28709
|
+
const decoderFromBody = /* @__PURE__ */ new Map();
|
|
28710
|
+
for (let i2 = 0; i2 < lines.length; i2++) {
|
|
28711
|
+
const t = lines[i2].trim();
|
|
28712
|
+
if (t.startsWith("//")) continue;
|
|
28713
|
+
const m = t.match(
|
|
28714
|
+
/^(\w+)\s*(?::=|=)\s*xml\s*\.\s*NewDecoder\s*\(\s*([^)]+)\)/
|
|
28715
|
+
);
|
|
28716
|
+
if (!m) continue;
|
|
28717
|
+
if (/\b\w+\s*\.\s*Body\b/.test(m[2])) decoderFromBody.set(m[1], i2);
|
|
28718
|
+
}
|
|
28719
|
+
if (decoderFromBody.size === 0) return findings;
|
|
28720
|
+
const seen = /* @__PURE__ */ new Set();
|
|
28721
|
+
for (let i2 = 0; i2 < lines.length; i2++) {
|
|
28722
|
+
const line = lines[i2];
|
|
28723
|
+
if (line.trim().startsWith("//")) continue;
|
|
28724
|
+
const m = line.match(
|
|
28725
|
+
/\b(\w+)\s*\.\s*(?:Strict\s*=\s*false|Entity\s*=)\b/
|
|
28726
|
+
);
|
|
28727
|
+
if (!m) continue;
|
|
28728
|
+
if (!decoderFromBody.has(m[1])) continue;
|
|
28729
|
+
if (seen.has(i2 + 1)) continue;
|
|
28730
|
+
seen.add(i2 + 1);
|
|
28731
|
+
findings.push({
|
|
28732
|
+
id: `xml_entity_expansion-${file}-${i2 + 1}-go-xml-decoder`,
|
|
28733
|
+
pass: "language-sources",
|
|
28734
|
+
category: "security",
|
|
28735
|
+
rule_id: "xml_entity_expansion",
|
|
28736
|
+
cwe: "CWE-611",
|
|
28737
|
+
severity: "high",
|
|
28738
|
+
level: "warning",
|
|
28739
|
+
message: "XXE: Go `xml.NewDecoder(req.Body)` with `Strict = false` (or a custom `Entity` map) allows entity references that the standard library normally rejects. Keep `Strict = true` and avoid setting `Entity` from attacker-controlled inputs.",
|
|
28740
|
+
file,
|
|
28741
|
+
line: i2 + 1,
|
|
28742
|
+
snippet: line.trim()
|
|
28743
|
+
});
|
|
28744
|
+
}
|
|
28745
|
+
return findings;
|
|
28746
|
+
}
|
|
28747
|
+
function findPythonJinjaTemplateSstiFindings(code, file) {
|
|
28748
|
+
const findings = [];
|
|
28749
|
+
if (typeof code !== "string" || code.length === 0) return findings;
|
|
28750
|
+
if (!/\bfrom\s+jinja2\s+import\s+[^#\n]*\bTemplate\b/.test(code) && !/\bjinja2\s*\.\s*Template\b/.test(code)) {
|
|
28751
|
+
return findings;
|
|
28752
|
+
}
|
|
28753
|
+
const reqExtractRe = /\brequest\s*\.\s*(?:args|form|values|json|data|cookies|headers)\b/;
|
|
28754
|
+
if (!reqExtractRe.test(code)) return findings;
|
|
28755
|
+
const lines = code.split("\n");
|
|
28756
|
+
const taintedVars = /* @__PURE__ */ new Set();
|
|
28757
|
+
for (let pass = 0; pass < 3; pass++) {
|
|
28758
|
+
const before = taintedVars.size;
|
|
28759
|
+
for (let i2 = 0; i2 < lines.length; i2++) {
|
|
28760
|
+
const t = lines[i2].trim();
|
|
28761
|
+
if (t.startsWith("#")) continue;
|
|
28762
|
+
const m = t.match(/^(\w+)\s*=\s*(.+?)$/);
|
|
28763
|
+
if (!m) continue;
|
|
28764
|
+
const lhs = m[1];
|
|
28765
|
+
const rhs = m[2];
|
|
28766
|
+
if (taintedVars.has(lhs)) continue;
|
|
28767
|
+
if (reqExtractRe.test(rhs)) {
|
|
28768
|
+
taintedVars.add(lhs);
|
|
28769
|
+
continue;
|
|
28770
|
+
}
|
|
28771
|
+
for (const v of taintedVars) {
|
|
28772
|
+
if (new RegExp(`\\b${v}\\b`).test(rhs)) {
|
|
28773
|
+
taintedVars.add(lhs);
|
|
28774
|
+
break;
|
|
28775
|
+
}
|
|
28776
|
+
}
|
|
28777
|
+
}
|
|
28778
|
+
if (taintedVars.size === before) break;
|
|
28779
|
+
}
|
|
28780
|
+
if (taintedVars.size === 0) return findings;
|
|
28781
|
+
const ctorRe = /\b(?:jinja2\s*\.\s*)?Template\s*\(\s*([^)]+?)\s*\)/;
|
|
28782
|
+
const seen = /* @__PURE__ */ new Set();
|
|
28783
|
+
for (let i2 = 0; i2 < lines.length; i2++) {
|
|
28784
|
+
const line = lines[i2];
|
|
28785
|
+
if (line.trim().startsWith("#")) continue;
|
|
28786
|
+
const m = line.match(ctorRe);
|
|
28787
|
+
if (!m) continue;
|
|
28788
|
+
const arg = m[1].trim();
|
|
28789
|
+
let tainted = reqExtractRe.test(arg);
|
|
28790
|
+
if (!tainted) {
|
|
28791
|
+
for (const v of taintedVars) {
|
|
28792
|
+
if (new RegExp(`\\b${v}\\b`).test(arg)) {
|
|
28793
|
+
tainted = true;
|
|
28794
|
+
break;
|
|
28795
|
+
}
|
|
28796
|
+
}
|
|
28797
|
+
}
|
|
28798
|
+
if (!tainted) continue;
|
|
28799
|
+
if (seen.has(i2 + 1)) continue;
|
|
28800
|
+
seen.add(i2 + 1);
|
|
28801
|
+
findings.push({
|
|
28802
|
+
id: `template_injection-${file}-${i2 + 1}-py-jinja-template`,
|
|
28803
|
+
pass: "language-sources",
|
|
28804
|
+
category: "security",
|
|
28805
|
+
rule_id: "template_injection",
|
|
28806
|
+
cwe: "CWE-1336",
|
|
28807
|
+
severity: "critical",
|
|
28808
|
+
level: "error",
|
|
28809
|
+
message: "Server-side template injection: `jinja2.Template(<tainted>).render()` compiles attacker-controlled source. Sandbox-escape gadgets such as `{{ ().__class__.__bases__[0].__subclasses__() }}` lead to RCE. Render fixed templates with user data passed as context variables.",
|
|
28810
|
+
file,
|
|
28811
|
+
line: i2 + 1,
|
|
28812
|
+
snippet: line.trim()
|
|
28813
|
+
});
|
|
28814
|
+
}
|
|
28815
|
+
return findings;
|
|
28816
|
+
}
|
|
28817
|
+
function findJsTemplateInjectionSstiFindings(code, file) {
|
|
28818
|
+
const findings = [];
|
|
28819
|
+
if (typeof code !== "string" || code.length === 0) return findings;
|
|
28820
|
+
if (!/\bHandlebars\s*\.\s*compile\s*\(/.test(code) && !/\bejs\s*\.\s*(?:render|compile)\s*\(/.test(code)) {
|
|
28821
|
+
return findings;
|
|
28822
|
+
}
|
|
28823
|
+
if (!/\breq\s*\.\s*(?:query|body|params|headers|cookies)\b/.test(code)) {
|
|
28824
|
+
return findings;
|
|
28825
|
+
}
|
|
28826
|
+
const lines = code.split("\n");
|
|
28827
|
+
const reqExtractRe = /\breq\s*\.\s*(?:query|body|params|headers|cookies)\b/;
|
|
28828
|
+
const taintedVars = /* @__PURE__ */ new Set();
|
|
28829
|
+
for (let pass = 0; pass < 3; pass++) {
|
|
28830
|
+
const before = taintedVars.size;
|
|
28831
|
+
for (let i2 = 0; i2 < lines.length; i2++) {
|
|
28832
|
+
const t = lines[i2].trim();
|
|
28833
|
+
if (t.startsWith("//")) continue;
|
|
28834
|
+
const m = t.match(/^(?:const|let|var)\s+(\w+)(?:\s*:\s*[^=]+)?\s*=\s*(.+?);?$/);
|
|
28835
|
+
if (!m) continue;
|
|
28836
|
+
const lhs = m[1];
|
|
28837
|
+
const rhs = m[2];
|
|
28838
|
+
if (taintedVars.has(lhs)) continue;
|
|
28839
|
+
if (reqExtractRe.test(rhs)) {
|
|
28840
|
+
taintedVars.add(lhs);
|
|
28841
|
+
continue;
|
|
28842
|
+
}
|
|
28843
|
+
for (const v of taintedVars) {
|
|
28844
|
+
if (new RegExp(`\\b${v}\\b`).test(rhs)) {
|
|
28845
|
+
taintedVars.add(lhs);
|
|
28846
|
+
break;
|
|
28847
|
+
}
|
|
28848
|
+
}
|
|
28849
|
+
}
|
|
28850
|
+
if (taintedVars.size === before) break;
|
|
28851
|
+
}
|
|
28852
|
+
if (taintedVars.size === 0) return findings;
|
|
28853
|
+
const callRe = /\b(Handlebars\s*\.\s*compile|ejs\s*\.\s*(?:render|compile))\s*\(\s*([^,)]+)/;
|
|
28854
|
+
const seen = /* @__PURE__ */ new Set();
|
|
28855
|
+
for (let i2 = 0; i2 < lines.length; i2++) {
|
|
28856
|
+
const line = lines[i2];
|
|
28857
|
+
if (line.trim().startsWith("//")) continue;
|
|
28858
|
+
const m = line.match(callRe);
|
|
28859
|
+
if (!m) continue;
|
|
28860
|
+
const arg = m[2].trim();
|
|
28861
|
+
let tainted = reqExtractRe.test(arg);
|
|
28862
|
+
if (!tainted) {
|
|
28863
|
+
for (const v of taintedVars) {
|
|
28864
|
+
if (new RegExp(`\\b${v}\\b`).test(arg)) {
|
|
28865
|
+
tainted = true;
|
|
28866
|
+
break;
|
|
28867
|
+
}
|
|
28868
|
+
}
|
|
28869
|
+
}
|
|
28870
|
+
if (!tainted) continue;
|
|
28871
|
+
if (seen.has(i2 + 1)) continue;
|
|
28872
|
+
seen.add(i2 + 1);
|
|
28873
|
+
findings.push({
|
|
28874
|
+
id: `template_injection-${file}-${i2 + 1}-js-${m[1].replace(/[\s.]/g, "").toLowerCase()}`,
|
|
28875
|
+
pass: "language-sources",
|
|
28876
|
+
category: "security",
|
|
28877
|
+
rule_id: "template_injection",
|
|
28878
|
+
cwe: "CWE-1336",
|
|
28879
|
+
severity: "critical",
|
|
28880
|
+
level: "error",
|
|
28881
|
+
message: "Server-side template injection: compiling/rendering a template whose source is attacker-controlled (`Handlebars.compile(...)` / `ejs.render(...)`) opens helper-shadowing and prototype-gadget RCE paths. Use a fixed template and pass user data as context.",
|
|
28882
|
+
file,
|
|
28883
|
+
line: i2 + 1,
|
|
28884
|
+
snippet: line.trim()
|
|
28885
|
+
});
|
|
28886
|
+
}
|
|
28887
|
+
return findings;
|
|
28888
|
+
}
|
|
28457
28889
|
|
|
28458
28890
|
// src/analysis/passes/sink-filter-pass.ts
|
|
28459
28891
|
var JS_XSS_SANITIZERS = [
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "circle-ir",
|
|
3
|
-
"version": "3.
|
|
3
|
+
"version": "3.138.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",
|