cognium-dev 3.129.0 → 3.132.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 +772 -14
- package/package.json +2 -2
package/dist/cli.js
CHANGED
|
@@ -10902,8 +10902,8 @@ var DEFAULT_SINKS = [
|
|
|
10902
10902
|
{ method: "unmarshal", class: "Unmarshaller", type: "xxe", cwe: "CWE-611", severity: "high", arg_positions: [0] },
|
|
10903
10903
|
{ method: "newTransformer", class: "TransformerFactory", type: "xxe", cwe: "CWE-611", severity: "high", arg_positions: [0] },
|
|
10904
10904
|
{ method: "transform", class: "Transformer", type: "xxe", cwe: "CWE-611", severity: "high", arg_positions: [0] },
|
|
10905
|
-
{ method: "sendRedirect", class: "HttpServletResponse", type: "
|
|
10906
|
-
{ method: "sendRedirect", type: "
|
|
10905
|
+
{ method: "sendRedirect", class: "HttpServletResponse", type: "open_redirect", cwe: "CWE-601", severity: "high", arg_positions: [0] },
|
|
10906
|
+
{ method: "sendRedirect", type: "open_redirect", cwe: "CWE-601", severity: "high", arg_positions: [0] },
|
|
10907
10907
|
{ method: "openConnection", class: "URL", type: "ssrf", cwe: "CWE-918", severity: "high", arg_positions: [] },
|
|
10908
10908
|
{ method: "openStream", class: "URL", type: "ssrf", cwe: "CWE-918", severity: "high", arg_positions: [] },
|
|
10909
10909
|
{ method: "execute", class: "HttpClient", type: "ssrf", cwe: "CWE-918", severity: "high", arg_positions: [0] },
|
|
@@ -13400,19 +13400,19 @@ function formatCallCode(call) {
|
|
|
13400
13400
|
// ../circle-ir/dist/analysis/findings.js
|
|
13401
13401
|
function canSourceReachSink(sourceType, sinkType) {
|
|
13402
13402
|
const sourceToSinkMapping = {
|
|
13403
|
-
http_param: ["sql_injection", "command_injection", "path_traversal", "xss", "xpath_injection", "ldap_injection", "ssrf", "mybatis_mapper_call", "code_injection", "crlf", "mass_assignment"],
|
|
13404
|
-
http_body: ["sql_injection", "command_injection", "deserialization", "xxe", "xss", "code_injection", "mybatis_mapper_call", "crlf", "mass_assignment"],
|
|
13405
|
-
http_header: ["sql_injection", "xss", "ssrf", "mybatis_mapper_call", "code_injection", "crlf"],
|
|
13406
|
-
http_cookie: ["sql_injection", "xss", "mybatis_mapper_call", "code_injection", "crlf"],
|
|
13407
|
-
http_path: ["path_traversal", "sql_injection", "ssrf", "mybatis_mapper_call"],
|
|
13408
|
-
http_query: ["sql_injection", "command_injection", "xss", "ssrf", "mybatis_mapper_call", "code_injection", "crlf", "mass_assignment"],
|
|
13403
|
+
http_param: ["sql_injection", "command_injection", "path_traversal", "xss", "xpath_injection", "ldap_injection", "ssrf", "mybatis_mapper_call", "code_injection", "crlf", "mass_assignment", "open_redirect"],
|
|
13404
|
+
http_body: ["sql_injection", "command_injection", "deserialization", "xxe", "xss", "code_injection", "mybatis_mapper_call", "crlf", "mass_assignment", "open_redirect"],
|
|
13405
|
+
http_header: ["sql_injection", "xss", "ssrf", "mybatis_mapper_call", "code_injection", "crlf", "open_redirect"],
|
|
13406
|
+
http_cookie: ["sql_injection", "xss", "mybatis_mapper_call", "code_injection", "crlf", "open_redirect"],
|
|
13407
|
+
http_path: ["path_traversal", "sql_injection", "ssrf", "mybatis_mapper_call", "open_redirect"],
|
|
13408
|
+
http_query: ["sql_injection", "command_injection", "xss", "ssrf", "mybatis_mapper_call", "code_injection", "crlf", "mass_assignment", "open_redirect"],
|
|
13409
13409
|
io_input: ["command_injection", "path_traversal", "deserialization", "xxe", "code_injection", "xss", "ssrf"],
|
|
13410
13410
|
env_input: ["command_injection", "path_traversal"],
|
|
13411
13411
|
db_input: ["xss", "sql_injection"],
|
|
13412
13412
|
file_input: ["deserialization", "xxe", "path_traversal", "command_injection", "code_injection"],
|
|
13413
13413
|
network_input: ["sql_injection", "command_injection", "xss", "ssrf"],
|
|
13414
13414
|
config_param: ["sql_injection", "command_injection", "path_traversal", "xss", "ssrf"],
|
|
13415
|
-
interprocedural_param: ["sql_injection", "command_injection", "path_traversal", "xss", "xpath_injection", "ldap_injection", "ssrf", "code_injection", "mybatis_mapper_call", "crlf", "mass_assignment"],
|
|
13415
|
+
interprocedural_param: ["sql_injection", "command_injection", "path_traversal", "xss", "xpath_injection", "ldap_injection", "ssrf", "code_injection", "mybatis_mapper_call", "crlf", "mass_assignment", "open_redirect"],
|
|
13416
13416
|
plugin_param: ["sql_injection", "command_injection", "path_traversal", "xss", "code_injection"]
|
|
13417
13417
|
};
|
|
13418
13418
|
const validSinks = sourceToSinkMapping[sourceType];
|
|
@@ -22604,6 +22604,12 @@ class LanguageSourcesPass {
|
|
|
22604
22604
|
for (const finding of goMisconfigFindings) {
|
|
22605
22605
|
ctx.addFinding(finding);
|
|
22606
22606
|
}
|
|
22607
|
+
for (const finding of findGoXssFindings(code, graph.ir.meta.file)) {
|
|
22608
|
+
ctx.addFinding(finding);
|
|
22609
|
+
}
|
|
22610
|
+
for (const finding of findGoLocationHeaderOpenRedirectFindings(code, graph.ir.meta.file)) {
|
|
22611
|
+
ctx.addFinding(finding);
|
|
22612
|
+
}
|
|
22607
22613
|
}
|
|
22608
22614
|
if (language === "python") {
|
|
22609
22615
|
additionalSanitizers.push(...findPythonNetlocAllowlistGuardSanitizers(code));
|
|
@@ -22616,6 +22622,15 @@ class LanguageSourcesPass {
|
|
|
22616
22622
|
for (const finding of pyMisconfigFindings) {
|
|
22617
22623
|
ctx.addFinding(finding);
|
|
22618
22624
|
}
|
|
22625
|
+
for (const finding of findPythonFlaskStringConcatXssFindings(code, graph.ir.meta.file)) {
|
|
22626
|
+
ctx.addFinding(finding);
|
|
22627
|
+
}
|
|
22628
|
+
for (const finding of findPythonJinjaMarkupXssFindings(code, graph.ir.meta.file)) {
|
|
22629
|
+
ctx.addFinding(finding);
|
|
22630
|
+
}
|
|
22631
|
+
for (const finding of findPythonHeadersSubscriptOpenRedirectFindings(code, graph.ir.meta.file)) {
|
|
22632
|
+
ctx.addFinding(finding);
|
|
22633
|
+
}
|
|
22619
22634
|
}
|
|
22620
22635
|
if (language === "rust") {
|
|
22621
22636
|
additionalSanitizers.push(...findRustSetAllowlistGuardSanitizers(code));
|
|
@@ -22637,6 +22652,9 @@ class LanguageSourcesPass {
|
|
|
22637
22652
|
for (const finding of findRustWeakCryptoEcbFindings(code, graph.ir.meta.file)) {
|
|
22638
22653
|
ctx.addFinding(finding);
|
|
22639
22654
|
}
|
|
22655
|
+
for (const finding of findRustAppendHeaderTupleOpenRedirectFindings(code, graph.ir.meta.file)) {
|
|
22656
|
+
ctx.addFinding(finding);
|
|
22657
|
+
}
|
|
22640
22658
|
}
|
|
22641
22659
|
if (language === "javascript" || language === "typescript" || language === "htmljs") {
|
|
22642
22660
|
additionalSanitizers.push(...findJsSafeJsonParseSanitizers(code));
|
|
@@ -22644,9 +22662,20 @@ class LanguageSourcesPass {
|
|
|
22644
22662
|
additionalSanitizers.push(...findJsCsvFormulaPrefixSanitizers(code));
|
|
22645
22663
|
additionalSanitizers.push(...findJsWrapperFunctionSanitizers(code));
|
|
22646
22664
|
additionalSanitizers.push(...findJsSsrfAllowlistGuardSanitizers(code));
|
|
22665
|
+
additionalSanitizers.push(...findJsArgvFormExecSanitizers(code));
|
|
22666
|
+
additionalSanitizers.push(...findJsParameterizedSqlSanitizers(code));
|
|
22647
22667
|
for (const finding of findJsPatternFindings(code, graph.ir.meta.file)) {
|
|
22648
22668
|
ctx.addFinding(finding);
|
|
22649
22669
|
}
|
|
22670
|
+
for (const finding of findJsVueVHtmlXssFindings(code, graph.ir.meta.file)) {
|
|
22671
|
+
ctx.addFinding(finding);
|
|
22672
|
+
}
|
|
22673
|
+
for (const finding of findTsAngularBypassXssFindings(code, graph.ir.meta.file)) {
|
|
22674
|
+
ctx.addFinding(finding);
|
|
22675
|
+
}
|
|
22676
|
+
for (const finding of findJsDomOpenRedirectFindings(code, graph.ir.meta.file)) {
|
|
22677
|
+
ctx.addFinding(finding);
|
|
22678
|
+
}
|
|
22650
22679
|
}
|
|
22651
22680
|
if (language === "java") {
|
|
22652
22681
|
additionalSanitizers.push(...findJavaSafeJsonParseSanitizers(code));
|
|
@@ -22656,6 +22685,9 @@ class LanguageSourcesPass {
|
|
|
22656
22685
|
for (const finding of findJavaPatternFindings(code, graph.ir.meta.file)) {
|
|
22657
22686
|
ctx.addFinding(finding);
|
|
22658
22687
|
}
|
|
22688
|
+
for (const finding of findJavaResponseWriterXssFindings(code, graph.ir.meta.file)) {
|
|
22689
|
+
ctx.addFinding(finding);
|
|
22690
|
+
}
|
|
22659
22691
|
}
|
|
22660
22692
|
if (language === "python" || language === "javascript" || language === "typescript" || language === "go") {
|
|
22661
22693
|
const exfilFindings = findExternalSecretExfiltrationFindings(code, graph.ir.meta.file, language);
|
|
@@ -24607,6 +24639,64 @@ function findJsSsrfAllowlistGuardSanitizers(code) {
|
|
|
24607
24639
|
}
|
|
24608
24640
|
return sanitizers;
|
|
24609
24641
|
}
|
|
24642
|
+
function findJsArgvFormExecSanitizers(code) {
|
|
24643
|
+
const sanitizers = [];
|
|
24644
|
+
const lines = code.split(`
|
|
24645
|
+
`);
|
|
24646
|
+
const argvExecRe = /\b(?:execFile|spawn)(?:Sync)?\s*\(\s*(?:'[^']*'|"[^"]*"|`[^`]*`)\s*,\s*\[/;
|
|
24647
|
+
const shellArgvRe = /\b(?:execFile|spawn)(?:Sync)?\s*\(\s*['"`](?:[\w./-]*\/)?(?:sh|bash|zsh|ksh|dash|cmd(?:\.exe)?|powershell|pwsh)['"`]\s*,\s*\[\s*['"`]-c['"`]/;
|
|
24648
|
+
for (let i2 = 0;i2 < lines.length; i2++) {
|
|
24649
|
+
const text = lines[i2];
|
|
24650
|
+
if (!argvExecRe.test(text))
|
|
24651
|
+
continue;
|
|
24652
|
+
if (shellArgvRe.test(text))
|
|
24653
|
+
continue;
|
|
24654
|
+
sanitizers.push({
|
|
24655
|
+
type: "js_argv_form_exec",
|
|
24656
|
+
method: "execFile",
|
|
24657
|
+
line: i2 + 1,
|
|
24658
|
+
sanitizes: ["command_injection", "external_taint_escape"]
|
|
24659
|
+
});
|
|
24660
|
+
}
|
|
24661
|
+
return sanitizers;
|
|
24662
|
+
}
|
|
24663
|
+
function findJsParameterizedSqlSanitizers(code) {
|
|
24664
|
+
const sanitizers = [];
|
|
24665
|
+
const lines = code.split(`
|
|
24666
|
+
`);
|
|
24667
|
+
const singleRe = /\.\s*(?:query|execute)\s*\(\s*'([^'\\]*(?:\\.[^'\\]*)*)'\s*,\s*\[/;
|
|
24668
|
+
const doubleRe = /\.\s*(?:query|execute)\s*\(\s*"([^"\\]*(?:\\.[^"\\]*)*)"\s*,\s*\[/;
|
|
24669
|
+
const tickRe = /\.\s*(?:query|execute)\s*\(\s*`([^`]*)`\s*,\s*\[/;
|
|
24670
|
+
const placeholderRe = /(?:\$\d+|\?)/;
|
|
24671
|
+
for (let i2 = 0;i2 < lines.length; i2++) {
|
|
24672
|
+
const text = lines[i2];
|
|
24673
|
+
let sql = null;
|
|
24674
|
+
const s = singleRe.exec(text);
|
|
24675
|
+
if (s)
|
|
24676
|
+
sql = s[1];
|
|
24677
|
+
if (sql == null) {
|
|
24678
|
+
const d = doubleRe.exec(text);
|
|
24679
|
+
if (d)
|
|
24680
|
+
sql = d[1];
|
|
24681
|
+
}
|
|
24682
|
+
if (sql == null) {
|
|
24683
|
+
const t = tickRe.exec(text);
|
|
24684
|
+
if (t && !/\$\{/.test(t[1]))
|
|
24685
|
+
sql = t[1];
|
|
24686
|
+
}
|
|
24687
|
+
if (sql == null)
|
|
24688
|
+
continue;
|
|
24689
|
+
if (!placeholderRe.test(sql))
|
|
24690
|
+
continue;
|
|
24691
|
+
sanitizers.push({
|
|
24692
|
+
type: "js_parameterized_sql",
|
|
24693
|
+
method: "query",
|
|
24694
|
+
line: i2 + 1,
|
|
24695
|
+
sanitizes: ["sql_injection", "external_taint_escape"]
|
|
24696
|
+
});
|
|
24697
|
+
}
|
|
24698
|
+
return sanitizers;
|
|
24699
|
+
}
|
|
24610
24700
|
function findJavaPathNormalizeStartsWithGuardSanitizers(code) {
|
|
24611
24701
|
const sanitizers = [];
|
|
24612
24702
|
const lines = code.split(`
|
|
@@ -25444,6 +25534,674 @@ function findJsPatternFindings(code, file) {
|
|
|
25444
25534
|
}
|
|
25445
25535
|
return out2;
|
|
25446
25536
|
}
|
|
25537
|
+
function findGoXssFindings(code, file) {
|
|
25538
|
+
const out2 = [];
|
|
25539
|
+
const lines = code.split(`
|
|
25540
|
+
`);
|
|
25541
|
+
const rwNames = new Set;
|
|
25542
|
+
const sigRe = /\(\s*([A-Za-z_]\w*)\s+http\.ResponseWriter\b/g;
|
|
25543
|
+
for (const line of lines) {
|
|
25544
|
+
let m;
|
|
25545
|
+
sigRe.lastIndex = 0;
|
|
25546
|
+
while ((m = sigRe.exec(line)) !== null)
|
|
25547
|
+
rwNames.add(m[1]);
|
|
25548
|
+
}
|
|
25549
|
+
if (rwNames.size === 0)
|
|
25550
|
+
return out2;
|
|
25551
|
+
const escaperRe = /\b(?:html|template)\.(?:EscapeString|HTMLEscapeString|HTMLEscaper|JSEscapeString|URLQueryEscaper)\s*\(/;
|
|
25552
|
+
for (let i2 = 0;i2 < lines.length; i2++) {
|
|
25553
|
+
const raw = lines[i2];
|
|
25554
|
+
const trimmed = raw.trim();
|
|
25555
|
+
if (!trimmed || trimmed.startsWith("//"))
|
|
25556
|
+
continue;
|
|
25557
|
+
const callRe = /\bfmt\.Fprint(?:f|ln)?\s*\(\s*([A-Za-z_]\w*)\s*,\s*([\s\S]*)\)\s*(?:\/\/.*)?$/;
|
|
25558
|
+
const m = trimmed.match(callRe);
|
|
25559
|
+
if (!m)
|
|
25560
|
+
continue;
|
|
25561
|
+
if (!rwNames.has(m[1]))
|
|
25562
|
+
continue;
|
|
25563
|
+
const tail = m[2];
|
|
25564
|
+
if (escaperRe.test(tail))
|
|
25565
|
+
continue;
|
|
25566
|
+
if (!/[A-Za-z_]\w*/.test(tail.replace(/"[^"]*"|`[^`]*`/g, "")))
|
|
25567
|
+
continue;
|
|
25568
|
+
out2.push({
|
|
25569
|
+
id: `xss-${file}-${i2 + 1}`,
|
|
25570
|
+
pass: "language-sources",
|
|
25571
|
+
category: "security",
|
|
25572
|
+
rule_id: "xss",
|
|
25573
|
+
cwe: "CWE-79",
|
|
25574
|
+
severity: "high",
|
|
25575
|
+
level: "error",
|
|
25576
|
+
message: "Reflected XSS: fmt.Fprint writes data to http.ResponseWriter " + "without HTML escaping. Wrap user-controlled args with " + "html.EscapeString / template.HTMLEscapeString.",
|
|
25577
|
+
file,
|
|
25578
|
+
line: i2 + 1,
|
|
25579
|
+
snippet: trimmed.substring(0, 100)
|
|
25580
|
+
});
|
|
25581
|
+
}
|
|
25582
|
+
return out2;
|
|
25583
|
+
}
|
|
25584
|
+
function findJavaResponseWriterXssFindings(code, file) {
|
|
25585
|
+
const out2 = [];
|
|
25586
|
+
const lines = code.split(`
|
|
25587
|
+
`);
|
|
25588
|
+
const respNames = new Set;
|
|
25589
|
+
const sigRe = /\bHttpServletResponse\s+([A-Za-z_]\w*)\b/g;
|
|
25590
|
+
for (const line of lines) {
|
|
25591
|
+
let m;
|
|
25592
|
+
sigRe.lastIndex = 0;
|
|
25593
|
+
while ((m = sigRe.exec(line)) !== null)
|
|
25594
|
+
respNames.add(m[1]);
|
|
25595
|
+
}
|
|
25596
|
+
if (respNames.size === 0)
|
|
25597
|
+
return out2;
|
|
25598
|
+
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*\(/;
|
|
25599
|
+
for (let i2 = 0;i2 < lines.length; i2++) {
|
|
25600
|
+
const raw = lines[i2];
|
|
25601
|
+
const trimmed = raw.trim();
|
|
25602
|
+
if (!trimmed || trimmed.startsWith("//") || trimmed.startsWith("*"))
|
|
25603
|
+
continue;
|
|
25604
|
+
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*(?:\/\/.*)?$/;
|
|
25605
|
+
const m = trimmed.match(chainRe);
|
|
25606
|
+
if (!m)
|
|
25607
|
+
continue;
|
|
25608
|
+
const recv = m[1];
|
|
25609
|
+
if (!respNames.has(recv))
|
|
25610
|
+
continue;
|
|
25611
|
+
const args2 = m[3];
|
|
25612
|
+
if (safeWrapRe.test(args2))
|
|
25613
|
+
continue;
|
|
25614
|
+
const argsTrim = args2.trim();
|
|
25615
|
+
if (/^"(?:\\.|[^"\\])*"$/.test(argsTrim))
|
|
25616
|
+
continue;
|
|
25617
|
+
if (!/[A-Za-z_]\w*/.test(argsTrim))
|
|
25618
|
+
continue;
|
|
25619
|
+
out2.push({
|
|
25620
|
+
id: `xss-${file}-${i2 + 1}`,
|
|
25621
|
+
pass: "language-sources",
|
|
25622
|
+
category: "security",
|
|
25623
|
+
rule_id: "xss",
|
|
25624
|
+
cwe: "CWE-79",
|
|
25625
|
+
severity: "high",
|
|
25626
|
+
level: "error",
|
|
25627
|
+
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.",
|
|
25628
|
+
file,
|
|
25629
|
+
line: i2 + 1,
|
|
25630
|
+
snippet: trimmed.substring(0, 100)
|
|
25631
|
+
});
|
|
25632
|
+
}
|
|
25633
|
+
return out2;
|
|
25634
|
+
}
|
|
25635
|
+
function findJsVueVHtmlXssFindings(code, file) {
|
|
25636
|
+
const out2 = [];
|
|
25637
|
+
const lines = code.split(`
|
|
25638
|
+
`);
|
|
25639
|
+
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/;
|
|
25640
|
+
const tplRe = /\btemplate\s*:\s*(['"`])([\s\S]*?)\1/g;
|
|
25641
|
+
let tm;
|
|
25642
|
+
const boundVars = new Set;
|
|
25643
|
+
while ((tm = tplRe.exec(code)) !== null) {
|
|
25644
|
+
const tpl = tm[2];
|
|
25645
|
+
const vhRe = /\bv-html\s*=\s*"([^"]+)"/g;
|
|
25646
|
+
let vm;
|
|
25647
|
+
while ((vm = vhRe.exec(tpl)) !== null) {
|
|
25648
|
+
const expr = vm[1].trim();
|
|
25649
|
+
const idMatch = expr.match(/^([A-Za-z_$][\w$]*)/);
|
|
25650
|
+
if (idMatch)
|
|
25651
|
+
boundVars.add(idMatch[1]);
|
|
25652
|
+
}
|
|
25653
|
+
}
|
|
25654
|
+
if (boundVars.size === 0)
|
|
25655
|
+
return out2;
|
|
25656
|
+
const taintedSet = new Set;
|
|
25657
|
+
const assignRe = /\b(?:const|let|var)\s+([A-Za-z_$][\w$]*)\s*=\s*([^;\n]+)/g;
|
|
25658
|
+
let am;
|
|
25659
|
+
assignRe.lastIndex = 0;
|
|
25660
|
+
while ((am = assignRe.exec(code)) !== null) {
|
|
25661
|
+
if (sourceRe.test(am[2]))
|
|
25662
|
+
taintedSet.add(am[1]);
|
|
25663
|
+
}
|
|
25664
|
+
for (let pass = 0;pass < 3; pass++) {
|
|
25665
|
+
assignRe.lastIndex = 0;
|
|
25666
|
+
const before = taintedSet.size;
|
|
25667
|
+
while ((am = assignRe.exec(code)) !== null) {
|
|
25668
|
+
if (taintedSet.has(am[1]))
|
|
25669
|
+
continue;
|
|
25670
|
+
for (const tv of taintedSet) {
|
|
25671
|
+
if (new RegExp(`\\b${tv}\\b`).test(am[2])) {
|
|
25672
|
+
taintedSet.add(am[1]);
|
|
25673
|
+
break;
|
|
25674
|
+
}
|
|
25675
|
+
}
|
|
25676
|
+
}
|
|
25677
|
+
if (taintedSet.size === before)
|
|
25678
|
+
break;
|
|
25679
|
+
}
|
|
25680
|
+
const taintedBindings = new Set;
|
|
25681
|
+
for (const v of boundVars) {
|
|
25682
|
+
const bindRe = new RegExp(`\\b${v}\\s*[:=]\\s*([^,;\\n]+)`);
|
|
25683
|
+
for (const line of lines) {
|
|
25684
|
+
const m = line.match(bindRe);
|
|
25685
|
+
if (!m)
|
|
25686
|
+
continue;
|
|
25687
|
+
const val = m[1];
|
|
25688
|
+
if (sourceRe.test(val)) {
|
|
25689
|
+
taintedBindings.add(v);
|
|
25690
|
+
break;
|
|
25691
|
+
}
|
|
25692
|
+
let found = false;
|
|
25693
|
+
for (const tv of taintedSet) {
|
|
25694
|
+
if (new RegExp(`\\b${tv}\\b`).test(val)) {
|
|
25695
|
+
taintedBindings.add(v);
|
|
25696
|
+
found = true;
|
|
25697
|
+
break;
|
|
25698
|
+
}
|
|
25699
|
+
}
|
|
25700
|
+
if (found)
|
|
25701
|
+
break;
|
|
25702
|
+
}
|
|
25703
|
+
const propsRe = new RegExp(`\\bprops\\s*:\\s*\\[[^\\]]*['"\`]${v}['"\`][^\\]]*\\]`);
|
|
25704
|
+
if (propsRe.test(code))
|
|
25705
|
+
taintedBindings.add(v);
|
|
25706
|
+
}
|
|
25707
|
+
if (taintedBindings.size === 0)
|
|
25708
|
+
return out2;
|
|
25709
|
+
for (let i2 = 0;i2 < lines.length; i2++) {
|
|
25710
|
+
const raw = lines[i2];
|
|
25711
|
+
if (!/v-html\s*=/.test(raw))
|
|
25712
|
+
continue;
|
|
25713
|
+
const vm = raw.match(/v-html\s*=\s*"([^"]+)"/);
|
|
25714
|
+
if (!vm)
|
|
25715
|
+
continue;
|
|
25716
|
+
const idMatch = vm[1].trim().match(/^([A-Za-z_$][\w$]*)/);
|
|
25717
|
+
if (!idMatch || !taintedBindings.has(idMatch[1]))
|
|
25718
|
+
continue;
|
|
25719
|
+
out2.push({
|
|
25720
|
+
id: `xss-${file}-${i2 + 1}`,
|
|
25721
|
+
pass: "language-sources",
|
|
25722
|
+
category: "security",
|
|
25723
|
+
rule_id: "xss",
|
|
25724
|
+
cwe: "CWE-79",
|
|
25725
|
+
severity: "high",
|
|
25726
|
+
level: "error",
|
|
25727
|
+
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.",
|
|
25728
|
+
file,
|
|
25729
|
+
line: i2 + 1,
|
|
25730
|
+
snippet: raw.trim().substring(0, 100)
|
|
25731
|
+
});
|
|
25732
|
+
}
|
|
25733
|
+
return out2;
|
|
25734
|
+
}
|
|
25735
|
+
function findTsAngularBypassXssFindings(code, file) {
|
|
25736
|
+
const out2 = [];
|
|
25737
|
+
const lines = code.split(`
|
|
25738
|
+
`);
|
|
25739
|
+
const sanitizerNames = new Set;
|
|
25740
|
+
const declRe = /\b(?:public|private|protected|readonly|\s)?\s*([A-Za-z_$][\w$]*)\s*:\s*DomSanitizer\b/g;
|
|
25741
|
+
let m;
|
|
25742
|
+
declRe.lastIndex = 0;
|
|
25743
|
+
while ((m = declRe.exec(code)) !== null)
|
|
25744
|
+
sanitizerNames.add(m[1]);
|
|
25745
|
+
if (sanitizerNames.size === 0)
|
|
25746
|
+
return out2;
|
|
25747
|
+
const assignRe = /\bthis\s*\.\s*([A-Za-z_$][\w$]*)\s*=\s*([A-Za-z_$][\w$]*)\b/g;
|
|
25748
|
+
let am;
|
|
25749
|
+
assignRe.lastIndex = 0;
|
|
25750
|
+
while ((am = assignRe.exec(code)) !== null) {
|
|
25751
|
+
if (sanitizerNames.has(am[2]))
|
|
25752
|
+
sanitizerNames.add(am[1]);
|
|
25753
|
+
}
|
|
25754
|
+
for (let i2 = 0;i2 < lines.length; i2++) {
|
|
25755
|
+
const raw = lines[i2];
|
|
25756
|
+
const trimmed = raw.trim();
|
|
25757
|
+
if (!trimmed || trimmed.startsWith("//") || trimmed.startsWith("*"))
|
|
25758
|
+
continue;
|
|
25759
|
+
const callRe = /\b(?:this\s*\.\s*)?([A-Za-z_$][\w$]*)\s*\.\s*bypassSecurityTrust(Html|Script|Url|ResourceUrl|Style)\s*\(([\s\S]*?)\)/;
|
|
25760
|
+
const cm = trimmed.match(callRe);
|
|
25761
|
+
if (!cm)
|
|
25762
|
+
continue;
|
|
25763
|
+
const recv = cm[1];
|
|
25764
|
+
if (!sanitizerNames.has(recv))
|
|
25765
|
+
continue;
|
|
25766
|
+
const arg = cm[3].trim();
|
|
25767
|
+
if (/^(['"`])[^'"`]*\1$/.test(arg))
|
|
25768
|
+
continue;
|
|
25769
|
+
out2.push({
|
|
25770
|
+
id: `xss-${file}-${i2 + 1}`,
|
|
25771
|
+
pass: "language-sources",
|
|
25772
|
+
category: "security",
|
|
25773
|
+
rule_id: "xss",
|
|
25774
|
+
cwe: "CWE-79",
|
|
25775
|
+
severity: "high",
|
|
25776
|
+
level: "error",
|
|
25777
|
+
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.",
|
|
25778
|
+
file,
|
|
25779
|
+
line: i2 + 1,
|
|
25780
|
+
snippet: trimmed.substring(0, 100)
|
|
25781
|
+
});
|
|
25782
|
+
}
|
|
25783
|
+
return out2;
|
|
25784
|
+
}
|
|
25785
|
+
function findPythonFlaskStringConcatXssFindings(code, file) {
|
|
25786
|
+
const out2 = [];
|
|
25787
|
+
const lines = code.split(`
|
|
25788
|
+
`);
|
|
25789
|
+
const fns = [];
|
|
25790
|
+
const defRe = /^(\s*)def\s+([A-Za-z_]\w*)\s*\(/;
|
|
25791
|
+
for (let i2 = 0;i2 < lines.length; i2++) {
|
|
25792
|
+
const raw = lines[i2];
|
|
25793
|
+
const dm = raw.match(defRe);
|
|
25794
|
+
if (!dm)
|
|
25795
|
+
continue;
|
|
25796
|
+
const indent = dm[1].length;
|
|
25797
|
+
let hasRoute = false;
|
|
25798
|
+
for (let j = i2 - 1;j >= 0; j--) {
|
|
25799
|
+
const prev = lines[j].trim();
|
|
25800
|
+
if (prev === "" || prev.startsWith("#"))
|
|
25801
|
+
continue;
|
|
25802
|
+
if (!prev.startsWith("@"))
|
|
25803
|
+
break;
|
|
25804
|
+
if (/@\s*[A-Za-z_]\w*\s*\.\s*route\s*\(/.test(prev) || /@\s*route\s*\(/.test(prev)) {
|
|
25805
|
+
hasRoute = true;
|
|
25806
|
+
}
|
|
25807
|
+
}
|
|
25808
|
+
if (!hasRoute)
|
|
25809
|
+
continue;
|
|
25810
|
+
let end = lines.length;
|
|
25811
|
+
for (let k = i2 + 1;k < lines.length; k++) {
|
|
25812
|
+
const ln = lines[k];
|
|
25813
|
+
if (!ln.trim())
|
|
25814
|
+
continue;
|
|
25815
|
+
const ind = ln.match(/^(\s*)/)?.[1].length ?? 0;
|
|
25816
|
+
if (ind <= indent) {
|
|
25817
|
+
end = k;
|
|
25818
|
+
break;
|
|
25819
|
+
}
|
|
25820
|
+
}
|
|
25821
|
+
fns.push({ name: dm[2], startLine: i2 + 1, endLine: end });
|
|
25822
|
+
}
|
|
25823
|
+
if (fns.length === 0)
|
|
25824
|
+
return out2;
|
|
25825
|
+
const requestSourceRe = /\b([A-Za-z_]\w*)\s*=\s*request\s*\.\s*(?:args|form|values|files|json|cookies|headers)(?:\s*\.\s*get\s*\(|\s*\[)/;
|
|
25826
|
+
const escapeWrapRe = /\b(?:html\.escape|markupsafe\.escape|escape|markupsafe\.Markup\s*\.\s*escape|werkzeug\.utils\.escape)\s*\(/;
|
|
25827
|
+
for (const fn of fns) {
|
|
25828
|
+
const taintedVars = new Set;
|
|
25829
|
+
for (let li = fn.startLine;li < fn.endLine; li++) {
|
|
25830
|
+
const raw = lines[li];
|
|
25831
|
+
const rm = raw.match(requestSourceRe);
|
|
25832
|
+
if (rm)
|
|
25833
|
+
taintedVars.add(rm[1]);
|
|
25834
|
+
}
|
|
25835
|
+
for (let li = fn.startLine;li < fn.endLine; li++) {
|
|
25836
|
+
const raw = lines[li];
|
|
25837
|
+
const trimmed = raw.trim();
|
|
25838
|
+
if (!trimmed || trimmed.startsWith("#"))
|
|
25839
|
+
continue;
|
|
25840
|
+
const retMatch = trimmed.match(/^(?:return|yield)\s+(.+)$/);
|
|
25841
|
+
if (!retMatch)
|
|
25842
|
+
continue;
|
|
25843
|
+
const expr = retMatch[1];
|
|
25844
|
+
const inlineSource = /\brequest\s*\.\s*(?:args|form|values|files|cookies|headers)(?:\s*\.\s*get\s*\(|\s*\[)/.test(expr);
|
|
25845
|
+
const hasTaintedVar = [...taintedVars].some((v) => new RegExp(`\\b${v}\\b`).test(expr));
|
|
25846
|
+
if (!hasTaintedVar && !inlineSource)
|
|
25847
|
+
continue;
|
|
25848
|
+
if (escapeWrapRe.test(expr))
|
|
25849
|
+
continue;
|
|
25850
|
+
const buildsHtml = /['"`]\s*\+/.test(expr) || /\+\s*['"`]/.test(expr) || /\bf['"`][^{\n]*\{[^}]+\}/.test(expr) || /['"`]\s*%\s*[^=]/.test(expr) || /['"`]\.\s*format\s*\(/.test(expr);
|
|
25851
|
+
if (!buildsHtml)
|
|
25852
|
+
continue;
|
|
25853
|
+
out2.push({
|
|
25854
|
+
id: `xss-${file}-${li + 1}`,
|
|
25855
|
+
pass: "language-sources",
|
|
25856
|
+
category: "security",
|
|
25857
|
+
rule_id: "xss",
|
|
25858
|
+
cwe: "CWE-79",
|
|
25859
|
+
severity: "high",
|
|
25860
|
+
level: "error",
|
|
25861
|
+
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).",
|
|
25862
|
+
file,
|
|
25863
|
+
line: li + 1,
|
|
25864
|
+
snippet: trimmed.substring(0, 100)
|
|
25865
|
+
});
|
|
25866
|
+
break;
|
|
25867
|
+
}
|
|
25868
|
+
}
|
|
25869
|
+
return out2;
|
|
25870
|
+
}
|
|
25871
|
+
function findPythonJinjaMarkupXssFindings(code, file) {
|
|
25872
|
+
const out2 = [];
|
|
25873
|
+
const lines = code.split(`
|
|
25874
|
+
`);
|
|
25875
|
+
const importRe = /^\s*from\s+(?:markupsafe|flask)\s+import\s+(?:[^#\n]*\b)?Markup\b/m;
|
|
25876
|
+
const classDefRe = /^\s*class\s+Markup\b/m;
|
|
25877
|
+
if (!importRe.test(code))
|
|
25878
|
+
return out2;
|
|
25879
|
+
if (classDefRe.test(code))
|
|
25880
|
+
return out2;
|
|
25881
|
+
const requestSourceRe = /\b([A-Za-z_]\w*)\s*=\s*request\s*\.\s*(?:args|form|values|files|json|cookies|headers)(?:\s*\.\s*get\s*\(|\s*\[)/;
|
|
25882
|
+
const taintedVars = new Set;
|
|
25883
|
+
for (const line of lines) {
|
|
25884
|
+
const m = line.match(requestSourceRe);
|
|
25885
|
+
if (m)
|
|
25886
|
+
taintedVars.add(m[1]);
|
|
25887
|
+
}
|
|
25888
|
+
if (taintedVars.size === 0)
|
|
25889
|
+
return out2;
|
|
25890
|
+
if (!/\.\s*render\s*\(/.test(code))
|
|
25891
|
+
return out2;
|
|
25892
|
+
const markupCallRe = /\bMarkup\s*\(\s*([A-Za-z_]\w*)\s*[,)]/g;
|
|
25893
|
+
for (let i2 = 0;i2 < lines.length; i2++) {
|
|
25894
|
+
const raw = lines[i2];
|
|
25895
|
+
const trimmed = raw.trim();
|
|
25896
|
+
if (!trimmed || trimmed.startsWith("#"))
|
|
25897
|
+
continue;
|
|
25898
|
+
let mm;
|
|
25899
|
+
markupCallRe.lastIndex = 0;
|
|
25900
|
+
while ((mm = markupCallRe.exec(trimmed)) !== null) {
|
|
25901
|
+
const argName = mm[1];
|
|
25902
|
+
if (!taintedVars.has(argName))
|
|
25903
|
+
continue;
|
|
25904
|
+
out2.push({
|
|
25905
|
+
id: `xss-${file}-${i2 + 1}`,
|
|
25906
|
+
pass: "language-sources",
|
|
25907
|
+
category: "security",
|
|
25908
|
+
rule_id: "xss",
|
|
25909
|
+
cwe: "CWE-79",
|
|
25910
|
+
severity: "high",
|
|
25911
|
+
level: "error",
|
|
25912
|
+
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.",
|
|
25913
|
+
file,
|
|
25914
|
+
line: i2 + 1,
|
|
25915
|
+
snippet: trimmed.substring(0, 100)
|
|
25916
|
+
});
|
|
25917
|
+
break;
|
|
25918
|
+
}
|
|
25919
|
+
}
|
|
25920
|
+
return out2;
|
|
25921
|
+
}
|
|
25922
|
+
function findGoLocationHeaderOpenRedirectFindings(code, file) {
|
|
25923
|
+
const out2 = [];
|
|
25924
|
+
const lines = code.split(`
|
|
25925
|
+
`);
|
|
25926
|
+
const rwNames = new Set;
|
|
25927
|
+
const sigRe = /\(\s*([A-Za-z_]\w*)\s+http\.ResponseWriter\b/g;
|
|
25928
|
+
for (const line of lines) {
|
|
25929
|
+
let m;
|
|
25930
|
+
sigRe.lastIndex = 0;
|
|
25931
|
+
while ((m = sigRe.exec(line)) !== null)
|
|
25932
|
+
rwNames.add(m[1]);
|
|
25933
|
+
}
|
|
25934
|
+
if (rwNames.size === 0)
|
|
25935
|
+
return out2;
|
|
25936
|
+
const safeWrapRe = /\b(?:net\/url|url)\.Parse\b[\s\S]{0,120}?\b(?:IsAbs|Host)\b|\bstrings\.HasPrefix\s*\(/;
|
|
25937
|
+
const callRe = /\b([A-Za-z_]\w*)\s*\.\s*Header\s*\(\s*\)\s*\.\s*(?:Set|Add)\s*\(\s*(['"])([^'"]+)\2\s*,\s*([\s\S]*)\)\s*(?:\/\/.*)?$/;
|
|
25938
|
+
for (let i2 = 0;i2 < lines.length; i2++) {
|
|
25939
|
+
const raw = lines[i2];
|
|
25940
|
+
const trimmed = raw.trim();
|
|
25941
|
+
if (!trimmed || trimmed.startsWith("//"))
|
|
25942
|
+
continue;
|
|
25943
|
+
const m = trimmed.match(callRe);
|
|
25944
|
+
if (!m)
|
|
25945
|
+
continue;
|
|
25946
|
+
const recv = m[1];
|
|
25947
|
+
if (!rwNames.has(recv))
|
|
25948
|
+
continue;
|
|
25949
|
+
const key = m[3];
|
|
25950
|
+
if (key.toLowerCase() !== "location")
|
|
25951
|
+
continue;
|
|
25952
|
+
const valExpr = m[4];
|
|
25953
|
+
const valTrim = valExpr.trim();
|
|
25954
|
+
if (/^"(?:\\.|[^"\\])*"$/.test(valTrim))
|
|
25955
|
+
continue;
|
|
25956
|
+
if (safeWrapRe.test(valExpr))
|
|
25957
|
+
continue;
|
|
25958
|
+
out2.push({
|
|
25959
|
+
id: `open_redirect-${file}-${i2 + 1}`,
|
|
25960
|
+
pass: "language-sources",
|
|
25961
|
+
category: "security",
|
|
25962
|
+
rule_id: "open_redirect",
|
|
25963
|
+
cwe: "CWE-601",
|
|
25964
|
+
severity: "high",
|
|
25965
|
+
level: "error",
|
|
25966
|
+
message: 'Open redirect: ResponseWriter.Header().Set("Location", ...) ' + "writes a user-controlled value into the Location header without " + "validating the target. Restrict to an allow-list of hosts/paths " + "or compare against a known-safe set before issuing the redirect.",
|
|
25967
|
+
file,
|
|
25968
|
+
line: i2 + 1,
|
|
25969
|
+
snippet: trimmed.substring(0, 100)
|
|
25970
|
+
});
|
|
25971
|
+
}
|
|
25972
|
+
return out2;
|
|
25973
|
+
}
|
|
25974
|
+
function findPythonHeadersSubscriptOpenRedirectFindings(code, file) {
|
|
25975
|
+
const out2 = [];
|
|
25976
|
+
const lines = code.split(`
|
|
25977
|
+
`);
|
|
25978
|
+
const requestSourceRe = /\b([A-Za-z_]\w*)\s*=\s*request\s*\.\s*(?:args|form|values|files|json|cookies|headers)(?:\s*\.\s*get\s*\(|\s*\[)/;
|
|
25979
|
+
const taintedVars = new Set;
|
|
25980
|
+
for (const line of lines) {
|
|
25981
|
+
const m = line.match(requestSourceRe);
|
|
25982
|
+
if (m)
|
|
25983
|
+
taintedVars.add(m[1]);
|
|
25984
|
+
}
|
|
25985
|
+
const safeWrapRe = /\b(?:urllib\.parse\.urlparse|urlparse)\s*\([\s\S]{0,120}?\bnetloc\b|\b(?:startswith)\s*\(/;
|
|
25986
|
+
const subscriptRe = /\b([A-Za-z_]\w*)\s*\.\s*headers\s*\[\s*(['"])([^'"]+)\2\s*\]\s*=\s*(.+)$/;
|
|
25987
|
+
for (let i2 = 0;i2 < lines.length; i2++) {
|
|
25988
|
+
const raw = lines[i2];
|
|
25989
|
+
const trimmed = raw.trim();
|
|
25990
|
+
if (!trimmed || trimmed.startsWith("#"))
|
|
25991
|
+
continue;
|
|
25992
|
+
const m = trimmed.match(subscriptRe);
|
|
25993
|
+
if (!m)
|
|
25994
|
+
continue;
|
|
25995
|
+
const key = m[3];
|
|
25996
|
+
if (key.toLowerCase() !== "location")
|
|
25997
|
+
continue;
|
|
25998
|
+
const rhs = m[4].replace(/\s*(?:#.*)?$/, "").trim();
|
|
25999
|
+
if (/^['"](?:\\.|[^'"\\])*['"]$/.test(rhs))
|
|
26000
|
+
continue;
|
|
26001
|
+
if (safeWrapRe.test(rhs))
|
|
26002
|
+
continue;
|
|
26003
|
+
const hasTaintedVar = [...taintedVars].some((v) => new RegExp(`\\b${v}\\b`).test(rhs));
|
|
26004
|
+
const inlineSource = /\brequest\s*\.\s*(?:args|form|values|files|cookies|headers|json)(?:\s*\.\s*get\s*\(|\s*\[)/.test(rhs);
|
|
26005
|
+
if (!hasTaintedVar && !inlineSource)
|
|
26006
|
+
continue;
|
|
26007
|
+
out2.push({
|
|
26008
|
+
id: `open_redirect-${file}-${i2 + 1}`,
|
|
26009
|
+
pass: "language-sources",
|
|
26010
|
+
category: "security",
|
|
26011
|
+
rule_id: "open_redirect",
|
|
26012
|
+
cwe: "CWE-601",
|
|
26013
|
+
severity: "high",
|
|
26014
|
+
level: "error",
|
|
26015
|
+
message: 'Open redirect: response.headers["Location"] = ... assigns a ' + "user-controlled value to the Location header. Validate the URL " + "against an allow-list of trusted hosts before sending.",
|
|
26016
|
+
file,
|
|
26017
|
+
line: i2 + 1,
|
|
26018
|
+
snippet: trimmed.substring(0, 100)
|
|
26019
|
+
});
|
|
26020
|
+
}
|
|
26021
|
+
return out2;
|
|
26022
|
+
}
|
|
26023
|
+
function findRustAppendHeaderTupleOpenRedirectFindings(code, file) {
|
|
26024
|
+
const out2 = [];
|
|
26025
|
+
const lines = code.split(`
|
|
26026
|
+
`);
|
|
26027
|
+
const extractorRe = /\b([A-Za-z_]\w*)\s*:\s*(?:web\s*::\s*)?(?:Query|Path|Form|Json)\s*<|\b([A-Za-z_]\w*)\s*:\s*&?\s*HttpRequest\b/g;
|
|
26028
|
+
const extractorParams = new Set;
|
|
26029
|
+
for (const line of lines) {
|
|
26030
|
+
let m;
|
|
26031
|
+
extractorRe.lastIndex = 0;
|
|
26032
|
+
while ((m = extractorRe.exec(line)) !== null) {
|
|
26033
|
+
const name2 = m[1] ?? m[2];
|
|
26034
|
+
if (name2)
|
|
26035
|
+
extractorParams.add(name2);
|
|
26036
|
+
}
|
|
26037
|
+
}
|
|
26038
|
+
if (extractorParams.size === 0)
|
|
26039
|
+
return out2;
|
|
26040
|
+
const taintedVars = new Set(extractorParams);
|
|
26041
|
+
const letRe = /\blet\s+(?:mut\s+)?([A-Za-z_]\w*)\s*(?::[^=]+)?=\s*([^;]+);/g;
|
|
26042
|
+
for (let pass = 0;pass < 4; pass++) {
|
|
26043
|
+
const before = taintedVars.size;
|
|
26044
|
+
let lm;
|
|
26045
|
+
letRe.lastIndex = 0;
|
|
26046
|
+
while ((lm = letRe.exec(code)) !== null) {
|
|
26047
|
+
const name2 = lm[1];
|
|
26048
|
+
const rhs = lm[2];
|
|
26049
|
+
if (taintedVars.has(name2))
|
|
26050
|
+
continue;
|
|
26051
|
+
for (const tv of taintedVars) {
|
|
26052
|
+
if (new RegExp(`\\b${tv}\\b`).test(rhs)) {
|
|
26053
|
+
taintedVars.add(name2);
|
|
26054
|
+
break;
|
|
26055
|
+
}
|
|
26056
|
+
}
|
|
26057
|
+
}
|
|
26058
|
+
if (taintedVars.size === before)
|
|
26059
|
+
break;
|
|
26060
|
+
}
|
|
26061
|
+
const safeWrapRe = /\b(?:Url\s*::\s*parse|url\s*::\s*Url\s*::\s*parse)\s*\([\s\S]{0,160}?\b(?:host_str|host|origin)\b/;
|
|
26062
|
+
const tupleCallRe = /\.\s*(?:append_header|insert_header)\s*\(\s*\(\s*(['"])([^'"]+)\1\s*,\s*([^)]*?)\s*\)\s*\)/g;
|
|
26063
|
+
for (let i2 = 0;i2 < lines.length; i2++) {
|
|
26064
|
+
const raw = lines[i2];
|
|
26065
|
+
const trimmed = raw.trim();
|
|
26066
|
+
if (!trimmed || trimmed.startsWith("//"))
|
|
26067
|
+
continue;
|
|
26068
|
+
let cm;
|
|
26069
|
+
tupleCallRe.lastIndex = 0;
|
|
26070
|
+
while ((cm = tupleCallRe.exec(raw)) !== null) {
|
|
26071
|
+
const key = cm[2];
|
|
26072
|
+
if (key.toLowerCase() !== "location")
|
|
26073
|
+
continue;
|
|
26074
|
+
const valExpr = cm[3];
|
|
26075
|
+
if (/^['"](?:\\.|[^'"\\])*['"]$/.test(valExpr.trim()))
|
|
26076
|
+
continue;
|
|
26077
|
+
if (safeWrapRe.test(valExpr))
|
|
26078
|
+
continue;
|
|
26079
|
+
const hasTainted = [...taintedVars].some((v) => new RegExp(`\\b${v}\\b`).test(valExpr));
|
|
26080
|
+
if (!hasTainted)
|
|
26081
|
+
continue;
|
|
26082
|
+
out2.push({
|
|
26083
|
+
id: `open_redirect-${file}-${i2 + 1}`,
|
|
26084
|
+
pass: "language-sources",
|
|
26085
|
+
category: "security",
|
|
26086
|
+
rule_id: "open_redirect",
|
|
26087
|
+
cwe: "CWE-601",
|
|
26088
|
+
severity: "high",
|
|
26089
|
+
level: "error",
|
|
26090
|
+
message: "Open redirect: HttpResponse builder " + '.append_header(("Location", ...)) sets the Location header from ' + "a request-derived value without allow-list validation. Parse " + "with url::Url::parse and compare host_str against an allow-list.",
|
|
26091
|
+
file,
|
|
26092
|
+
line: i2 + 1,
|
|
26093
|
+
snippet: trimmed.substring(0, 100)
|
|
26094
|
+
});
|
|
26095
|
+
break;
|
|
26096
|
+
}
|
|
26097
|
+
}
|
|
26098
|
+
return out2;
|
|
26099
|
+
}
|
|
26100
|
+
function findJsDomOpenRedirectFindings(code, file) {
|
|
26101
|
+
const out2 = [];
|
|
26102
|
+
const lines = code.split(`
|
|
26103
|
+
`);
|
|
26104
|
+
const domSourceRe = /\blocation\s*\.\s*(?:search|hash|href|pathname)\b|\bwindow\s*\.\s*location\s*\.\s*(?:search|hash|href|pathname)\b|\bURLSearchParams\b|\bdocument\s*\.\s*(?:referrer|URL|location\s*\.\s*(?:search|hash|href|pathname))\b|\bwindow\s*\.\s*name\b/;
|
|
26105
|
+
const taintedVars = new Set;
|
|
26106
|
+
const assignRe = /\b(?:const|let|var)\s+([A-Za-z_$][\w$]*)\s*=\s*([^;\n]+)/g;
|
|
26107
|
+
let am;
|
|
26108
|
+
assignRe.lastIndex = 0;
|
|
26109
|
+
while ((am = assignRe.exec(code)) !== null) {
|
|
26110
|
+
if (domSourceRe.test(am[2]))
|
|
26111
|
+
taintedVars.add(am[1]);
|
|
26112
|
+
}
|
|
26113
|
+
for (let pass = 0;pass < 4; pass++) {
|
|
26114
|
+
const before = taintedVars.size;
|
|
26115
|
+
assignRe.lastIndex = 0;
|
|
26116
|
+
while ((am = assignRe.exec(code)) !== null) {
|
|
26117
|
+
if (taintedVars.has(am[1]))
|
|
26118
|
+
continue;
|
|
26119
|
+
for (const tv of taintedVars) {
|
|
26120
|
+
if (new RegExp(`\\b${tv}\\b`).test(am[2])) {
|
|
26121
|
+
taintedVars.add(am[1]);
|
|
26122
|
+
break;
|
|
26123
|
+
}
|
|
26124
|
+
}
|
|
26125
|
+
}
|
|
26126
|
+
if (taintedVars.size === before)
|
|
26127
|
+
break;
|
|
26128
|
+
}
|
|
26129
|
+
const safeWrapRe = /\b(?:startsWith|includes)\s*\(\s*['"`]\/[^'"`]*['"`]\s*\)|\bnew\s+URL\s*\([\s\S]{0,80}?\)\s*\.\s*(?:origin|hostname)\s*===/;
|
|
26130
|
+
const containsTaint = (expr) => {
|
|
26131
|
+
if (domSourceRe.test(expr))
|
|
26132
|
+
return true;
|
|
26133
|
+
for (const tv of taintedVars) {
|
|
26134
|
+
if (new RegExp(`\\b${tv}\\b`).test(expr))
|
|
26135
|
+
return true;
|
|
26136
|
+
}
|
|
26137
|
+
return false;
|
|
26138
|
+
};
|
|
26139
|
+
const hrefSinkRe = /\b(?:(?:window|document|self|top|parent)\s*\.\s*)?location\s*\.\s*href\s*=\s*([^;\n]+?)\s*;?\s*(?:\/\/.*)?$/;
|
|
26140
|
+
const locSinkRe = /\b(?:window|document|self|top|parent)\s*\.\s*location\s*=\s*([^;\n]+?)\s*;?\s*(?:\/\/.*)?$/;
|
|
26141
|
+
const contentSinkRe = /\.\s*content\s*=\s*([^\n]+?)\s*$/;
|
|
26142
|
+
const callSinkRe = /\b(?:(?:window|document|self|top|parent)\s*\.\s*)?location\s*\.\s*(?:assign|replace)\s*\(\s*([^)]+)\)/;
|
|
26143
|
+
const emit = (line, msg, snippet) => {
|
|
26144
|
+
out2.push({
|
|
26145
|
+
id: `open_redirect-${file}-${line}`,
|
|
26146
|
+
pass: "language-sources",
|
|
26147
|
+
category: "security",
|
|
26148
|
+
rule_id: "open_redirect",
|
|
26149
|
+
cwe: "CWE-601",
|
|
26150
|
+
severity: "high",
|
|
26151
|
+
level: "error",
|
|
26152
|
+
message: msg,
|
|
26153
|
+
file,
|
|
26154
|
+
line,
|
|
26155
|
+
snippet: snippet.substring(0, 100)
|
|
26156
|
+
});
|
|
26157
|
+
};
|
|
26158
|
+
const isLiteralOnly = (expr) => /^['"`](?:\\.|[^'"`\\])*['"`]$/.test(expr.trim());
|
|
26159
|
+
for (let i2 = 0;i2 < lines.length; i2++) {
|
|
26160
|
+
const raw = lines[i2];
|
|
26161
|
+
const trimmed = raw.trim();
|
|
26162
|
+
if (!trimmed || trimmed.startsWith("//"))
|
|
26163
|
+
continue;
|
|
26164
|
+
let matched = false;
|
|
26165
|
+
const hm = trimmed.match(hrefSinkRe);
|
|
26166
|
+
if (hm) {
|
|
26167
|
+
const rhs = hm[1].trim();
|
|
26168
|
+
if (!isLiteralOnly(rhs) && containsTaint(rhs) && !safeWrapRe.test(rhs)) {
|
|
26169
|
+
emit(i2 + 1, "Open redirect: assignment to location.href uses a value " + "derived from a URL query/hash without allow-list validation. " + "Compare the target origin to a known-safe set before navigating.", trimmed);
|
|
26170
|
+
matched = true;
|
|
26171
|
+
}
|
|
26172
|
+
}
|
|
26173
|
+
if (!matched) {
|
|
26174
|
+
const lm = trimmed.match(locSinkRe);
|
|
26175
|
+
if (lm) {
|
|
26176
|
+
const rhs = lm[1].trim();
|
|
26177
|
+
if (!isLiteralOnly(rhs) && containsTaint(rhs) && !safeWrapRe.test(rhs)) {
|
|
26178
|
+
emit(i2 + 1, "Open redirect: assignment to window.location uses a value " + "derived from a URL query/hash without allow-list validation. " + "Compare the target origin to a known-safe set before navigating.", trimmed);
|
|
26179
|
+
matched = true;
|
|
26180
|
+
}
|
|
26181
|
+
}
|
|
26182
|
+
}
|
|
26183
|
+
if (!matched) {
|
|
26184
|
+
const cm = trimmed.match(contentSinkRe);
|
|
26185
|
+
if (cm) {
|
|
26186
|
+
const rhs = cm[1].replace(/\s*\/\/.*$/, "").replace(/\s*;\s*$/, "").trim();
|
|
26187
|
+
if (!isLiteralOnly(rhs) && /['"`][^'"`]*\burl\s*=/i.test(rhs) && containsTaint(rhs) && !safeWrapRe.test(rhs)) {
|
|
26188
|
+
emit(i2 + 1, "Open redirect: DOM assignment to <meta>.content with a " + "meta-refresh URL built from a URL query/hash without " + "allow-list validation. Validate origin before navigating.", trimmed);
|
|
26189
|
+
matched = true;
|
|
26190
|
+
}
|
|
26191
|
+
}
|
|
26192
|
+
}
|
|
26193
|
+
if (!matched) {
|
|
26194
|
+
const callm = trimmed.match(callSinkRe);
|
|
26195
|
+
if (callm) {
|
|
26196
|
+
const arg = callm[1].trim();
|
|
26197
|
+
if (!isLiteralOnly(arg) && containsTaint(arg) && !safeWrapRe.test(arg)) {
|
|
26198
|
+
emit(i2 + 1, "Open redirect: location.assign / location.replace invoked " + "with a value derived from a URL query/hash without allow-" + "list validation. Validate origin before navigating.", trimmed);
|
|
26199
|
+
}
|
|
26200
|
+
}
|
|
26201
|
+
}
|
|
26202
|
+
}
|
|
26203
|
+
return out2;
|
|
26204
|
+
}
|
|
25447
26205
|
|
|
25448
26206
|
// ../circle-ir/dist/analysis/passes/sink-filter-pass.js
|
|
25449
26207
|
var JS_XSS_SANITIZERS = [
|
|
@@ -26908,13 +27666,13 @@ class TaintPropagationPass {
|
|
|
26908
27666
|
}));
|
|
26909
27667
|
const arrayFlows = detectArrayElementFlows(calls, sources, sinks, constProp.taintedArrayElements, constProp.unreachableLines, types) ?? [];
|
|
26910
27668
|
for (const f of arrayFlows) {
|
|
26911
|
-
if (!flows.some((x) => x.source_line === f.source_line && x.sink_line === f.sink_line)) {
|
|
27669
|
+
if (!flows.some((x) => x.source_line === f.source_line && x.sink_line === f.sink_line && x.sink_type === f.sink_type)) {
|
|
26912
27670
|
flows.push(f);
|
|
26913
27671
|
}
|
|
26914
27672
|
}
|
|
26915
27673
|
const collectionFlows = detectCollectionFlows(calls, sources, sinks, constProp.tainted, constProp.unreachableLines, ctx.code, types) ?? [];
|
|
26916
27674
|
for (const f of collectionFlows) {
|
|
26917
|
-
if (flows.some((x) => x.source_line === f.source_line && x.sink_line === f.sink_line))
|
|
27675
|
+
if (flows.some((x) => x.source_line === f.source_line && x.sink_line === f.sink_line && x.sink_type === f.sink_type))
|
|
26918
27676
|
continue;
|
|
26919
27677
|
const flowForCheck = {
|
|
26920
27678
|
source: { line: f.source_line },
|
|
@@ -26936,7 +27694,7 @@ class TaintPropagationPass {
|
|
|
26936
27694
|
}
|
|
26937
27695
|
const paramFlows = detectParameterSinkFlows(types, calls, sources, sinks, constProp.unreachableLines, constProp.tainted, ctx.code) ?? [];
|
|
26938
27696
|
for (const f of paramFlows) {
|
|
26939
|
-
if (!flows.some((x) => x.source_line === f.source_line && x.sink_line === f.sink_line)) {
|
|
27697
|
+
if (!flows.some((x) => x.source_line === f.source_line && x.sink_line === f.sink_line && x.sink_type === f.sink_type)) {
|
|
26940
27698
|
flows.push(f);
|
|
26941
27699
|
}
|
|
26942
27700
|
}
|
|
@@ -27374,7 +28132,7 @@ function detectParameterSinkFlows(types, calls, sources, sinks, unreachableLines
|
|
|
27374
28132
|
}
|
|
27375
28133
|
const paramSource = methodParamSources.get(arg.variable);
|
|
27376
28134
|
if (paramSource) {
|
|
27377
|
-
const exists = flows.some((f) => f.source_line === paramSource.line && f.sink_line === sink.line);
|
|
28135
|
+
const exists = flows.some((f) => f.source_line === paramSource.line && f.sink_line === sink.line && f.sink_type === sink.type);
|
|
27378
28136
|
if (!exists) {
|
|
27379
28137
|
if (typeof code === "string" && isReassignedToLiteralBetween(code, arg.variable, paramSource.line, sink.line)) {
|
|
27380
28138
|
continue;
|
|
@@ -38571,7 +39329,7 @@ var colors = {
|
|
|
38571
39329
|
};
|
|
38572
39330
|
|
|
38573
39331
|
// src/version.ts
|
|
38574
|
-
var version = "3.
|
|
39332
|
+
var version = "3.132.0";
|
|
38575
39333
|
|
|
38576
39334
|
// src/formatters.ts
|
|
38577
39335
|
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.132.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.132.0"
|
|
70
70
|
},
|
|
71
71
|
"devDependencies": {
|
|
72
72
|
"@types/node": "^25.5.0",
|