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 sink_param_name(fn_name_lower: &str) -> Option<&'static str> {
match fn_name_lower {
"fopen" | "file_get_contents" | "file_put_contents" | "readfile" | "file" | "unlink" => {
Some("filename")
}
"move_uploaded_file" => Some("to"),
"unserialize" => Some("data"),
_ => None,
}
}
pub fn sink_positional_index_override(fn_name_lower: &str) -> Option<usize> {
match fn_name_lower {
"move_uploaded_file" => Some(1),
_ => None,
}
}
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"
| "move_uploaded_file" => 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 taint_sink_issue(kind: &str) -> mir_issues::IssueKind {
match kind {
"llm_prompt" => mir_issues::IssueKind::TaintedLlmPrompt,
"html" => mir_issues::IssueKind::TaintedHtml,
"sql" => mir_issues::IssueKind::TaintedSql,
"shell" => mir_issues::IssueKind::TaintedShell,
other => mir_issues::IssueKind::TaintedInput {
sink: other.to_string(),
},
}
}
pub fn is_expr_tainted(
expr: &Expr,
ctx: &FlowState,
db: &dyn crate::db::MirDatabase,
file: &str,
) -> 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, db, file)
}
ExprKind::Parenthesized(inner) => is_expr_tainted(inner, ctx, db, file),
ExprKind::PropertyAccess(pa) | ExprKind::NullsafePropertyAccess(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, db, file),
ExprKind::Binary(op) => {
is_expr_tainted(&op.left, ctx, db, file) || is_expr_tainted(&op.right, ctx, db, file)
}
ExprKind::UnaryPrefix(u) => is_expr_tainted(&u.operand, ctx, db, file),
ExprKind::InterpolatedString(parts) | ExprKind::Heredoc { parts, .. } => {
parts.iter().any(|p| match p {
StringPart::Expr(e) => is_expr_tainted(e, ctx, db, file),
StringPart::Literal(_) => false,
})
}
ExprKind::Ternary(t) => match &t.then_expr {
Some(then_e) => {
is_expr_tainted(then_e, ctx, db, file)
|| is_expr_tainted(&t.else_expr, ctx, db, file)
}
None => {
is_expr_tainted(&t.condition, ctx, db, file)
|| is_expr_tainted(&t.else_expr, ctx, db, file)
}
},
ExprKind::NullCoalesce(nc) => {
is_expr_tainted(&nc.left, ctx, db, file) || is_expr_tainted(&nc.right, ctx, db, file)
}
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, db, file),
},
ExprKind::Match(m) => m
.arms
.iter()
.any(|arm| is_expr_tainted(&arm.body, ctx, db, file)),
ExprKind::Array(elements) => elements.iter().any(|el| {
is_expr_tainted(&el.value, ctx, db, file)
|| el
.key
.as_ref()
.is_some_and(|k| is_expr_tainted(k, ctx, db, file))
}),
ExprKind::FunctionCall(fc) => {
if let ExprKind::Identifier(name) = &fc.name.kind {
let resolved = crate::db::resolve_name(db, file, name.as_ref());
let here = crate::db::Fqcn::from_str(db, resolved.as_str());
if crate::db::find_function(db, here).is_some_and(|f| f.is_taint_source) {
return true;
}
}
false
}
ExprKind::StaticPropertyAccess(spa) => {
if let ExprKind::Identifier(id) = &spa.class.kind {
let resolved = crate::db::resolve_name(db, file, id.as_ref());
let fqcn_opt: Option<std::sync::Arc<str>> = match resolved.as_str() {
"self" | "static" => ctx.self_fqcn.clone().or_else(|| ctx.static_fqcn.clone()),
"parent" => ctx.parent_fqcn.clone(),
s => Some(std::sync::Arc::from(s)),
};
if let Some(fqcn) = fqcn_opt {
if let Some(prop_name) = match &spa.member.kind {
ExprKind::Variable(name) | ExprKind::Identifier(name) => {
Some(name.trim_start_matches('$').to_string())
}
_ => None,
} {
return ctx.is_static_prop_tainted(&fqcn, &prop_name);
}
}
}
false
}
ExprKind::MethodCall(mc) | ExprKind::NullsafeMethodCall(mc) => {
if let ExprKind::Identifier(method_name) = &mc.method.kind {
if let Some(recv_ty) =
crate::expr::assignment::resolve_chained_receiver_type(&mc.object, ctx, db)
{
let method_lower = crate::util::php_ident_lowercase(method_name.as_ref());
for atom in &recv_ty.types {
if let mir_types::Atomic::TNamedObject { fqcn, .. } = atom {
let here = crate::db::Fqcn::from_str(db, fqcn.as_ref());
if crate::db::find_method_respecting_precedence(db, here, &method_lower)
.is_some_and(|(_, m)| m.is_taint_source)
{
return true;
}
}
}
}
}
false
}
ExprKind::StaticMethodCall(smc) => {
if let ExprKind::Identifier(id) = &smc.class.kind {
let resolved = crate::db::resolve_name(db, file, id.as_ref());
let fqcn_opt: Option<std::sync::Arc<str>> = match resolved.as_str() {
"self" | "static" => ctx.self_fqcn.clone().or_else(|| ctx.static_fqcn.clone()),
"parent" => ctx.parent_fqcn.clone(),
s => Some(std::sync::Arc::from(s)),
};
if let (Some(fqcn), ExprKind::Identifier(method_name)) =
(fqcn_opt, &smc.method.kind)
{
let method_lower = crate::util::php_ident_lowercase(method_name.as_ref());
let here = crate::db::Fqcn::from_str(db, fqcn.as_ref());
if crate::db::find_method_respecting_precedence(db, here, &method_lower)
.is_some_and(|(_, m)| m.is_taint_source)
{
return true;
}
}
}
false
}
_ => false,
}
}