use php_ast::owned::{Expr, ExprKind, StringPart};
use crate::flow_state::FlowState;
pub const SUPERGLOBALS: &[&str] = &[
"_GET", "_POST", "_REQUEST", "_COOKIE", "_FILES", "_SERVER", "_ENV",
];
pub fn is_superglobal(name: &str) -> bool {
SUPERGLOBALS.contains(&name)
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum SinkKind {
Html, Sql, Shell, File, Unserialize, }
impl SinkKind {
pub fn tainted_arg_indices(self) -> Option<&'static [usize]> {
match self {
SinkKind::Html | SinkKind::Sql | SinkKind::Shell => None,
SinkKind::File => Some(&[0]),
SinkKind::Unserialize => Some(&[0]),
}
}
}
pub fn classify_sink(fn_name: &str) -> Option<SinkKind> {
match crate::util::php_ident_lowercase(fn_name).as_str() {
"echo" | "print" | "printf" | "vprintf" | "fprintf" | "header" | "setcookie" => {
Some(SinkKind::Html)
}
"mysql_query" | "mysqli_query" | "pg_query" | "pg_exec" | "sqlite_query"
| "mssql_query" => Some(SinkKind::Sql),
"system" | "exec" | "shell_exec" | "passthru" | "popen" | "proc_open" | "pcntl_exec" => {
Some(SinkKind::Shell)
}
"fopen" | "file_get_contents" | "file_put_contents" | "readfile" | "file" | "unlink" => {
Some(SinkKind::File)
}
"unserialize" => Some(SinkKind::Unserialize),
_ => None,
}
}
pub fn classify_method_sink(
db: &dyn crate::db::MirDatabase,
fqcn: &str,
method_name: &str,
) -> Option<SinkKind> {
let method = crate::util::php_ident_lowercase(method_name);
let is_a = |base: &str| {
fqcn.eq_ignore_ascii_case(base) || crate::db::extends_or_implements(db, fqcn, base)
};
if is_a("PDO") && matches!(method.as_str(), "query" | "exec" | "prepare") {
return Some(SinkKind::Sql);
}
if is_a("mysqli")
&& matches!(
method.as_str(),
"query" | "prepare" | "real_query" | "multi_query"
)
{
return Some(SinkKind::Sql);
}
if is_a("SQLite3") && matches!(method.as_str(), "query" | "exec" | "prepare") {
return Some(SinkKind::Sql);
}
None
}
pub fn is_expr_tainted(expr: &Expr, ctx: &FlowState) -> bool {
match &expr.kind {
ExprKind::Variable(name) => {
let n = name.trim_start_matches('$');
is_superglobal(n) || ctx.is_tainted(n)
}
ExprKind::ArrayAccess(aa) => {
is_expr_tainted(&aa.array, ctx)
}
ExprKind::Parenthesized(inner) => is_expr_tainted(inner, ctx),
ExprKind::PropertyAccess(pa) => {
if let ExprKind::Variable(obj_var) = &pa.object.kind {
if let Some(prop_name) =
crate::expr::helpers::extract_string_from_expr(&pa.property)
{
return ctx.is_prop_tainted(obj_var.trim_start_matches('$'), &prop_name);
}
}
false
}
ExprKind::Assign(a) => is_expr_tainted(&a.value, ctx),
ExprKind::Binary(op) => is_expr_tainted(&op.left, ctx) || is_expr_tainted(&op.right, ctx),
ExprKind::UnaryPrefix(u) => is_expr_tainted(&u.operand, ctx),
ExprKind::InterpolatedString(parts) | ExprKind::Heredoc { parts, .. } => {
parts.iter().any(|p| match p {
StringPart::Expr(e) => is_expr_tainted(e, ctx),
StringPart::Literal(_) => false,
})
}
ExprKind::Ternary(t) => {
t.then_expr
.as_deref()
.is_some_and(|e| is_expr_tainted(e, ctx))
|| is_expr_tainted(&t.else_expr, ctx)
}
ExprKind::Cast(kind, inner) => match kind {
php_ast::ast::CastKind::Int
| php_ast::ast::CastKind::Float
| php_ast::ast::CastKind::Bool => false,
_ => is_expr_tainted(inner, ctx),
},
ExprKind::Match(m) => m.arms.iter().any(|arm| is_expr_tainted(&arm.body, ctx)),
ExprKind::Array(elements) => elements.iter().any(|el| is_expr_tainted(&el.value, ctx)),
_ => false,
}
}