cognium-dev 3.137.0 → 3.139.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/cli.js +629 -1
- package/package.json +2 -2
package/dist/cli.js
CHANGED
|
@@ -22619,6 +22619,12 @@ class LanguageSourcesPass {
|
|
|
22619
22619
|
for (const finding of findGoLdapInjectionFindings(code, graph.ir.meta.file)) {
|
|
22620
22620
|
ctx.addFinding(finding);
|
|
22621
22621
|
}
|
|
22622
|
+
for (const finding of findGoGobDeserializationFindings(code, graph.ir.meta.file)) {
|
|
22623
|
+
ctx.addFinding(finding);
|
|
22624
|
+
}
|
|
22625
|
+
for (const finding of findGoXmlDecoderXxeFindings(code, graph.ir.meta.file)) {
|
|
22626
|
+
ctx.addFinding(finding);
|
|
22627
|
+
}
|
|
22622
22628
|
}
|
|
22623
22629
|
if (language === "python") {
|
|
22624
22630
|
additionalSanitizers.push(...findPythonNetlocAllowlistGuardSanitizers(code));
|
|
@@ -22652,6 +22658,9 @@ class LanguageSourcesPass {
|
|
|
22652
22658
|
for (const finding of findPythonHeaderCrlfInjectionFindings(code, graph.ir.meta.file)) {
|
|
22653
22659
|
ctx.addFinding(finding);
|
|
22654
22660
|
}
|
|
22661
|
+
for (const finding of findPythonJinjaTemplateSstiFindings(code, graph.ir.meta.file)) {
|
|
22662
|
+
ctx.addFinding(finding);
|
|
22663
|
+
}
|
|
22655
22664
|
}
|
|
22656
22665
|
if (language === "rust") {
|
|
22657
22666
|
additionalSanitizers.push(...findRustSetAllowlistGuardSanitizers(code));
|
|
@@ -22715,6 +22724,15 @@ class LanguageSourcesPass {
|
|
|
22715
22724
|
for (const finding of findJsLdapInjectionFindings(code, graph.ir.meta.file)) {
|
|
22716
22725
|
ctx.addFinding(finding);
|
|
22717
22726
|
}
|
|
22727
|
+
for (const finding of findJsJsonParseBodyFindings(code, graph.ir.meta.file)) {
|
|
22728
|
+
ctx.addFinding(finding);
|
|
22729
|
+
}
|
|
22730
|
+
for (const finding of findJsDomXpathInjectionFindings(code, graph.ir.meta.file)) {
|
|
22731
|
+
ctx.addFinding(finding);
|
|
22732
|
+
}
|
|
22733
|
+
for (const finding of findJsTemplateInjectionSstiFindings(code, graph.ir.meta.file)) {
|
|
22734
|
+
ctx.addFinding(finding);
|
|
22735
|
+
}
|
|
22718
22736
|
}
|
|
22719
22737
|
if (language === "java") {
|
|
22720
22738
|
additionalSanitizers.push(...findJavaSafeJsonParseSanitizers(code));
|
|
@@ -27886,6 +27904,466 @@ function findRustLogInjectionFindings(code, file) {
|
|
|
27886
27904
|
}
|
|
27887
27905
|
return findings;
|
|
27888
27906
|
}
|
|
27907
|
+
function findGoGobDeserializationFindings(code, file) {
|
|
27908
|
+
const findings = [];
|
|
27909
|
+
if (typeof code !== "string" || code.length === 0)
|
|
27910
|
+
return findings;
|
|
27911
|
+
if (!/\bgob\s*\.\s*NewDecoder\s*\(/.test(code))
|
|
27912
|
+
return findings;
|
|
27913
|
+
if (!/\*http\.Request\b/.test(code) && !/\bhttp\.HandlerFunc\b/.test(code)) {
|
|
27914
|
+
return findings;
|
|
27915
|
+
}
|
|
27916
|
+
const lines = code.split(`
|
|
27917
|
+
`);
|
|
27918
|
+
const decoderFromBody = new Set;
|
|
27919
|
+
const newDecoderAssignRe = /^(\w+)\s*(?::=|=)\s*gob\s*\.\s*NewDecoder\s*\(\s*([^)]+)\)/;
|
|
27920
|
+
for (const raw of lines) {
|
|
27921
|
+
const t = raw.trim();
|
|
27922
|
+
if (t.startsWith("//"))
|
|
27923
|
+
continue;
|
|
27924
|
+
const m = t.match(newDecoderAssignRe);
|
|
27925
|
+
if (!m)
|
|
27926
|
+
continue;
|
|
27927
|
+
const lhs = m[1];
|
|
27928
|
+
const arg = m[2];
|
|
27929
|
+
if (/\b\w+\s*\.\s*Body\b/.test(arg)) {
|
|
27930
|
+
decoderFromBody.add(lhs);
|
|
27931
|
+
}
|
|
27932
|
+
}
|
|
27933
|
+
const seen = new Set;
|
|
27934
|
+
if (decoderFromBody.size > 0) {
|
|
27935
|
+
for (let i2 = 0;i2 < lines.length; i2++) {
|
|
27936
|
+
const line = lines[i2];
|
|
27937
|
+
if (line.trim().startsWith("//"))
|
|
27938
|
+
continue;
|
|
27939
|
+
const m = line.match(/\b(\w+)\s*\.\s*Decode\s*\(/);
|
|
27940
|
+
if (!m)
|
|
27941
|
+
continue;
|
|
27942
|
+
if (!decoderFromBody.has(m[1]))
|
|
27943
|
+
continue;
|
|
27944
|
+
if (seen.has(i2 + 1))
|
|
27945
|
+
continue;
|
|
27946
|
+
seen.add(i2 + 1);
|
|
27947
|
+
findings.push({
|
|
27948
|
+
id: `insecure_deserialization-${file}-${i2 + 1}-go-gob`,
|
|
27949
|
+
pass: "language-sources",
|
|
27950
|
+
category: "security",
|
|
27951
|
+
rule_id: "insecure_deserialization",
|
|
27952
|
+
cwe: "CWE-502",
|
|
27953
|
+
severity: "critical",
|
|
27954
|
+
level: "error",
|
|
27955
|
+
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).",
|
|
27956
|
+
file,
|
|
27957
|
+
line: i2 + 1,
|
|
27958
|
+
snippet: line.trim()
|
|
27959
|
+
});
|
|
27960
|
+
}
|
|
27961
|
+
}
|
|
27962
|
+
for (let i2 = 0;i2 < lines.length; i2++) {
|
|
27963
|
+
const line = lines[i2];
|
|
27964
|
+
if (line.trim().startsWith("//"))
|
|
27965
|
+
continue;
|
|
27966
|
+
if (!/\bgob\s*\.\s*NewDecoder\s*\(\s*\w+\s*\.\s*Body\s*\)\s*\.\s*Decode\s*\(/.test(line)) {
|
|
27967
|
+
continue;
|
|
27968
|
+
}
|
|
27969
|
+
if (seen.has(i2 + 1))
|
|
27970
|
+
continue;
|
|
27971
|
+
seen.add(i2 + 1);
|
|
27972
|
+
findings.push({
|
|
27973
|
+
id: `insecure_deserialization-${file}-${i2 + 1}-go-gob-inline`,
|
|
27974
|
+
pass: "language-sources",
|
|
27975
|
+
category: "security",
|
|
27976
|
+
rule_id: "insecure_deserialization",
|
|
27977
|
+
cwe: "CWE-502",
|
|
27978
|
+
severity: "critical",
|
|
27979
|
+
level: "error",
|
|
27980
|
+
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).",
|
|
27981
|
+
file,
|
|
27982
|
+
line: i2 + 1,
|
|
27983
|
+
snippet: line.trim()
|
|
27984
|
+
});
|
|
27985
|
+
}
|
|
27986
|
+
return findings;
|
|
27987
|
+
}
|
|
27988
|
+
function findJsJsonParseBodyFindings(code, file) {
|
|
27989
|
+
const findings = [];
|
|
27990
|
+
if (typeof code !== "string" || code.length === 0)
|
|
27991
|
+
return findings;
|
|
27992
|
+
if (!/\bJSON\s*\.\s*parse\s*\(/.test(code))
|
|
27993
|
+
return findings;
|
|
27994
|
+
if (!/\breq\s*\.\s*body\b/.test(code))
|
|
27995
|
+
return findings;
|
|
27996
|
+
const lines = code.split(`
|
|
27997
|
+
`);
|
|
27998
|
+
const reqBodyRe = /\breq\s*\.\s*body\b/;
|
|
27999
|
+
const taintedVars = new Set;
|
|
28000
|
+
for (let pass = 0;pass < 3; pass++) {
|
|
28001
|
+
const before = taintedVars.size;
|
|
28002
|
+
for (let i2 = 0;i2 < lines.length; i2++) {
|
|
28003
|
+
const t = lines[i2].trim();
|
|
28004
|
+
if (t.startsWith("//"))
|
|
28005
|
+
continue;
|
|
28006
|
+
const m = t.match(/^(?:const|let|var)\s+(\w+)(?:\s*:\s*[^=]+)?\s*=\s*(.+?);?$/);
|
|
28007
|
+
if (!m)
|
|
28008
|
+
continue;
|
|
28009
|
+
const lhs = m[1];
|
|
28010
|
+
const rhs = m[2];
|
|
28011
|
+
if (taintedVars.has(lhs))
|
|
28012
|
+
continue;
|
|
28013
|
+
if (reqBodyRe.test(rhs)) {
|
|
28014
|
+
taintedVars.add(lhs);
|
|
28015
|
+
continue;
|
|
28016
|
+
}
|
|
28017
|
+
for (const v of taintedVars) {
|
|
28018
|
+
if (new RegExp(`\\b${v}\\b`).test(rhs)) {
|
|
28019
|
+
taintedVars.add(lhs);
|
|
28020
|
+
break;
|
|
28021
|
+
}
|
|
28022
|
+
}
|
|
28023
|
+
}
|
|
28024
|
+
if (taintedVars.size === before)
|
|
28025
|
+
break;
|
|
28026
|
+
}
|
|
28027
|
+
const callRe = /\bJSON\s*\.\s*parse\s*\(\s*([^)]+?)\s*\)/g;
|
|
28028
|
+
const seen = new Set;
|
|
28029
|
+
for (let i2 = 0;i2 < lines.length; i2++) {
|
|
28030
|
+
const line = lines[i2];
|
|
28031
|
+
if (line.trim().startsWith("//"))
|
|
28032
|
+
continue;
|
|
28033
|
+
callRe.lastIndex = 0;
|
|
28034
|
+
let m;
|
|
28035
|
+
while ((m = callRe.exec(line)) !== null) {
|
|
28036
|
+
const arg = m[1];
|
|
28037
|
+
let tainted = reqBodyRe.test(arg);
|
|
28038
|
+
if (!tainted) {
|
|
28039
|
+
for (const v of taintedVars) {
|
|
28040
|
+
if (new RegExp(`\\b${v}\\b`).test(arg)) {
|
|
28041
|
+
tainted = true;
|
|
28042
|
+
break;
|
|
28043
|
+
}
|
|
28044
|
+
}
|
|
28045
|
+
}
|
|
28046
|
+
if (!tainted)
|
|
28047
|
+
continue;
|
|
28048
|
+
if (seen.has(i2 + 1))
|
|
28049
|
+
continue;
|
|
28050
|
+
seen.add(i2 + 1);
|
|
28051
|
+
findings.push({
|
|
28052
|
+
id: `insecure_deserialization-${file}-${i2 + 1}-js-jsonparse-body`,
|
|
28053
|
+
pass: "language-sources",
|
|
28054
|
+
category: "security",
|
|
28055
|
+
rule_id: "insecure_deserialization",
|
|
28056
|
+
cwe: "CWE-502",
|
|
28057
|
+
severity: "high",
|
|
28058
|
+
level: "warning",
|
|
28059
|
+
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.",
|
|
28060
|
+
file,
|
|
28061
|
+
line: i2 + 1,
|
|
28062
|
+
snippet: line.trim()
|
|
28063
|
+
});
|
|
28064
|
+
}
|
|
28065
|
+
}
|
|
28066
|
+
return findings;
|
|
28067
|
+
}
|
|
28068
|
+
function findJsDomXpathInjectionFindings(code, file) {
|
|
28069
|
+
const findings = [];
|
|
28070
|
+
if (typeof code !== "string" || code.length === 0)
|
|
28071
|
+
return findings;
|
|
28072
|
+
if (!/\.\s*evaluate\s*\(/.test(code))
|
|
28073
|
+
return findings;
|
|
28074
|
+
if (!/\bXPathResult\b/.test(code))
|
|
28075
|
+
return findings;
|
|
28076
|
+
const browserSourceRe = /\b(?:location\s*\.\s*(?:search|hash|href)|URLSearchParams|window\s*\.\s*name|document\s*\.\s*cookie)\b/;
|
|
28077
|
+
if (!browserSourceRe.test(code))
|
|
28078
|
+
return findings;
|
|
28079
|
+
const lines = code.split(`
|
|
28080
|
+
`);
|
|
28081
|
+
const taintedVars = new Set;
|
|
28082
|
+
for (let pass = 0;pass < 3; pass++) {
|
|
28083
|
+
const before = taintedVars.size;
|
|
28084
|
+
for (let i2 = 0;i2 < lines.length; i2++) {
|
|
28085
|
+
const t = lines[i2].trim();
|
|
28086
|
+
if (t.startsWith("//"))
|
|
28087
|
+
continue;
|
|
28088
|
+
const m = t.match(/^(?:const|let|var)\s+(\w+)(?:\s*:\s*[^=]+)?\s*=\s*(.+?);?$/);
|
|
28089
|
+
if (!m)
|
|
28090
|
+
continue;
|
|
28091
|
+
const lhs = m[1];
|
|
28092
|
+
const rhs = m[2];
|
|
28093
|
+
if (taintedVars.has(lhs))
|
|
28094
|
+
continue;
|
|
28095
|
+
if (browserSourceRe.test(rhs)) {
|
|
28096
|
+
taintedVars.add(lhs);
|
|
28097
|
+
continue;
|
|
28098
|
+
}
|
|
28099
|
+
for (const v of taintedVars) {
|
|
28100
|
+
if (new RegExp(`\\b${v}\\b`).test(rhs)) {
|
|
28101
|
+
taintedVars.add(lhs);
|
|
28102
|
+
break;
|
|
28103
|
+
}
|
|
28104
|
+
}
|
|
28105
|
+
}
|
|
28106
|
+
if (taintedVars.size === before)
|
|
28107
|
+
break;
|
|
28108
|
+
}
|
|
28109
|
+
if (taintedVars.size === 0)
|
|
28110
|
+
return findings;
|
|
28111
|
+
const evalRe = /\.\s*evaluate\s*\(\s*([^,)]+)/;
|
|
28112
|
+
const seen = new Set;
|
|
28113
|
+
for (let i2 = 0;i2 < lines.length; i2++) {
|
|
28114
|
+
const line = lines[i2];
|
|
28115
|
+
if (line.trim().startsWith("//"))
|
|
28116
|
+
continue;
|
|
28117
|
+
const m = line.match(evalRe);
|
|
28118
|
+
if (!m)
|
|
28119
|
+
continue;
|
|
28120
|
+
const arg = m[1].trim();
|
|
28121
|
+
let tainted = false;
|
|
28122
|
+
for (const v of taintedVars) {
|
|
28123
|
+
if (new RegExp(`\\b${v}\\b`).test(arg)) {
|
|
28124
|
+
tainted = true;
|
|
28125
|
+
break;
|
|
28126
|
+
}
|
|
28127
|
+
}
|
|
28128
|
+
if (!tainted)
|
|
28129
|
+
continue;
|
|
28130
|
+
if (seen.has(i2 + 1))
|
|
28131
|
+
continue;
|
|
28132
|
+
seen.add(i2 + 1);
|
|
28133
|
+
findings.push({
|
|
28134
|
+
id: `xpath_injection-${file}-${i2 + 1}-js-dom-evaluate`,
|
|
28135
|
+
pass: "language-sources",
|
|
28136
|
+
category: "security",
|
|
28137
|
+
rule_id: "xpath_injection",
|
|
28138
|
+
cwe: "CWE-643",
|
|
28139
|
+
severity: "high",
|
|
28140
|
+
level: "warning",
|
|
28141
|
+
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.",
|
|
28142
|
+
file,
|
|
28143
|
+
line: i2 + 1,
|
|
28144
|
+
snippet: line.trim()
|
|
28145
|
+
});
|
|
28146
|
+
}
|
|
28147
|
+
return findings;
|
|
28148
|
+
}
|
|
28149
|
+
function findGoXmlDecoderXxeFindings(code, file) {
|
|
28150
|
+
const findings = [];
|
|
28151
|
+
if (typeof code !== "string" || code.length === 0)
|
|
28152
|
+
return findings;
|
|
28153
|
+
if (!/\bxml\s*\.\s*NewDecoder\s*\(/.test(code))
|
|
28154
|
+
return findings;
|
|
28155
|
+
if (!/\*http\.Request\b/.test(code) && !/\bhttp\.HandlerFunc\b/.test(code)) {
|
|
28156
|
+
return findings;
|
|
28157
|
+
}
|
|
28158
|
+
const lines = code.split(`
|
|
28159
|
+
`);
|
|
28160
|
+
const decoderFromBody = new Map;
|
|
28161
|
+
for (let i2 = 0;i2 < lines.length; i2++) {
|
|
28162
|
+
const t = lines[i2].trim();
|
|
28163
|
+
if (t.startsWith("//"))
|
|
28164
|
+
continue;
|
|
28165
|
+
const m = t.match(/^(\w+)\s*(?::=|=)\s*xml\s*\.\s*NewDecoder\s*\(\s*([^)]+)\)/);
|
|
28166
|
+
if (!m)
|
|
28167
|
+
continue;
|
|
28168
|
+
if (/\b\w+\s*\.\s*Body\b/.test(m[2]))
|
|
28169
|
+
decoderFromBody.set(m[1], i2);
|
|
28170
|
+
}
|
|
28171
|
+
if (decoderFromBody.size === 0)
|
|
28172
|
+
return findings;
|
|
28173
|
+
const seen = new Set;
|
|
28174
|
+
for (let i2 = 0;i2 < lines.length; i2++) {
|
|
28175
|
+
const line = lines[i2];
|
|
28176
|
+
if (line.trim().startsWith("//"))
|
|
28177
|
+
continue;
|
|
28178
|
+
const m = line.match(/\b(\w+)\s*\.\s*(?:Strict\s*=\s*false|Entity\s*=)\b/);
|
|
28179
|
+
if (!m)
|
|
28180
|
+
continue;
|
|
28181
|
+
if (!decoderFromBody.has(m[1]))
|
|
28182
|
+
continue;
|
|
28183
|
+
if (seen.has(i2 + 1))
|
|
28184
|
+
continue;
|
|
28185
|
+
seen.add(i2 + 1);
|
|
28186
|
+
findings.push({
|
|
28187
|
+
id: `xml_entity_expansion-${file}-${i2 + 1}-go-xml-decoder`,
|
|
28188
|
+
pass: "language-sources",
|
|
28189
|
+
category: "security",
|
|
28190
|
+
rule_id: "xml_entity_expansion",
|
|
28191
|
+
cwe: "CWE-611",
|
|
28192
|
+
severity: "high",
|
|
28193
|
+
level: "warning",
|
|
28194
|
+
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.",
|
|
28195
|
+
file,
|
|
28196
|
+
line: i2 + 1,
|
|
28197
|
+
snippet: line.trim()
|
|
28198
|
+
});
|
|
28199
|
+
}
|
|
28200
|
+
return findings;
|
|
28201
|
+
}
|
|
28202
|
+
function findPythonJinjaTemplateSstiFindings(code, file) {
|
|
28203
|
+
const findings = [];
|
|
28204
|
+
if (typeof code !== "string" || code.length === 0)
|
|
28205
|
+
return findings;
|
|
28206
|
+
if (!/\bfrom\s+jinja2\s+import\s+[^#\n]*\bTemplate\b/.test(code) && !/\bjinja2\s*\.\s*Template\b/.test(code)) {
|
|
28207
|
+
return findings;
|
|
28208
|
+
}
|
|
28209
|
+
const reqExtractRe = /\brequest\s*\.\s*(?:args|form|values|json|data|cookies|headers)\b/;
|
|
28210
|
+
if (!reqExtractRe.test(code))
|
|
28211
|
+
return findings;
|
|
28212
|
+
const lines = code.split(`
|
|
28213
|
+
`);
|
|
28214
|
+
const taintedVars = new Set;
|
|
28215
|
+
for (let pass = 0;pass < 3; pass++) {
|
|
28216
|
+
const before = taintedVars.size;
|
|
28217
|
+
for (let i2 = 0;i2 < lines.length; i2++) {
|
|
28218
|
+
const t = lines[i2].trim();
|
|
28219
|
+
if (t.startsWith("#"))
|
|
28220
|
+
continue;
|
|
28221
|
+
const m = t.match(/^(\w+)\s*=\s*(.+?)$/);
|
|
28222
|
+
if (!m)
|
|
28223
|
+
continue;
|
|
28224
|
+
const lhs = m[1];
|
|
28225
|
+
const rhs = m[2];
|
|
28226
|
+
if (taintedVars.has(lhs))
|
|
28227
|
+
continue;
|
|
28228
|
+
if (reqExtractRe.test(rhs)) {
|
|
28229
|
+
taintedVars.add(lhs);
|
|
28230
|
+
continue;
|
|
28231
|
+
}
|
|
28232
|
+
for (const v of taintedVars) {
|
|
28233
|
+
if (new RegExp(`\\b${v}\\b`).test(rhs)) {
|
|
28234
|
+
taintedVars.add(lhs);
|
|
28235
|
+
break;
|
|
28236
|
+
}
|
|
28237
|
+
}
|
|
28238
|
+
}
|
|
28239
|
+
if (taintedVars.size === before)
|
|
28240
|
+
break;
|
|
28241
|
+
}
|
|
28242
|
+
if (taintedVars.size === 0)
|
|
28243
|
+
return findings;
|
|
28244
|
+
const ctorRe = /\b(?:jinja2\s*\.\s*)?Template\s*\(\s*([^)]+?)\s*\)/;
|
|
28245
|
+
const seen = new Set;
|
|
28246
|
+
for (let i2 = 0;i2 < lines.length; i2++) {
|
|
28247
|
+
const line = lines[i2];
|
|
28248
|
+
if (line.trim().startsWith("#"))
|
|
28249
|
+
continue;
|
|
28250
|
+
const m = line.match(ctorRe);
|
|
28251
|
+
if (!m)
|
|
28252
|
+
continue;
|
|
28253
|
+
const arg = m[1].trim();
|
|
28254
|
+
let tainted = reqExtractRe.test(arg);
|
|
28255
|
+
if (!tainted) {
|
|
28256
|
+
for (const v of taintedVars) {
|
|
28257
|
+
if (new RegExp(`\\b${v}\\b`).test(arg)) {
|
|
28258
|
+
tainted = true;
|
|
28259
|
+
break;
|
|
28260
|
+
}
|
|
28261
|
+
}
|
|
28262
|
+
}
|
|
28263
|
+
if (!tainted)
|
|
28264
|
+
continue;
|
|
28265
|
+
if (seen.has(i2 + 1))
|
|
28266
|
+
continue;
|
|
28267
|
+
seen.add(i2 + 1);
|
|
28268
|
+
findings.push({
|
|
28269
|
+
id: `template_injection-${file}-${i2 + 1}-py-jinja-template`,
|
|
28270
|
+
pass: "language-sources",
|
|
28271
|
+
category: "security",
|
|
28272
|
+
rule_id: "template_injection",
|
|
28273
|
+
cwe: "CWE-1336",
|
|
28274
|
+
severity: "critical",
|
|
28275
|
+
level: "error",
|
|
28276
|
+
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.",
|
|
28277
|
+
file,
|
|
28278
|
+
line: i2 + 1,
|
|
28279
|
+
snippet: line.trim()
|
|
28280
|
+
});
|
|
28281
|
+
}
|
|
28282
|
+
return findings;
|
|
28283
|
+
}
|
|
28284
|
+
function findJsTemplateInjectionSstiFindings(code, file) {
|
|
28285
|
+
const findings = [];
|
|
28286
|
+
if (typeof code !== "string" || code.length === 0)
|
|
28287
|
+
return findings;
|
|
28288
|
+
if (!/\bHandlebars\s*\.\s*compile\s*\(/.test(code) && !/\bejs\s*\.\s*(?:render|compile)\s*\(/.test(code)) {
|
|
28289
|
+
return findings;
|
|
28290
|
+
}
|
|
28291
|
+
if (!/\breq\s*\.\s*(?:query|body|params|headers|cookies)\b/.test(code)) {
|
|
28292
|
+
return findings;
|
|
28293
|
+
}
|
|
28294
|
+
const lines = code.split(`
|
|
28295
|
+
`);
|
|
28296
|
+
const reqExtractRe = /\breq\s*\.\s*(?:query|body|params|headers|cookies)\b/;
|
|
28297
|
+
const taintedVars = new Set;
|
|
28298
|
+
for (let pass = 0;pass < 3; pass++) {
|
|
28299
|
+
const before = taintedVars.size;
|
|
28300
|
+
for (let i2 = 0;i2 < lines.length; i2++) {
|
|
28301
|
+
const t = lines[i2].trim();
|
|
28302
|
+
if (t.startsWith("//"))
|
|
28303
|
+
continue;
|
|
28304
|
+
const m = t.match(/^(?:const|let|var)\s+(\w+)(?:\s*:\s*[^=]+)?\s*=\s*(.+?);?$/);
|
|
28305
|
+
if (!m)
|
|
28306
|
+
continue;
|
|
28307
|
+
const lhs = m[1];
|
|
28308
|
+
const rhs = m[2];
|
|
28309
|
+
if (taintedVars.has(lhs))
|
|
28310
|
+
continue;
|
|
28311
|
+
if (reqExtractRe.test(rhs)) {
|
|
28312
|
+
taintedVars.add(lhs);
|
|
28313
|
+
continue;
|
|
28314
|
+
}
|
|
28315
|
+
for (const v of taintedVars) {
|
|
28316
|
+
if (new RegExp(`\\b${v}\\b`).test(rhs)) {
|
|
28317
|
+
taintedVars.add(lhs);
|
|
28318
|
+
break;
|
|
28319
|
+
}
|
|
28320
|
+
}
|
|
28321
|
+
}
|
|
28322
|
+
if (taintedVars.size === before)
|
|
28323
|
+
break;
|
|
28324
|
+
}
|
|
28325
|
+
if (taintedVars.size === 0)
|
|
28326
|
+
return findings;
|
|
28327
|
+
const callRe = /\b(Handlebars\s*\.\s*compile|ejs\s*\.\s*(?:render|compile))\s*\(\s*([^,)]+)/;
|
|
28328
|
+
const seen = new Set;
|
|
28329
|
+
for (let i2 = 0;i2 < lines.length; i2++) {
|
|
28330
|
+
const line = lines[i2];
|
|
28331
|
+
if (line.trim().startsWith("//"))
|
|
28332
|
+
continue;
|
|
28333
|
+
const m = line.match(callRe);
|
|
28334
|
+
if (!m)
|
|
28335
|
+
continue;
|
|
28336
|
+
const arg = m[2].trim();
|
|
28337
|
+
let tainted = reqExtractRe.test(arg);
|
|
28338
|
+
if (!tainted) {
|
|
28339
|
+
for (const v of taintedVars) {
|
|
28340
|
+
if (new RegExp(`\\b${v}\\b`).test(arg)) {
|
|
28341
|
+
tainted = true;
|
|
28342
|
+
break;
|
|
28343
|
+
}
|
|
28344
|
+
}
|
|
28345
|
+
}
|
|
28346
|
+
if (!tainted)
|
|
28347
|
+
continue;
|
|
28348
|
+
if (seen.has(i2 + 1))
|
|
28349
|
+
continue;
|
|
28350
|
+
seen.add(i2 + 1);
|
|
28351
|
+
findings.push({
|
|
28352
|
+
id: `template_injection-${file}-${i2 + 1}-js-${m[1].replace(/[\s.]/g, "").toLowerCase()}`,
|
|
28353
|
+
pass: "language-sources",
|
|
28354
|
+
category: "security",
|
|
28355
|
+
rule_id: "template_injection",
|
|
28356
|
+
cwe: "CWE-1336",
|
|
28357
|
+
severity: "critical",
|
|
28358
|
+
level: "error",
|
|
28359
|
+
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.",
|
|
28360
|
+
file,
|
|
28361
|
+
line: i2 + 1,
|
|
28362
|
+
snippet: line.trim()
|
|
28363
|
+
});
|
|
28364
|
+
}
|
|
28365
|
+
return findings;
|
|
28366
|
+
}
|
|
27889
28367
|
|
|
27890
28368
|
// ../circle-ir/dist/analysis/passes/sink-filter-pass.js
|
|
27891
28369
|
var JS_XSS_SANITIZERS = [
|
|
@@ -37614,6 +38092,154 @@ class UnrestrictedFileUploadPass {
|
|
|
37614
38092
|
}
|
|
37615
38093
|
}
|
|
37616
38094
|
|
|
38095
|
+
// ../circle-ir/dist/analysis/passes/missing-sanitizer-gate-pass.js
|
|
38096
|
+
var HTML_OUTPUT_SINKS = new Set([
|
|
38097
|
+
"addAttribute",
|
|
38098
|
+
"setAttribute",
|
|
38099
|
+
"write",
|
|
38100
|
+
"writeAttribute",
|
|
38101
|
+
"writeRaw",
|
|
38102
|
+
"writeStartElement",
|
|
38103
|
+
"writeEndElement",
|
|
38104
|
+
"writeCharacters",
|
|
38105
|
+
"printXML",
|
|
38106
|
+
"printXMLElement",
|
|
38107
|
+
"printXMLStartElement",
|
|
38108
|
+
"printXMLEndElement",
|
|
38109
|
+
"printRaw",
|
|
38110
|
+
"print",
|
|
38111
|
+
"println",
|
|
38112
|
+
"printf",
|
|
38113
|
+
"append",
|
|
38114
|
+
"format"
|
|
38115
|
+
]);
|
|
38116
|
+
var SANITIZER_METHODS2 = new Set([
|
|
38117
|
+
"isAttributeAllowed",
|
|
38118
|
+
"isElementAllowed",
|
|
38119
|
+
"isSafe",
|
|
38120
|
+
"sanitize",
|
|
38121
|
+
"clean",
|
|
38122
|
+
"escape",
|
|
38123
|
+
"encode",
|
|
38124
|
+
"forHtml",
|
|
38125
|
+
"forHtmlAttribute",
|
|
38126
|
+
"forHtmlContent",
|
|
38127
|
+
"encodeForHTML",
|
|
38128
|
+
"encodeForHTMLAttribute",
|
|
38129
|
+
"escapeHtml",
|
|
38130
|
+
"escapeHtml4",
|
|
38131
|
+
"escapeXml",
|
|
38132
|
+
"htmlEscape",
|
|
38133
|
+
"xmlEscape"
|
|
38134
|
+
]);
|
|
38135
|
+
var SANITIZER_PREFIX_RE = /^(?:clean|sanitize|escape|encode)[A-Z]/;
|
|
38136
|
+
function isSanitizerCall(methodName) {
|
|
38137
|
+
return SANITIZER_METHODS2.has(methodName) || SANITIZER_PREFIX_RE.test(methodName);
|
|
38138
|
+
}
|
|
38139
|
+
var STRING_PARAM_NAME_PATTERN = /attr|attribute|html|xml|body|content/i;
|
|
38140
|
+
|
|
38141
|
+
class MissingSanitizerGatePass {
|
|
38142
|
+
name = "missing-sanitizer-gate";
|
|
38143
|
+
category = "security";
|
|
38144
|
+
run(ctx) {
|
|
38145
|
+
const { graph, language } = ctx;
|
|
38146
|
+
if (language !== "java")
|
|
38147
|
+
return { findings: 0 };
|
|
38148
|
+
const { cfg, calls, types } = graph.ir;
|
|
38149
|
+
if (cfg.blocks.length === 0 || cfg.edges.length === 0)
|
|
38150
|
+
return { findings: 0 };
|
|
38151
|
+
if (calls.length === 0)
|
|
38152
|
+
return { findings: 0 };
|
|
38153
|
+
const file = graph.ir.meta.file;
|
|
38154
|
+
const sinkCalls = [];
|
|
38155
|
+
const sanitizerLines = [];
|
|
38156
|
+
for (const call of calls) {
|
|
38157
|
+
if (HTML_OUTPUT_SINKS.has(call.method_name)) {
|
|
38158
|
+
sinkCalls.push({ line: call.location.line, method: call.method_name });
|
|
38159
|
+
}
|
|
38160
|
+
if (isSanitizerCall(call.method_name)) {
|
|
38161
|
+
sanitizerLines.push(call.location.line);
|
|
38162
|
+
}
|
|
38163
|
+
}
|
|
38164
|
+
if (sinkCalls.length === 0)
|
|
38165
|
+
return { findings: 0 };
|
|
38166
|
+
const blockContainingLine = (line) => cfg.blocks.find((b) => b.start_line <= line && line <= b.end_line) ?? null;
|
|
38167
|
+
const domByMethodKey = new Map;
|
|
38168
|
+
const getDomForMethod = (methodKey, method) => {
|
|
38169
|
+
if (domByMethodKey.has(methodKey))
|
|
38170
|
+
return domByMethodKey.get(methodKey) ?? null;
|
|
38171
|
+
const entry = cfg.blocks.find((b) => b.type === "entry" && b.start_line >= method.start_line && b.end_line <= method.end_line) ?? cfg.blocks.find((b) => b.start_line >= method.start_line && b.end_line <= method.end_line);
|
|
38172
|
+
const dom = entry ? new DominatorGraph2(cfg, entry.id) : null;
|
|
38173
|
+
domByMethodKey.set(methodKey, dom);
|
|
38174
|
+
return dom;
|
|
38175
|
+
};
|
|
38176
|
+
const reportedMethods = new Set;
|
|
38177
|
+
let count = 0;
|
|
38178
|
+
for (const sink of sinkCalls) {
|
|
38179
|
+
const sinkBlock = blockContainingLine(sink.line);
|
|
38180
|
+
if (!sinkBlock)
|
|
38181
|
+
continue;
|
|
38182
|
+
const methodInfo = graph.methodAtLine(sink.line);
|
|
38183
|
+
if (!methodInfo)
|
|
38184
|
+
continue;
|
|
38185
|
+
const methodKey = `${methodInfo.type.name}::${methodInfo.method.name}`;
|
|
38186
|
+
if (reportedMethods.has(methodKey))
|
|
38187
|
+
continue;
|
|
38188
|
+
const tier = classifyEntryPointTier(methodInfo.method, methodInfo.type, {
|
|
38189
|
+
types,
|
|
38190
|
+
language
|
|
38191
|
+
});
|
|
38192
|
+
if (tier === "TIER_1_ENTRY_POINT")
|
|
38193
|
+
continue;
|
|
38194
|
+
if (!methodAcceptsHtmlShapedParam(methodInfo.method))
|
|
38195
|
+
continue;
|
|
38196
|
+
const dom = getDomForMethod(methodKey, methodInfo.method);
|
|
38197
|
+
if (!dom)
|
|
38198
|
+
continue;
|
|
38199
|
+
const { start_line, end_line } = methodInfo.method;
|
|
38200
|
+
const sanitizersInMethod = sanitizerLines.filter((l) => l >= start_line && l <= end_line);
|
|
38201
|
+
const dominated = sanitizersInMethod.some((sanLine) => {
|
|
38202
|
+
const sanBlock = blockContainingLine(sanLine);
|
|
38203
|
+
return sanBlock !== null && dom.dominates(sanBlock.id, sinkBlock.id);
|
|
38204
|
+
});
|
|
38205
|
+
if (!dominated) {
|
|
38206
|
+
reportedMethods.add(methodKey);
|
|
38207
|
+
count++;
|
|
38208
|
+
ctx.addFinding({
|
|
38209
|
+
id: `missing-sanitizer-gate-${file}-${sink.line}`,
|
|
38210
|
+
pass: this.name,
|
|
38211
|
+
category: this.category,
|
|
38212
|
+
rule_id: this.name,
|
|
38213
|
+
cwe: "CWE-79",
|
|
38214
|
+
severity: "medium",
|
|
38215
|
+
level: "note",
|
|
38216
|
+
confidence: "medium",
|
|
38217
|
+
message: `HTML output sink \`${sink.method}()\` at line ${sink.line} is not ` + `dominated by any sanitizer call in method \`${methodInfo.method.name}\`. ` + `Caller-supplied attribute/HTML values may reach the sink unescaped.`,
|
|
38218
|
+
file,
|
|
38219
|
+
line: sink.line,
|
|
38220
|
+
fix: `Gate the sink on all paths with an allow-list check ` + `(e.g. \`isAttributeAllowed(...)\`) or wrap the argument in an HTML/XML ` + `escaper (\`escapeHtml\`, \`encodeForHTMLAttribute\`, \`forHtmlAttribute\`).`,
|
|
38221
|
+
evidence: { sink: sink.method, method: methodInfo.method.name }
|
|
38222
|
+
});
|
|
38223
|
+
}
|
|
38224
|
+
}
|
|
38225
|
+
return { findings: count };
|
|
38226
|
+
}
|
|
38227
|
+
}
|
|
38228
|
+
function methodAcceptsHtmlShapedParam(method) {
|
|
38229
|
+
for (const p of method.parameters) {
|
|
38230
|
+
if (!p.type)
|
|
38231
|
+
continue;
|
|
38232
|
+
const type = p.type.replace(/\s+/g, "");
|
|
38233
|
+
if (type === "Attributes")
|
|
38234
|
+
return true;
|
|
38235
|
+
if (type.startsWith("Map<String,String"))
|
|
38236
|
+
return true;
|
|
38237
|
+
if (type === "String" && STRING_PARAM_NAME_PATTERN.test(p.name))
|
|
38238
|
+
return true;
|
|
38239
|
+
}
|
|
38240
|
+
return false;
|
|
38241
|
+
}
|
|
38242
|
+
|
|
37617
38243
|
// ../circle-ir/dist/analysis/passes/plaintext-password-storage-pass.js
|
|
37618
38244
|
function isWriteStorageCall(call, language) {
|
|
37619
38245
|
const method = call.method_name ?? "";
|
|
@@ -40200,6 +40826,8 @@ async function analyze(code, filePath, language, options = {}) {
|
|
|
40200
40826
|
pipeline.add(new InfoDisclosureStacktracePass);
|
|
40201
40827
|
if (!disabledPasses.has("unrestricted-file-upload"))
|
|
40202
40828
|
pipeline.add(new UnrestrictedFileUploadPass);
|
|
40829
|
+
if (!disabledPasses.has("missing-sanitizer-gate"))
|
|
40830
|
+
pipeline.add(new MissingSanitizerGatePass);
|
|
40203
40831
|
const { results, findings } = pipeline.run(graph, code, language, config);
|
|
40204
40832
|
const sinkFilter = results.get("sink-filter");
|
|
40205
40833
|
const interProc = results.get("interprocedural");
|
|
@@ -41013,7 +41641,7 @@ var colors = {
|
|
|
41013
41641
|
};
|
|
41014
41642
|
|
|
41015
41643
|
// src/version.ts
|
|
41016
|
-
var version = "3.
|
|
41644
|
+
var version = "3.139.0";
|
|
41017
41645
|
|
|
41018
41646
|
// src/formatters.ts
|
|
41019
41647
|
var LIBRARY_API_SURFACE_TAG2 = "library-api-surface:caller-responsibility";
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "cognium-dev",
|
|
3
|
-
"version": "3.
|
|
3
|
+
"version": "3.139.0",
|
|
4
4
|
"description": "Static Application Security Testing CLI for detecting security vulnerabilities via taint tracking",
|
|
5
5
|
"main": "dist/index.js",
|
|
6
6
|
"types": "dist/index.d.ts",
|
|
@@ -66,7 +66,7 @@
|
|
|
66
66
|
},
|
|
67
67
|
"dependencies": {
|
|
68
68
|
"@cognium/project-profile-detect": "^1.1.0",
|
|
69
|
-
"circle-ir": "^3.
|
|
69
|
+
"circle-ir": "^3.139.0"
|
|
70
70
|
},
|
|
71
71
|
"devDependencies": {
|
|
72
72
|
"@types/node": "^25.5.0",
|