cognium-dev 3.131.0 → 3.133.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 +787 -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];
|
|
@@ -22607,6 +22607,12 @@ class LanguageSourcesPass {
|
|
|
22607
22607
|
for (const finding of findGoXssFindings(code, graph.ir.meta.file)) {
|
|
22608
22608
|
ctx.addFinding(finding);
|
|
22609
22609
|
}
|
|
22610
|
+
for (const finding of findGoLocationHeaderOpenRedirectFindings(code, graph.ir.meta.file)) {
|
|
22611
|
+
ctx.addFinding(finding);
|
|
22612
|
+
}
|
|
22613
|
+
for (const finding of findGoPluginOpenCodeInjectionFindings(code, graph.ir.meta.file)) {
|
|
22614
|
+
ctx.addFinding(finding);
|
|
22615
|
+
}
|
|
22610
22616
|
}
|
|
22611
22617
|
if (language === "python") {
|
|
22612
22618
|
additionalSanitizers.push(...findPythonNetlocAllowlistGuardSanitizers(code));
|
|
@@ -22625,6 +22631,12 @@ class LanguageSourcesPass {
|
|
|
22625
22631
|
for (const finding of findPythonJinjaMarkupXssFindings(code, graph.ir.meta.file)) {
|
|
22626
22632
|
ctx.addFinding(finding);
|
|
22627
22633
|
}
|
|
22634
|
+
for (const finding of findPythonHeadersSubscriptOpenRedirectFindings(code, graph.ir.meta.file)) {
|
|
22635
|
+
ctx.addFinding(finding);
|
|
22636
|
+
}
|
|
22637
|
+
for (const finding of findPythonInteractiveInterpreterCodeInjectionFindings(code, graph.ir.meta.file)) {
|
|
22638
|
+
ctx.addFinding(finding);
|
|
22639
|
+
}
|
|
22628
22640
|
}
|
|
22629
22641
|
if (language === "rust") {
|
|
22630
22642
|
additionalSanitizers.push(...findRustSetAllowlistGuardSanitizers(code));
|
|
@@ -22646,6 +22658,12 @@ class LanguageSourcesPass {
|
|
|
22646
22658
|
for (const finding of findRustWeakCryptoEcbFindings(code, graph.ir.meta.file)) {
|
|
22647
22659
|
ctx.addFinding(finding);
|
|
22648
22660
|
}
|
|
22661
|
+
for (const finding of findRustAppendHeaderTupleOpenRedirectFindings(code, graph.ir.meta.file)) {
|
|
22662
|
+
ctx.addFinding(finding);
|
|
22663
|
+
}
|
|
22664
|
+
for (const finding of findRustEvalCrateCodeInjectionFindings(code, graph.ir.meta.file)) {
|
|
22665
|
+
ctx.addFinding(finding);
|
|
22666
|
+
}
|
|
22649
22667
|
}
|
|
22650
22668
|
if (language === "javascript" || language === "typescript" || language === "htmljs") {
|
|
22651
22669
|
additionalSanitizers.push(...findJsSafeJsonParseSanitizers(code));
|
|
@@ -22664,6 +22682,12 @@ class LanguageSourcesPass {
|
|
|
22664
22682
|
for (const finding of findTsAngularBypassXssFindings(code, graph.ir.meta.file)) {
|
|
22665
22683
|
ctx.addFinding(finding);
|
|
22666
22684
|
}
|
|
22685
|
+
for (const finding of findJsDomOpenRedirectFindings(code, graph.ir.meta.file)) {
|
|
22686
|
+
ctx.addFinding(finding);
|
|
22687
|
+
}
|
|
22688
|
+
for (const finding of findJsIndirectEvalCodeInjectionFindings(code, graph.ir.meta.file)) {
|
|
22689
|
+
ctx.addFinding(finding);
|
|
22690
|
+
}
|
|
22667
22691
|
}
|
|
22668
22692
|
if (language === "java") {
|
|
22669
22693
|
additionalSanitizers.push(...findJavaSafeJsonParseSanitizers(code));
|
|
@@ -25907,6 +25931,755 @@ function findPythonJinjaMarkupXssFindings(code, file) {
|
|
|
25907
25931
|
}
|
|
25908
25932
|
return out2;
|
|
25909
25933
|
}
|
|
25934
|
+
function findGoLocationHeaderOpenRedirectFindings(code, file) {
|
|
25935
|
+
const out2 = [];
|
|
25936
|
+
const lines = code.split(`
|
|
25937
|
+
`);
|
|
25938
|
+
const rwNames = new Set;
|
|
25939
|
+
const sigRe = /\(\s*([A-Za-z_]\w*)\s+http\.ResponseWriter\b/g;
|
|
25940
|
+
for (const line of lines) {
|
|
25941
|
+
let m;
|
|
25942
|
+
sigRe.lastIndex = 0;
|
|
25943
|
+
while ((m = sigRe.exec(line)) !== null)
|
|
25944
|
+
rwNames.add(m[1]);
|
|
25945
|
+
}
|
|
25946
|
+
if (rwNames.size === 0)
|
|
25947
|
+
return out2;
|
|
25948
|
+
const safeWrapRe = /\b(?:net\/url|url)\.Parse\b[\s\S]{0,120}?\b(?:IsAbs|Host)\b|\bstrings\.HasPrefix\s*\(/;
|
|
25949
|
+
const callRe = /\b([A-Za-z_]\w*)\s*\.\s*Header\s*\(\s*\)\s*\.\s*(?:Set|Add)\s*\(\s*(['"])([^'"]+)\2\s*,\s*([\s\S]*)\)\s*(?:\/\/.*)?$/;
|
|
25950
|
+
for (let i2 = 0;i2 < lines.length; i2++) {
|
|
25951
|
+
const raw = lines[i2];
|
|
25952
|
+
const trimmed = raw.trim();
|
|
25953
|
+
if (!trimmed || trimmed.startsWith("//"))
|
|
25954
|
+
continue;
|
|
25955
|
+
const m = trimmed.match(callRe);
|
|
25956
|
+
if (!m)
|
|
25957
|
+
continue;
|
|
25958
|
+
const recv = m[1];
|
|
25959
|
+
if (!rwNames.has(recv))
|
|
25960
|
+
continue;
|
|
25961
|
+
const key = m[3];
|
|
25962
|
+
if (key.toLowerCase() !== "location")
|
|
25963
|
+
continue;
|
|
25964
|
+
const valExpr = m[4];
|
|
25965
|
+
const valTrim = valExpr.trim();
|
|
25966
|
+
if (/^"(?:\\.|[^"\\])*"$/.test(valTrim))
|
|
25967
|
+
continue;
|
|
25968
|
+
if (safeWrapRe.test(valExpr))
|
|
25969
|
+
continue;
|
|
25970
|
+
out2.push({
|
|
25971
|
+
id: `open_redirect-${file}-${i2 + 1}`,
|
|
25972
|
+
pass: "language-sources",
|
|
25973
|
+
category: "security",
|
|
25974
|
+
rule_id: "open_redirect",
|
|
25975
|
+
cwe: "CWE-601",
|
|
25976
|
+
severity: "high",
|
|
25977
|
+
level: "error",
|
|
25978
|
+
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.",
|
|
25979
|
+
file,
|
|
25980
|
+
line: i2 + 1,
|
|
25981
|
+
snippet: trimmed.substring(0, 100)
|
|
25982
|
+
});
|
|
25983
|
+
}
|
|
25984
|
+
return out2;
|
|
25985
|
+
}
|
|
25986
|
+
function findPythonHeadersSubscriptOpenRedirectFindings(code, file) {
|
|
25987
|
+
const out2 = [];
|
|
25988
|
+
const lines = code.split(`
|
|
25989
|
+
`);
|
|
25990
|
+
const requestSourceRe = /\b([A-Za-z_]\w*)\s*=\s*request\s*\.\s*(?:args|form|values|files|json|cookies|headers)(?:\s*\.\s*get\s*\(|\s*\[)/;
|
|
25991
|
+
const taintedVars = new Set;
|
|
25992
|
+
for (const line of lines) {
|
|
25993
|
+
const m = line.match(requestSourceRe);
|
|
25994
|
+
if (m)
|
|
25995
|
+
taintedVars.add(m[1]);
|
|
25996
|
+
}
|
|
25997
|
+
const safeWrapRe = /\b(?:urllib\.parse\.urlparse|urlparse)\s*\([\s\S]{0,120}?\bnetloc\b|\b(?:startswith)\s*\(/;
|
|
25998
|
+
const subscriptRe = /\b([A-Za-z_]\w*)\s*\.\s*headers\s*\[\s*(['"])([^'"]+)\2\s*\]\s*=\s*(.+)$/;
|
|
25999
|
+
for (let i2 = 0;i2 < lines.length; i2++) {
|
|
26000
|
+
const raw = lines[i2];
|
|
26001
|
+
const trimmed = raw.trim();
|
|
26002
|
+
if (!trimmed || trimmed.startsWith("#"))
|
|
26003
|
+
continue;
|
|
26004
|
+
const m = trimmed.match(subscriptRe);
|
|
26005
|
+
if (!m)
|
|
26006
|
+
continue;
|
|
26007
|
+
const key = m[3];
|
|
26008
|
+
if (key.toLowerCase() !== "location")
|
|
26009
|
+
continue;
|
|
26010
|
+
const rhs = m[4].replace(/\s*(?:#.*)?$/, "").trim();
|
|
26011
|
+
if (/^['"](?:\\.|[^'"\\])*['"]$/.test(rhs))
|
|
26012
|
+
continue;
|
|
26013
|
+
if (safeWrapRe.test(rhs))
|
|
26014
|
+
continue;
|
|
26015
|
+
const hasTaintedVar = [...taintedVars].some((v) => new RegExp(`\\b${v}\\b`).test(rhs));
|
|
26016
|
+
const inlineSource = /\brequest\s*\.\s*(?:args|form|values|files|cookies|headers|json)(?:\s*\.\s*get\s*\(|\s*\[)/.test(rhs);
|
|
26017
|
+
if (!hasTaintedVar && !inlineSource)
|
|
26018
|
+
continue;
|
|
26019
|
+
out2.push({
|
|
26020
|
+
id: `open_redirect-${file}-${i2 + 1}`,
|
|
26021
|
+
pass: "language-sources",
|
|
26022
|
+
category: "security",
|
|
26023
|
+
rule_id: "open_redirect",
|
|
26024
|
+
cwe: "CWE-601",
|
|
26025
|
+
severity: "high",
|
|
26026
|
+
level: "error",
|
|
26027
|
+
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.",
|
|
26028
|
+
file,
|
|
26029
|
+
line: i2 + 1,
|
|
26030
|
+
snippet: trimmed.substring(0, 100)
|
|
26031
|
+
});
|
|
26032
|
+
}
|
|
26033
|
+
return out2;
|
|
26034
|
+
}
|
|
26035
|
+
function findRustAppendHeaderTupleOpenRedirectFindings(code, file) {
|
|
26036
|
+
const out2 = [];
|
|
26037
|
+
const lines = code.split(`
|
|
26038
|
+
`);
|
|
26039
|
+
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;
|
|
26040
|
+
const extractorParams = new Set;
|
|
26041
|
+
for (const line of lines) {
|
|
26042
|
+
let m;
|
|
26043
|
+
extractorRe.lastIndex = 0;
|
|
26044
|
+
while ((m = extractorRe.exec(line)) !== null) {
|
|
26045
|
+
const name2 = m[1] ?? m[2];
|
|
26046
|
+
if (name2)
|
|
26047
|
+
extractorParams.add(name2);
|
|
26048
|
+
}
|
|
26049
|
+
}
|
|
26050
|
+
if (extractorParams.size === 0)
|
|
26051
|
+
return out2;
|
|
26052
|
+
const taintedVars = new Set(extractorParams);
|
|
26053
|
+
const letRe = /\blet\s+(?:mut\s+)?([A-Za-z_]\w*)\s*(?::[^=]+)?=\s*([^;]+);/g;
|
|
26054
|
+
for (let pass = 0;pass < 4; pass++) {
|
|
26055
|
+
const before = taintedVars.size;
|
|
26056
|
+
let lm;
|
|
26057
|
+
letRe.lastIndex = 0;
|
|
26058
|
+
while ((lm = letRe.exec(code)) !== null) {
|
|
26059
|
+
const name2 = lm[1];
|
|
26060
|
+
const rhs = lm[2];
|
|
26061
|
+
if (taintedVars.has(name2))
|
|
26062
|
+
continue;
|
|
26063
|
+
for (const tv of taintedVars) {
|
|
26064
|
+
if (new RegExp(`\\b${tv}\\b`).test(rhs)) {
|
|
26065
|
+
taintedVars.add(name2);
|
|
26066
|
+
break;
|
|
26067
|
+
}
|
|
26068
|
+
}
|
|
26069
|
+
}
|
|
26070
|
+
if (taintedVars.size === before)
|
|
26071
|
+
break;
|
|
26072
|
+
}
|
|
26073
|
+
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/;
|
|
26074
|
+
const tupleCallRe = /\.\s*(?:append_header|insert_header)\s*\(\s*\(\s*(['"])([^'"]+)\1\s*,\s*([^)]*?)\s*\)\s*\)/g;
|
|
26075
|
+
for (let i2 = 0;i2 < lines.length; i2++) {
|
|
26076
|
+
const raw = lines[i2];
|
|
26077
|
+
const trimmed = raw.trim();
|
|
26078
|
+
if (!trimmed || trimmed.startsWith("//"))
|
|
26079
|
+
continue;
|
|
26080
|
+
let cm;
|
|
26081
|
+
tupleCallRe.lastIndex = 0;
|
|
26082
|
+
while ((cm = tupleCallRe.exec(raw)) !== null) {
|
|
26083
|
+
const key = cm[2];
|
|
26084
|
+
if (key.toLowerCase() !== "location")
|
|
26085
|
+
continue;
|
|
26086
|
+
const valExpr = cm[3];
|
|
26087
|
+
if (/^['"](?:\\.|[^'"\\])*['"]$/.test(valExpr.trim()))
|
|
26088
|
+
continue;
|
|
26089
|
+
if (safeWrapRe.test(valExpr))
|
|
26090
|
+
continue;
|
|
26091
|
+
const hasTainted = [...taintedVars].some((v) => new RegExp(`\\b${v}\\b`).test(valExpr));
|
|
26092
|
+
if (!hasTainted)
|
|
26093
|
+
continue;
|
|
26094
|
+
out2.push({
|
|
26095
|
+
id: `open_redirect-${file}-${i2 + 1}`,
|
|
26096
|
+
pass: "language-sources",
|
|
26097
|
+
category: "security",
|
|
26098
|
+
rule_id: "open_redirect",
|
|
26099
|
+
cwe: "CWE-601",
|
|
26100
|
+
severity: "high",
|
|
26101
|
+
level: "error",
|
|
26102
|
+
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.",
|
|
26103
|
+
file,
|
|
26104
|
+
line: i2 + 1,
|
|
26105
|
+
snippet: trimmed.substring(0, 100)
|
|
26106
|
+
});
|
|
26107
|
+
break;
|
|
26108
|
+
}
|
|
26109
|
+
}
|
|
26110
|
+
return out2;
|
|
26111
|
+
}
|
|
26112
|
+
function findJsDomOpenRedirectFindings(code, file) {
|
|
26113
|
+
const out2 = [];
|
|
26114
|
+
const lines = code.split(`
|
|
26115
|
+
`);
|
|
26116
|
+
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/;
|
|
26117
|
+
const taintedVars = new Set;
|
|
26118
|
+
const assignRe = /\b(?:const|let|var)\s+([A-Za-z_$][\w$]*)\s*=\s*([^;\n]+)/g;
|
|
26119
|
+
let am;
|
|
26120
|
+
assignRe.lastIndex = 0;
|
|
26121
|
+
while ((am = assignRe.exec(code)) !== null) {
|
|
26122
|
+
if (domSourceRe.test(am[2]))
|
|
26123
|
+
taintedVars.add(am[1]);
|
|
26124
|
+
}
|
|
26125
|
+
for (let pass = 0;pass < 4; pass++) {
|
|
26126
|
+
const before = taintedVars.size;
|
|
26127
|
+
assignRe.lastIndex = 0;
|
|
26128
|
+
while ((am = assignRe.exec(code)) !== null) {
|
|
26129
|
+
if (taintedVars.has(am[1]))
|
|
26130
|
+
continue;
|
|
26131
|
+
for (const tv of taintedVars) {
|
|
26132
|
+
if (new RegExp(`\\b${tv}\\b`).test(am[2])) {
|
|
26133
|
+
taintedVars.add(am[1]);
|
|
26134
|
+
break;
|
|
26135
|
+
}
|
|
26136
|
+
}
|
|
26137
|
+
}
|
|
26138
|
+
if (taintedVars.size === before)
|
|
26139
|
+
break;
|
|
26140
|
+
}
|
|
26141
|
+
const safeWrapRe = /\b(?:startsWith|includes)\s*\(\s*['"`]\/[^'"`]*['"`]\s*\)|\bnew\s+URL\s*\([\s\S]{0,80}?\)\s*\.\s*(?:origin|hostname)\s*===/;
|
|
26142
|
+
const containsTaint = (expr) => {
|
|
26143
|
+
if (domSourceRe.test(expr))
|
|
26144
|
+
return true;
|
|
26145
|
+
for (const tv of taintedVars) {
|
|
26146
|
+
if (new RegExp(`\\b${tv}\\b`).test(expr))
|
|
26147
|
+
return true;
|
|
26148
|
+
}
|
|
26149
|
+
return false;
|
|
26150
|
+
};
|
|
26151
|
+
const hrefSinkRe = /\b(?:(?:window|document|self|top|parent)\s*\.\s*)?location\s*\.\s*href\s*=\s*([^;\n]+?)\s*;?\s*(?:\/\/.*)?$/;
|
|
26152
|
+
const locSinkRe = /\b(?:window|document|self|top|parent)\s*\.\s*location\s*=\s*([^;\n]+?)\s*;?\s*(?:\/\/.*)?$/;
|
|
26153
|
+
const contentSinkRe = /\.\s*content\s*=\s*([^\n]+?)\s*$/;
|
|
26154
|
+
const callSinkRe = /\b(?:(?:window|document|self|top|parent)\s*\.\s*)?location\s*\.\s*(?:assign|replace)\s*\(\s*([^)]+)\)/;
|
|
26155
|
+
const emit = (line, msg, snippet) => {
|
|
26156
|
+
out2.push({
|
|
26157
|
+
id: `open_redirect-${file}-${line}`,
|
|
26158
|
+
pass: "language-sources",
|
|
26159
|
+
category: "security",
|
|
26160
|
+
rule_id: "open_redirect",
|
|
26161
|
+
cwe: "CWE-601",
|
|
26162
|
+
severity: "high",
|
|
26163
|
+
level: "error",
|
|
26164
|
+
message: msg,
|
|
26165
|
+
file,
|
|
26166
|
+
line,
|
|
26167
|
+
snippet: snippet.substring(0, 100)
|
|
26168
|
+
});
|
|
26169
|
+
};
|
|
26170
|
+
const isLiteralOnly = (expr) => /^['"`](?:\\.|[^'"`\\])*['"`]$/.test(expr.trim());
|
|
26171
|
+
for (let i2 = 0;i2 < lines.length; i2++) {
|
|
26172
|
+
const raw = lines[i2];
|
|
26173
|
+
const trimmed = raw.trim();
|
|
26174
|
+
if (!trimmed || trimmed.startsWith("//"))
|
|
26175
|
+
continue;
|
|
26176
|
+
let matched = false;
|
|
26177
|
+
const hm = trimmed.match(hrefSinkRe);
|
|
26178
|
+
if (hm) {
|
|
26179
|
+
const rhs = hm[1].trim();
|
|
26180
|
+
if (!isLiteralOnly(rhs) && containsTaint(rhs) && !safeWrapRe.test(rhs)) {
|
|
26181
|
+
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);
|
|
26182
|
+
matched = true;
|
|
26183
|
+
}
|
|
26184
|
+
}
|
|
26185
|
+
if (!matched) {
|
|
26186
|
+
const lm = trimmed.match(locSinkRe);
|
|
26187
|
+
if (lm) {
|
|
26188
|
+
const rhs = lm[1].trim();
|
|
26189
|
+
if (!isLiteralOnly(rhs) && containsTaint(rhs) && !safeWrapRe.test(rhs)) {
|
|
26190
|
+
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);
|
|
26191
|
+
matched = true;
|
|
26192
|
+
}
|
|
26193
|
+
}
|
|
26194
|
+
}
|
|
26195
|
+
if (!matched) {
|
|
26196
|
+
const cm = trimmed.match(contentSinkRe);
|
|
26197
|
+
if (cm) {
|
|
26198
|
+
const rhs = cm[1].replace(/\s*\/\/.*$/, "").replace(/\s*;\s*$/, "").trim();
|
|
26199
|
+
if (!isLiteralOnly(rhs) && /['"`][^'"`]*\burl\s*=/i.test(rhs) && containsTaint(rhs) && !safeWrapRe.test(rhs)) {
|
|
26200
|
+
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);
|
|
26201
|
+
matched = true;
|
|
26202
|
+
}
|
|
26203
|
+
}
|
|
26204
|
+
}
|
|
26205
|
+
if (!matched) {
|
|
26206
|
+
const callm = trimmed.match(callSinkRe);
|
|
26207
|
+
if (callm) {
|
|
26208
|
+
const arg = callm[1].trim();
|
|
26209
|
+
if (!isLiteralOnly(arg) && containsTaint(arg) && !safeWrapRe.test(arg)) {
|
|
26210
|
+
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);
|
|
26211
|
+
}
|
|
26212
|
+
}
|
|
26213
|
+
}
|
|
26214
|
+
}
|
|
26215
|
+
return out2;
|
|
26216
|
+
}
|
|
26217
|
+
function findGoPluginOpenCodeInjectionFindings(code, file) {
|
|
26218
|
+
const findings = [];
|
|
26219
|
+
if (typeof code !== "string" || code.length === 0)
|
|
26220
|
+
return findings;
|
|
26221
|
+
if (!/\bplugin\s*\.\s*(?:Open|Lookup)\s*\(/.test(code))
|
|
26222
|
+
return findings;
|
|
26223
|
+
const lines = code.split(`
|
|
26224
|
+
`);
|
|
26225
|
+
const reqExtractRe = /\b\w+\s*\.\s*(?:FormValue|PostFormValue|URL\.Query\(\)\.Get|Header\.Get|Cookie)\s*\(/;
|
|
26226
|
+
const httpReqParamRe = /\*\s*http\.Request\b/;
|
|
26227
|
+
const callRe = /\bplugin\s*\.\s*(?:Open|Lookup)\s*\(\s*([^)]*)\s*\)/;
|
|
26228
|
+
const sinkLabel = (op) => op === "Lookup" ? "Go plugin.Lookup" : "Go plugin.Open";
|
|
26229
|
+
const funcs = [];
|
|
26230
|
+
let cur = null;
|
|
26231
|
+
for (let i2 = 0;i2 < lines.length; i2++) {
|
|
26232
|
+
const t = lines[i2].trim();
|
|
26233
|
+
if (/^func\b/.test(t)) {
|
|
26234
|
+
if (cur) {
|
|
26235
|
+
cur.end = i2 - 1;
|
|
26236
|
+
funcs.push(cur);
|
|
26237
|
+
}
|
|
26238
|
+
cur = { start: i2, end: lines.length - 1 };
|
|
26239
|
+
}
|
|
26240
|
+
}
|
|
26241
|
+
if (cur)
|
|
26242
|
+
funcs.push(cur);
|
|
26243
|
+
for (const fn of funcs) {
|
|
26244
|
+
const header = lines[fn.start];
|
|
26245
|
+
if (!httpReqParamRe.test(header))
|
|
26246
|
+
continue;
|
|
26247
|
+
const taintedVars = new Set;
|
|
26248
|
+
for (let pass = 0;pass < 3; pass++) {
|
|
26249
|
+
const before = taintedVars.size;
|
|
26250
|
+
for (let i2 = fn.start;i2 <= fn.end; i2++) {
|
|
26251
|
+
const line = lines[i2];
|
|
26252
|
+
const trimmed = line.trim();
|
|
26253
|
+
const assignMatch = trimmed.match(/^(\w+)\s*(?::=|=)\s*(.+?)(?:\s*\/\/.*)?$/);
|
|
26254
|
+
if (!assignMatch)
|
|
26255
|
+
continue;
|
|
26256
|
+
const lhs = assignMatch[1];
|
|
26257
|
+
const rhs = assignMatch[2];
|
|
26258
|
+
if (taintedVars.has(lhs))
|
|
26259
|
+
continue;
|
|
26260
|
+
if (reqExtractRe.test(rhs)) {
|
|
26261
|
+
taintedVars.add(lhs);
|
|
26262
|
+
continue;
|
|
26263
|
+
}
|
|
26264
|
+
for (const v of taintedVars) {
|
|
26265
|
+
const re = new RegExp(`\\b${v}\\b`);
|
|
26266
|
+
if (re.test(rhs)) {
|
|
26267
|
+
taintedVars.add(lhs);
|
|
26268
|
+
break;
|
|
26269
|
+
}
|
|
26270
|
+
}
|
|
26271
|
+
}
|
|
26272
|
+
if (taintedVars.size === before)
|
|
26273
|
+
break;
|
|
26274
|
+
}
|
|
26275
|
+
if (taintedVars.size === 0)
|
|
26276
|
+
continue;
|
|
26277
|
+
for (let i2 = fn.start;i2 <= fn.end; i2++) {
|
|
26278
|
+
const line = lines[i2];
|
|
26279
|
+
const m = line.match(callRe);
|
|
26280
|
+
if (!m)
|
|
26281
|
+
continue;
|
|
26282
|
+
const arg = m[1].trim();
|
|
26283
|
+
if (arg.length === 0)
|
|
26284
|
+
continue;
|
|
26285
|
+
if (/^"[^"]*"$/.test(arg))
|
|
26286
|
+
continue;
|
|
26287
|
+
let tainted = false;
|
|
26288
|
+
if (reqExtractRe.test(arg))
|
|
26289
|
+
tainted = true;
|
|
26290
|
+
else {
|
|
26291
|
+
for (const v of taintedVars) {
|
|
26292
|
+
const re = new RegExp(`\\b${v}\\b`);
|
|
26293
|
+
if (re.test(arg)) {
|
|
26294
|
+
tainted = true;
|
|
26295
|
+
break;
|
|
26296
|
+
}
|
|
26297
|
+
}
|
|
26298
|
+
}
|
|
26299
|
+
if (!tainted)
|
|
26300
|
+
continue;
|
|
26301
|
+
const op = /\bplugin\s*\.\s*Lookup\b/.test(line) ? "Lookup" : "Open";
|
|
26302
|
+
findings.push({
|
|
26303
|
+
id: `code_injection-${file}-${i2 + 1}-go-plugin-${op.toLowerCase()}`,
|
|
26304
|
+
pass: "language-sources",
|
|
26305
|
+
category: "security",
|
|
26306
|
+
rule_id: "code_injection",
|
|
26307
|
+
cwe: "CWE-94",
|
|
26308
|
+
severity: "critical",
|
|
26309
|
+
level: "error",
|
|
26310
|
+
message: `Code injection: ${sinkLabel(op)} called with a path/symbol derived ` + "from an *http.Request without an allow-list. Loading a plugin " + "runs its init() and exposes arbitrary exported symbols. Restrict " + "the path to a trusted directory or use a fixed allow-list.",
|
|
26311
|
+
file,
|
|
26312
|
+
line: i2 + 1,
|
|
26313
|
+
snippet: line.trim()
|
|
26314
|
+
});
|
|
26315
|
+
}
|
|
26316
|
+
}
|
|
26317
|
+
return findings;
|
|
26318
|
+
}
|
|
26319
|
+
function findJsIndirectEvalCodeInjectionFindings(code, file) {
|
|
26320
|
+
const findings = [];
|
|
26321
|
+
if (typeof code !== "string" || code.length === 0)
|
|
26322
|
+
return findings;
|
|
26323
|
+
if (!/\beval\b/.test(code))
|
|
26324
|
+
return findings;
|
|
26325
|
+
const lines = code.split(`
|
|
26326
|
+
`);
|
|
26327
|
+
const reqExtractRe = /\breq(?:uest)?\s*\.\s*(?:body|query|params|headers|cookies)\b/;
|
|
26328
|
+
const aliasRe = /^\s*(?:const|let|var)\s+(\w+)\s*=\s*(?:globalThis\s*\.\s*eval|global\s*\.\s*eval|window\s*\.\s*eval|self\s*\.\s*eval|eval)\s*;?\s*$/;
|
|
26329
|
+
const aliases = new Set;
|
|
26330
|
+
for (const line of lines) {
|
|
26331
|
+
const m = line.match(aliasRe);
|
|
26332
|
+
if (m)
|
|
26333
|
+
aliases.add(m[1]);
|
|
26334
|
+
}
|
|
26335
|
+
const taintedVars = new Set;
|
|
26336
|
+
const assignRe = /^\s*(?:const|let|var)\s+(\w+)\s*=\s*(.+?);?\s*$/;
|
|
26337
|
+
const reassignRe = /^\s*(\w+)\s*=\s*(.+?);?\s*$/;
|
|
26338
|
+
for (let pass = 0;pass < 3; pass++) {
|
|
26339
|
+
const before = taintedVars.size;
|
|
26340
|
+
for (const line of lines) {
|
|
26341
|
+
const m = line.match(assignRe) || line.match(reassignRe);
|
|
26342
|
+
if (!m)
|
|
26343
|
+
continue;
|
|
26344
|
+
const lhs = m[1];
|
|
26345
|
+
const rhs = m[2];
|
|
26346
|
+
if (taintedVars.has(lhs))
|
|
26347
|
+
continue;
|
|
26348
|
+
if (lhs === "const" || lhs === "let" || lhs === "var")
|
|
26349
|
+
continue;
|
|
26350
|
+
if (reqExtractRe.test(rhs)) {
|
|
26351
|
+
taintedVars.add(lhs);
|
|
26352
|
+
continue;
|
|
26353
|
+
}
|
|
26354
|
+
for (const v of taintedVars) {
|
|
26355
|
+
const re = new RegExp(`\\b${v}\\b`);
|
|
26356
|
+
if (re.test(rhs)) {
|
|
26357
|
+
taintedVars.add(lhs);
|
|
26358
|
+
break;
|
|
26359
|
+
}
|
|
26360
|
+
}
|
|
26361
|
+
}
|
|
26362
|
+
if (taintedVars.size === before)
|
|
26363
|
+
break;
|
|
26364
|
+
}
|
|
26365
|
+
const indirectCommaRe = /\(\s*0\s*,\s*eval\s*\)\s*\(\s*([^)]*)\s*\)/;
|
|
26366
|
+
const indirectMemberRe = /\b(?:globalThis|global|window|self)\s*\.\s*eval\s*\(\s*([^)]*)\s*\)/;
|
|
26367
|
+
for (let i2 = 0;i2 < lines.length; i2++) {
|
|
26368
|
+
const line = lines[i2];
|
|
26369
|
+
const trimmed = line.trim();
|
|
26370
|
+
if (!trimmed || trimmed.startsWith("//") || trimmed.startsWith("*"))
|
|
26371
|
+
continue;
|
|
26372
|
+
if (aliasRe.test(line))
|
|
26373
|
+
continue;
|
|
26374
|
+
let arg = null;
|
|
26375
|
+
let formLabel = "";
|
|
26376
|
+
let m = trimmed.match(indirectCommaRe);
|
|
26377
|
+
if (m) {
|
|
26378
|
+
arg = m[1].trim();
|
|
26379
|
+
formLabel = "(0, eval)(...) indirect eval";
|
|
26380
|
+
}
|
|
26381
|
+
if (!arg) {
|
|
26382
|
+
m = trimmed.match(indirectMemberRe);
|
|
26383
|
+
if (m) {
|
|
26384
|
+
arg = m[1].trim();
|
|
26385
|
+
formLabel = "globalThis.eval / window.eval / self.eval indirect eval";
|
|
26386
|
+
}
|
|
26387
|
+
}
|
|
26388
|
+
if (!arg && aliases.size > 0) {
|
|
26389
|
+
for (const a of aliases) {
|
|
26390
|
+
const aliasCallRe = new RegExp(`\\b${a}\\s*\\(\\s*([^)]*)\\s*\\)`);
|
|
26391
|
+
const mm = trimmed.match(aliasCallRe);
|
|
26392
|
+
if (mm) {
|
|
26393
|
+
arg = mm[1].trim();
|
|
26394
|
+
formLabel = `aliased eval reference \`${a}(...)\``;
|
|
26395
|
+
break;
|
|
26396
|
+
}
|
|
26397
|
+
}
|
|
26398
|
+
}
|
|
26399
|
+
if (arg === null)
|
|
26400
|
+
continue;
|
|
26401
|
+
if (arg.length === 0)
|
|
26402
|
+
continue;
|
|
26403
|
+
if (/^['"`][^'"`]*['"`]$/.test(arg))
|
|
26404
|
+
continue;
|
|
26405
|
+
let tainted = false;
|
|
26406
|
+
if (reqExtractRe.test(arg))
|
|
26407
|
+
tainted = true;
|
|
26408
|
+
else {
|
|
26409
|
+
for (const v of taintedVars) {
|
|
26410
|
+
const re = new RegExp(`\\b${v}\\b`);
|
|
26411
|
+
if (re.test(arg)) {
|
|
26412
|
+
tainted = true;
|
|
26413
|
+
break;
|
|
26414
|
+
}
|
|
26415
|
+
}
|
|
26416
|
+
}
|
|
26417
|
+
if (!tainted)
|
|
26418
|
+
continue;
|
|
26419
|
+
findings.push({
|
|
26420
|
+
id: `code_injection-${file}-${i2 + 1}-js-indirect-eval`,
|
|
26421
|
+
pass: "language-sources",
|
|
26422
|
+
category: "security",
|
|
26423
|
+
rule_id: "code_injection",
|
|
26424
|
+
cwe: "CWE-94",
|
|
26425
|
+
severity: "critical",
|
|
26426
|
+
level: "error",
|
|
26427
|
+
message: `Code injection: ${formLabel} called with a value derived from ` + "an HTTP request body/query/headers. Indirect eval forms still " + "execute arbitrary code in the global scope. Remove the eval and " + "parse the input with a safe data parser instead.",
|
|
26428
|
+
file,
|
|
26429
|
+
line: i2 + 1,
|
|
26430
|
+
snippet: trimmed
|
|
26431
|
+
});
|
|
26432
|
+
}
|
|
26433
|
+
return findings;
|
|
26434
|
+
}
|
|
26435
|
+
function findPythonInteractiveInterpreterCodeInjectionFindings(code, file) {
|
|
26436
|
+
const findings = [];
|
|
26437
|
+
if (typeof code !== "string" || code.length === 0)
|
|
26438
|
+
return findings;
|
|
26439
|
+
if (!/^\s*import\s+code\b/m.test(code))
|
|
26440
|
+
return findings;
|
|
26441
|
+
if (!/\bcode\s*\.\s*(?:InteractiveInterpreter|InteractiveConsole|compile_command)\b/.test(code)) {
|
|
26442
|
+
return findings;
|
|
26443
|
+
}
|
|
26444
|
+
const lines = code.split(`
|
|
26445
|
+
`);
|
|
26446
|
+
const reqExtractRe = /\brequest\s*\.\s*(?:args|form|values|files|json|cookies|headers|get_data|get_json)\b/;
|
|
26447
|
+
const taintedVars = new Set;
|
|
26448
|
+
const assignRe = /^\s*(\w+)\s*=\s*(.+?)\s*(?:#.*)?$/;
|
|
26449
|
+
for (let pass = 0;pass < 3; pass++) {
|
|
26450
|
+
const before = taintedVars.size;
|
|
26451
|
+
for (const line of lines) {
|
|
26452
|
+
const m = line.match(assignRe);
|
|
26453
|
+
if (!m)
|
|
26454
|
+
continue;
|
|
26455
|
+
const lhs = m[1];
|
|
26456
|
+
const rhs = m[2];
|
|
26457
|
+
if (taintedVars.has(lhs))
|
|
26458
|
+
continue;
|
|
26459
|
+
if (reqExtractRe.test(rhs)) {
|
|
26460
|
+
taintedVars.add(lhs);
|
|
26461
|
+
continue;
|
|
26462
|
+
}
|
|
26463
|
+
for (const v of taintedVars) {
|
|
26464
|
+
const re = new RegExp(`\\b${v}\\b`);
|
|
26465
|
+
if (re.test(rhs)) {
|
|
26466
|
+
taintedVars.add(lhs);
|
|
26467
|
+
break;
|
|
26468
|
+
}
|
|
26469
|
+
}
|
|
26470
|
+
}
|
|
26471
|
+
if (taintedVars.size === before)
|
|
26472
|
+
break;
|
|
26473
|
+
}
|
|
26474
|
+
const callRe = /\bcode\s*\.\s*(?:InteractiveInterpreter|InteractiveConsole)\s*\([^)]*\)\s*\.\s*(runsource|runcode|push|interact)\s*\(\s*([^),]+)/;
|
|
26475
|
+
const compileRe = /\bcode\s*\.\s*compile_command\s*\(\s*([^),]+)/;
|
|
26476
|
+
for (let i2 = 0;i2 < lines.length; i2++) {
|
|
26477
|
+
const line = lines[i2];
|
|
26478
|
+
const trimmed = line.trim();
|
|
26479
|
+
let arg = null;
|
|
26480
|
+
let formLabel = "";
|
|
26481
|
+
const m1 = trimmed.match(callRe);
|
|
26482
|
+
if (m1) {
|
|
26483
|
+
arg = m1[2].trim();
|
|
26484
|
+
formLabel = `code.${/Interpreter/.test(trimmed) ? "InteractiveInterpreter" : "InteractiveConsole"}().${m1[1]}`;
|
|
26485
|
+
}
|
|
26486
|
+
if (!arg) {
|
|
26487
|
+
const m2 = trimmed.match(compileRe);
|
|
26488
|
+
if (m2) {
|
|
26489
|
+
arg = m2[1].trim();
|
|
26490
|
+
formLabel = "code.compile_command";
|
|
26491
|
+
}
|
|
26492
|
+
}
|
|
26493
|
+
if (arg === null)
|
|
26494
|
+
continue;
|
|
26495
|
+
if (arg.length === 0)
|
|
26496
|
+
continue;
|
|
26497
|
+
if (/^['"][^'"]*['"]$/.test(arg))
|
|
26498
|
+
continue;
|
|
26499
|
+
let tainted = false;
|
|
26500
|
+
if (reqExtractRe.test(arg))
|
|
26501
|
+
tainted = true;
|
|
26502
|
+
else {
|
|
26503
|
+
for (const v of taintedVars) {
|
|
26504
|
+
const re = new RegExp(`\\b${v}\\b`);
|
|
26505
|
+
if (re.test(arg)) {
|
|
26506
|
+
tainted = true;
|
|
26507
|
+
break;
|
|
26508
|
+
}
|
|
26509
|
+
}
|
|
26510
|
+
}
|
|
26511
|
+
if (!tainted)
|
|
26512
|
+
continue;
|
|
26513
|
+
findings.push({
|
|
26514
|
+
id: `code_injection-${file}-${i2 + 1}-py-interactive-interpreter`,
|
|
26515
|
+
pass: "language-sources",
|
|
26516
|
+
category: "security",
|
|
26517
|
+
rule_id: "code_injection",
|
|
26518
|
+
cwe: "CWE-94",
|
|
26519
|
+
severity: "critical",
|
|
26520
|
+
level: "error",
|
|
26521
|
+
message: `Code injection: ${formLabel}(...) called with a value derived from ` + "a Flask request extractor. The Python `code` module compiles and " + "executes arbitrary source. Remove the call and validate input " + "against a fixed allow-list instead.",
|
|
26522
|
+
file,
|
|
26523
|
+
line: i2 + 1,
|
|
26524
|
+
snippet: trimmed
|
|
26525
|
+
});
|
|
26526
|
+
}
|
|
26527
|
+
return findings;
|
|
26528
|
+
}
|
|
26529
|
+
function findRustEvalCrateCodeInjectionFindings(code, file) {
|
|
26530
|
+
const findings = [];
|
|
26531
|
+
if (typeof code !== "string" || code.length === 0)
|
|
26532
|
+
return findings;
|
|
26533
|
+
if (!/\b(?:evalexpr\s*::\s*eval|libloading\s*::\s*Library\s*::\s*new|\.\s*load\s*\([^)]*\)\s*\.\s*(?:exec|eval|call))/.test(code)) {
|
|
26534
|
+
return findings;
|
|
26535
|
+
}
|
|
26536
|
+
const lines = code.split(`
|
|
26537
|
+
`);
|
|
26538
|
+
const extractorTypeRe = /:\s*(?:String|Bytes|bytes::Bytes|axum::body::Bytes|web::Query\b|web::Path\b|web::Form\b|web::Json\b|HttpRequest\b|actix_web::HttpRequest\b)/;
|
|
26539
|
+
const fns = [];
|
|
26540
|
+
let cur = null;
|
|
26541
|
+
for (let i2 = 0;i2 < lines.length; i2++) {
|
|
26542
|
+
const t = lines[i2];
|
|
26543
|
+
if (/^\s*(?:pub\s+)?(?:async\s+)?fn\s+\w+\s*\(/.test(t)) {
|
|
26544
|
+
if (cur) {
|
|
26545
|
+
cur.end = i2 - 1;
|
|
26546
|
+
fns.push(cur);
|
|
26547
|
+
}
|
|
26548
|
+
cur = { start: i2, end: lines.length - 1, tainted: new Set };
|
|
26549
|
+
const headerJoined = (() => {
|
|
26550
|
+
let j = i2;
|
|
26551
|
+
let s = "";
|
|
26552
|
+
while (j < lines.length && !/\{\s*$/.test(s)) {
|
|
26553
|
+
s += lines[j];
|
|
26554
|
+
if (/\{\s*$/.test(lines[j]))
|
|
26555
|
+
break;
|
|
26556
|
+
j++;
|
|
26557
|
+
if (j - i2 > 12)
|
|
26558
|
+
break;
|
|
26559
|
+
}
|
|
26560
|
+
return s;
|
|
26561
|
+
})();
|
|
26562
|
+
const open = headerJoined.indexOf("(");
|
|
26563
|
+
const close = headerJoined.lastIndexOf(")");
|
|
26564
|
+
if (open !== -1 && close > open) {
|
|
26565
|
+
const params = headerJoined.substring(open + 1, close);
|
|
26566
|
+
let depth = 0;
|
|
26567
|
+
let buf = "";
|
|
26568
|
+
const parts2 = [];
|
|
26569
|
+
for (const ch of params) {
|
|
26570
|
+
if (ch === "<" || ch === "(")
|
|
26571
|
+
depth++;
|
|
26572
|
+
else if (ch === ">" || ch === ")")
|
|
26573
|
+
depth--;
|
|
26574
|
+
if (ch === "," && depth === 0) {
|
|
26575
|
+
parts2.push(buf);
|
|
26576
|
+
buf = "";
|
|
26577
|
+
continue;
|
|
26578
|
+
}
|
|
26579
|
+
buf += ch;
|
|
26580
|
+
}
|
|
26581
|
+
if (buf.trim().length > 0)
|
|
26582
|
+
parts2.push(buf);
|
|
26583
|
+
for (const p of parts2) {
|
|
26584
|
+
const pm = p.match(/(?:mut\s+)?(\w+)\s*:/);
|
|
26585
|
+
if (!pm)
|
|
26586
|
+
continue;
|
|
26587
|
+
if (extractorTypeRe.test(p))
|
|
26588
|
+
cur.tainted.add(pm[1]);
|
|
26589
|
+
}
|
|
26590
|
+
}
|
|
26591
|
+
}
|
|
26592
|
+
}
|
|
26593
|
+
if (cur)
|
|
26594
|
+
fns.push(cur);
|
|
26595
|
+
for (const fn of fns) {
|
|
26596
|
+
for (let pass = 0;pass < 3; pass++) {
|
|
26597
|
+
const before = fn.tainted.size;
|
|
26598
|
+
for (let i2 = fn.start;i2 <= fn.end; i2++) {
|
|
26599
|
+
const t = lines[i2].trim();
|
|
26600
|
+
const m = t.match(/^let\s+(?:mut\s+)?(\w+)\s*(?::\s*[^=]+)?=\s*(.+?);?$/);
|
|
26601
|
+
if (!m)
|
|
26602
|
+
continue;
|
|
26603
|
+
const lhs = m[1];
|
|
26604
|
+
const rhs = m[2];
|
|
26605
|
+
if (fn.tainted.has(lhs))
|
|
26606
|
+
continue;
|
|
26607
|
+
for (const v of fn.tainted) {
|
|
26608
|
+
const re = new RegExp(`\\b${v}\\b`);
|
|
26609
|
+
if (re.test(rhs)) {
|
|
26610
|
+
fn.tainted.add(lhs);
|
|
26611
|
+
break;
|
|
26612
|
+
}
|
|
26613
|
+
}
|
|
26614
|
+
}
|
|
26615
|
+
if (fn.tainted.size === before)
|
|
26616
|
+
break;
|
|
26617
|
+
}
|
|
26618
|
+
}
|
|
26619
|
+
const evalExprRe = /\bevalexpr\s*::\s*eval(?:_with_context|_boolean|_int|_float|_string|_tuple|_empty)?\s*\(\s*([^)]+)\s*\)/;
|
|
26620
|
+
const libloadingRe = /\blibloading\s*::\s*Library\s*::\s*new\s*\(\s*([^)]+)\s*\)/;
|
|
26621
|
+
const luaLoadRe = /\.\s*load\s*\(\s*([^)]+)\s*\)\s*\.\s*(?:exec|eval|call)\b/;
|
|
26622
|
+
for (const fn of fns) {
|
|
26623
|
+
if (fn.tainted.size === 0)
|
|
26624
|
+
continue;
|
|
26625
|
+
for (let i2 = fn.start;i2 <= fn.end; i2++) {
|
|
26626
|
+
const line = lines[i2];
|
|
26627
|
+
const trimmed = line.trim();
|
|
26628
|
+
let arg = null;
|
|
26629
|
+
let formLabel = "";
|
|
26630
|
+
let m = trimmed.match(evalExprRe);
|
|
26631
|
+
if (m) {
|
|
26632
|
+
arg = m[1].trim();
|
|
26633
|
+
formLabel = "evalexpr::eval";
|
|
26634
|
+
}
|
|
26635
|
+
if (!arg) {
|
|
26636
|
+
m = trimmed.match(libloadingRe);
|
|
26637
|
+
if (m) {
|
|
26638
|
+
arg = m[1].trim();
|
|
26639
|
+
formLabel = "libloading::Library::new";
|
|
26640
|
+
}
|
|
26641
|
+
}
|
|
26642
|
+
if (!arg) {
|
|
26643
|
+
m = trimmed.match(luaLoadRe);
|
|
26644
|
+
if (m) {
|
|
26645
|
+
arg = m[1].trim();
|
|
26646
|
+
formLabel = "mlua/rlua Lua::load().{exec|eval|call}";
|
|
26647
|
+
}
|
|
26648
|
+
}
|
|
26649
|
+
if (arg === null)
|
|
26650
|
+
continue;
|
|
26651
|
+
if (arg.length === 0)
|
|
26652
|
+
continue;
|
|
26653
|
+
let unwrapped = arg.replace(/^&\s*/, "").trim();
|
|
26654
|
+
if (/^"[^"]*"$/.test(unwrapped))
|
|
26655
|
+
continue;
|
|
26656
|
+
let tainted = false;
|
|
26657
|
+
for (const v of fn.tainted) {
|
|
26658
|
+
const re = new RegExp(`\\b${v}\\b`);
|
|
26659
|
+
if (re.test(unwrapped)) {
|
|
26660
|
+
tainted = true;
|
|
26661
|
+
break;
|
|
26662
|
+
}
|
|
26663
|
+
}
|
|
26664
|
+
if (!tainted)
|
|
26665
|
+
continue;
|
|
26666
|
+
findings.push({
|
|
26667
|
+
id: `code_injection-${file}-${i2 + 1}-rust-eval-crate`,
|
|
26668
|
+
pass: "language-sources",
|
|
26669
|
+
category: "security",
|
|
26670
|
+
rule_id: "code_injection",
|
|
26671
|
+
cwe: "CWE-94",
|
|
26672
|
+
severity: "critical",
|
|
26673
|
+
level: "error",
|
|
26674
|
+
message: `Code injection: ${formLabel}(...) called with a value derived ` + "from an HTTP request extractor (body / Query / Path / Form / " + "Json / HttpRequest). The expression / library / Lua chunk is " + "executed as code. Remove the dynamic-eval path or restrict " + "input to a fixed allow-list.",
|
|
26675
|
+
file,
|
|
26676
|
+
line: i2 + 1,
|
|
26677
|
+
snippet: trimmed
|
|
26678
|
+
});
|
|
26679
|
+
}
|
|
26680
|
+
}
|
|
26681
|
+
return findings;
|
|
26682
|
+
}
|
|
25910
26683
|
|
|
25911
26684
|
// ../circle-ir/dist/analysis/passes/sink-filter-pass.js
|
|
25912
26685
|
var JS_XSS_SANITIZERS = [
|
|
@@ -27371,13 +28144,13 @@ class TaintPropagationPass {
|
|
|
27371
28144
|
}));
|
|
27372
28145
|
const arrayFlows = detectArrayElementFlows(calls, sources, sinks, constProp.taintedArrayElements, constProp.unreachableLines, types) ?? [];
|
|
27373
28146
|
for (const f of arrayFlows) {
|
|
27374
|
-
if (!flows.some((x) => x.source_line === f.source_line && x.sink_line === f.sink_line)) {
|
|
28147
|
+
if (!flows.some((x) => x.source_line === f.source_line && x.sink_line === f.sink_line && x.sink_type === f.sink_type)) {
|
|
27375
28148
|
flows.push(f);
|
|
27376
28149
|
}
|
|
27377
28150
|
}
|
|
27378
28151
|
const collectionFlows = detectCollectionFlows(calls, sources, sinks, constProp.tainted, constProp.unreachableLines, ctx.code, types) ?? [];
|
|
27379
28152
|
for (const f of collectionFlows) {
|
|
27380
|
-
if (flows.some((x) => x.source_line === f.source_line && x.sink_line === f.sink_line))
|
|
28153
|
+
if (flows.some((x) => x.source_line === f.source_line && x.sink_line === f.sink_line && x.sink_type === f.sink_type))
|
|
27381
28154
|
continue;
|
|
27382
28155
|
const flowForCheck = {
|
|
27383
28156
|
source: { line: f.source_line },
|
|
@@ -27399,7 +28172,7 @@ class TaintPropagationPass {
|
|
|
27399
28172
|
}
|
|
27400
28173
|
const paramFlows = detectParameterSinkFlows(types, calls, sources, sinks, constProp.unreachableLines, constProp.tainted, ctx.code) ?? [];
|
|
27401
28174
|
for (const f of paramFlows) {
|
|
27402
|
-
if (!flows.some((x) => x.source_line === f.source_line && x.sink_line === f.sink_line)) {
|
|
28175
|
+
if (!flows.some((x) => x.source_line === f.source_line && x.sink_line === f.sink_line && x.sink_type === f.sink_type)) {
|
|
27403
28176
|
flows.push(f);
|
|
27404
28177
|
}
|
|
27405
28178
|
}
|
|
@@ -27837,7 +28610,7 @@ function detectParameterSinkFlows(types, calls, sources, sinks, unreachableLines
|
|
|
27837
28610
|
}
|
|
27838
28611
|
const paramSource = methodParamSources.get(arg.variable);
|
|
27839
28612
|
if (paramSource) {
|
|
27840
|
-
const exists = flows.some((f) => f.source_line === paramSource.line && f.sink_line === sink.line);
|
|
28613
|
+
const exists = flows.some((f) => f.source_line === paramSource.line && f.sink_line === sink.line && f.sink_type === sink.type);
|
|
27841
28614
|
if (!exists) {
|
|
27842
28615
|
if (typeof code === "string" && isReassignedToLiteralBetween(code, arg.variable, paramSource.line, sink.line)) {
|
|
27843
28616
|
continue;
|
|
@@ -39034,7 +39807,7 @@ var colors = {
|
|
|
39034
39807
|
};
|
|
39035
39808
|
|
|
39036
39809
|
// src/version.ts
|
|
39037
|
-
var version = "3.
|
|
39810
|
+
var version = "3.133.0";
|
|
39038
39811
|
|
|
39039
39812
|
// src/formatters.ts
|
|
39040
39813
|
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.133.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.133.0"
|
|
70
70
|
},
|
|
71
71
|
"devDependencies": {
|
|
72
72
|
"@types/node": "^25.5.0",
|