use super::dominators::{self, dominates};
use super::{
AnalysisContext, CfgAnalysis, CfgFinding, Confidence, is_auth_call, is_entry_point_func,
is_sink,
};
use crate::cfg::StmtKind;
use crate::labels::DataLabel;
use crate::patterns::Severity;
use crate::symbol::Lang;
use petgraph::graph::NodeIndex;
pub struct AuthGap;
fn is_privileged_sink(info: &crate::cfg::NodeInfo) -> bool {
use crate::labels::Cap;
info.taint.labels.iter().any(|l| {
if let DataLabel::Sink(caps) = l {
caps.intersects(Cap::SHELL_ESCAPE | Cap::FILE_IO)
} else {
false
}
})
}
fn has_web_handler_params(ctx: &AnalysisContext, func_name: &str) -> bool {
let param_names: Vec<&str> = ctx
.func_summaries
.values()
.filter(|s| ctx.cfg[s.entry].ast.enclosing_func.as_deref() == Some(func_name))
.flat_map(|s| s.param_names.iter().map(|p| p.as_str()))
.collect();
match ctx.lang {
Lang::Rust => {
let web_params = [
"request",
"req",
"http_request",
"httprequest",
"json",
"query",
"form",
"payload",
"body",
"web",
];
param_names
.iter()
.any(|p| web_params.contains(&p.to_ascii_lowercase().as_str()))
}
Lang::JavaScript | Lang::TypeScript => {
let lower: Vec<String> = param_names.iter().map(|p| p.to_ascii_lowercase()).collect();
let has_req = lower
.iter()
.any(|p| p == "req" || p == "request" || p == "ctx");
let has_res = lower.iter().any(|p| p == "res" || p == "response");
(has_req && has_res) || lower.iter().any(|p| p == "ctx")
}
Lang::Python => {
let lower: Vec<String> = param_names.iter().map(|p| p.to_ascii_lowercase()).collect();
lower.iter().any(|p| p == "request" || p == "req")
}
Lang::Go => {
let lower: Vec<String> = param_names.iter().map(|p| p.to_ascii_lowercase()).collect();
let has_writer = lower.iter().any(|p| p == "w" || p == "writer" || p == "rw");
let has_request = lower
.iter()
.any(|p| p == "r" || p == "req" || p == "request");
has_writer && has_request
}
Lang::Java => {
let lower: Vec<String> = param_names.iter().map(|p| p.to_ascii_lowercase()).collect();
lower
.iter()
.any(|p| p == "request" || p == "req" || p.contains("httpservlet"))
}
Lang::Ruby => {
let lower: Vec<String> = param_names.iter().map(|p| p.to_ascii_lowercase()).collect();
lower
.iter()
.any(|p| p == "request" || p == "req" || p == "params")
}
Lang::Php => {
let lower: Vec<String> = param_names.iter().map(|p| p.to_ascii_lowercase()).collect();
lower
.iter()
.any(|p| p == "$request" || p == "request" || p == "$req")
}
_ => false,
}
}
fn is_web_entrypoint(ctx: &AnalysisContext, func_name: &str) -> bool {
if func_name == "main" {
return has_web_handler_params(ctx, func_name);
}
if !is_entry_point_func(func_name, ctx.lang) {
return false;
}
let has_params = has_web_handler_params(ctx, func_name);
let name_lower = func_name.to_ascii_lowercase();
let strong_handler_name = name_lower.starts_with("handle_")
|| name_lower.starts_with("route_")
|| name_lower.starts_with("api_")
|| name_lower == "handler";
has_params || strong_handler_name
}
fn find_web_entry_point_functions(ctx: &AnalysisContext) -> Vec<String> {
let mut entry_funcs = Vec::new();
for idx in ctx.cfg.node_indices() {
if let Some(func_name) = &ctx.cfg[idx].ast.enclosing_func
&& is_web_entrypoint(ctx, func_name)
&& !entry_funcs.contains(func_name)
{
entry_funcs.push(func_name.clone());
}
}
entry_funcs
}
fn find_auth_nodes(ctx: &AnalysisContext) -> Vec<NodeIndex> {
ctx.cfg
.node_indices()
.filter(|&idx| is_auth_call(&ctx.cfg[idx], ctx.lang))
.collect()
}
impl CfgAnalysis for AuthGap {
fn name(&self) -> &'static str {
"auth-gap"
}
fn run(&self, ctx: &AnalysisContext) -> Vec<CfgFinding> {
let body_auth_level = crate::state::classify_auth_decorators(ctx.lang, ctx.auth_decorators);
if body_auth_level >= crate::state::domain::AuthLevel::Authed {
return Vec::new();
}
let doms = dominators::compute_dominators(ctx.cfg, ctx.entry);
let entry_funcs = find_web_entry_point_functions(ctx);
let auth_nodes = find_auth_nodes(ctx);
if entry_funcs.is_empty() {
return Vec::new();
}
let mut findings = Vec::new();
for idx in ctx.cfg.node_indices() {
let info = &ctx.cfg[idx];
if !is_sink(info) && info.kind != StmtKind::Call {
continue;
}
let func_name = match &info.ast.enclosing_func {
Some(name) if entry_funcs.contains(name) => name.clone(),
_ => continue,
};
if !is_sink(info) {
continue;
}
if !is_privileged_sink(info) {
continue;
}
let has_auth = auth_nodes
.iter()
.any(|&auth_idx| dominates(&doms, auth_idx, idx));
if !has_auth {
let callee_desc = info.call.callee.as_deref().unwrap_or("(sensitive op)");
findings.push(CfgFinding {
rule_id: "cfg-auth-gap".to_string(),
title: "Missing auth check".to_string(),
severity: Severity::High,
confidence: Confidence::Medium,
span: info.ast.span,
message: format!(
"Sensitive operation `{callee_desc}` in web handler `{func_name}` \
has no dominating authentication check"
),
evidence: vec![idx],
score: None,
});
}
}
findings
}
}