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.
Files changed (2) hide show
  1. package/dist/cli.js +787 -14
  2. 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: "ssrf", cwe: "CWE-601", severity: "high", arg_positions: [0] },
10906
- { method: "sendRedirect", type: "ssrf", cwe: "CWE-601", severity: "high", arg_positions: [0] },
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.131.0";
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.131.0",
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.131.0"
69
+ "circle-ir": "^3.133.0"
70
70
  },
71
71
  "devDependencies": {
72
72
  "@types/node": "^25.5.0",