use php_ast::ast::{Expr, ExprKind, StringPart};
use crate::context::Context;
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, }
pub fn classify_sink(fn_name: &str) -> Option<SinkKind> {
match fn_name.to_lowercase().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),
_ => None,
}
}
pub fn is_expr_tainted<'arena, 'src>(expr: &Expr<'arena, 'src>, ctx: &Context) -> bool {
match &expr.kind {
ExprKind::Variable(name) => {
let n = name.as_ref().trim_start_matches('$');
is_superglobal(n) || ctx.is_tainted(n)
}
ExprKind::ArrayAccess(aa) => {
is_expr_tainted(aa.array, ctx)
}
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.is_some_and(|e| is_expr_tainted(e, ctx))
|| is_expr_tainted(t.else_expr, ctx)
}
ExprKind::Cast(_kind, inner) => is_expr_tainted(inner, ctx),
_ => false,
}
}