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(),
},
}
}
fn unwrap_parens(expr: &Expr) -> &Expr {
let mut e = expr;
while let ExprKind::Parenthesized(inner) = &e.kind {
e = inner;
}
e
}
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)
|| (ctx.has_dynamic_tainted_var_def
&& !ctx.var_is_defined_sym(mir_types::Name::from(n)))
}
ExprKind::VariableVariable(inner) => {
if let Some(var_name) = crate::expr::helpers::extract_simple_var(inner) {
let inner_ty = ctx.get_var(&var_name);
inner_ty.types.iter().any(|atomic| {
matches!(atomic, mir_types::Atomic::TLiteralString(accessed)
if ctx.is_tainted(accessed.as_ref()))
})
} else {
false
}
}
ExprKind::ArrayAccess(aa) => {
is_expr_tainted(&aa.array, ctx, db, file)
}
ExprKind::Parenthesized(inner) => is_expr_tainted(inner, ctx, db, file),
ExprKind::ErrorSuppress(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, .. }
| ExprKind::ShellExec(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 {
if matches!(
crate::util::php_ident_lowercase(name.as_ref()).as_str(),
"getallheaders" | "apache_request_headers"
) {
return true;
}
if matches!(
crate::util::php_ident_lowercase(name.as_ref()).as_str(),
"sprintf" | "vsprintf"
) && fc
.args
.iter()
.any(|a| is_expr_tainted(&a.value, ctx, db, file))
{
return true;
}
if crate::util::php_ident_lowercase(name.as_ref()) == "compact"
&& fc.args.iter().any(|a| {
matches!(&a.value.kind, ExprKind::String(var_name) if ctx.is_tainted(var_name.as_ref()))
})
{
return true;
}
if matches!(
crate::util::php_ident_lowercase(name.as_ref()).as_str(),
"str_replace"
| "str_ireplace"
| "preg_replace"
| "preg_replace_callback"
| "substr_replace"
| "trim"
| "ltrim"
| "rtrim"
| "explode"
| "implode"
| "join"
| "strtolower"
| "strtoupper"
| "mb_strtolower"
| "mb_strtoupper"
| "ucfirst"
| "ucwords"
| "wordwrap"
| "chunk_split"
| "str_pad"
| "str_rot13"
| "nl2br"
) && fc
.args
.iter()
.any(|a| is_expr_tainted(&a.value, ctx, db, file))
{
return true;
}
let lower_name = crate::util::php_ident_lowercase(name.as_ref());
if lower_name == "array_map"
&& fc
.args
.iter()
.skip(1)
.any(|a| is_expr_tainted(&a.value, ctx, db, file))
{
return true;
}
if matches!(lower_name.as_str(), "array_filter" | "array_reduce")
&& fc
.args
.first()
.is_some_and(|a| is_expr_tainted(&a.value, ctx, db, file))
{
return true;
}
if lower_name == "json_decode"
&& fc
.args
.first()
.is_some_and(|a| is_expr_tainted(&a.value, ctx, db, file))
{
return true;
}
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;
}
} else if let ExprKind::ArrowFunction(af) = &unwrap_parens(&fc.name).kind {
if af.params.is_empty() && fc.args.is_empty() {
return is_expr_tainted(&af.body, ctx, db, file);
}
}
false
}
ExprKind::StaticPropertyAccess(spa) => {
crate::expr::assignment::resolve_static_prop_target(spa, ctx, db, file)
.is_some_and(|(fqcn, prop_name)| ctx.is_static_prop_tainted(&fqcn, &prop_name))
}
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, file,
) {
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) => {
let fqcn_opt: Option<std::sync::Arc<str>> = if let ExprKind::Identifier(id) =
&smc.class.kind
{
let resolved = crate::db::resolve_name(db, file, id.as_ref());
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)),
}
} else {
crate::expr::assignment::resolve_chained_receiver_type(&smc.class, ctx, db, file)
.and_then(|ty| {
ty.types.iter().find_map(|atom| match atom {
mir_types::Atomic::TClassString(Some(fqcn))
| mir_types::Atomic::TNamedObject { fqcn, .. }
| mir_types::Atomic::TSelf { fqcn }
| mir_types::Atomic::TStaticObject { fqcn }
| mir_types::Atomic::TParent { fqcn } => {
Some(std::sync::Arc::from(fqcn.as_ref()))
}
_ => None,
})
})
};
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,
}
}