PHP Tainted Callable Injection

Critical Risk Code Injection
phpcallable-injectiondynamic-executioncode-injectionfunction-call

What it is

The PHP application uses user-controlled input to determine which functions or methods to call dynamically, leading to potential code execution vulnerabilities. This occurs when user input influences call_user_func(), call_user_func_array(), or variable function calls without proper validation.

// Vulnerable: User-controlled callable execution class DataProcessor { public function processData($action, $data) { // Dangerous: User controls which method to call $method = $_GET['action']; // e.g., 'system', 'exec', 'eval' if (method_exists($this, $method)) { // Extremely dangerous: Direct method call return $this->$method($data); } // Also dangerous: User-controlled function call if (function_exists($method)) { return call_user_func($method, $data); } } public function formatData($data) { return json_encode($data); } public function validateData($data) { return filter_var($data, FILTER_SANITIZE_STRING); } } // Usage that allows arbitrary function execution $processor = new DataProcessor(); $userFunction = $_POST['function']; // Could be 'system', 'exec', etc. $userData = $_POST['data']; // Dangerous: User controls the function $result = call_user_func($userFunction, $userData);
// Secure: Allowlist-based callable validation class SecureDataProcessor { private $allowedMethods = [ 'format' => 'formatData', 'validate' => 'validateData', 'clean' => 'cleanData' ]; private $allowedFunctions = [ 'json_encode', 'htmlspecialchars', 'strip_tags' ]; public function processData($action, $data) { // Validate action against allowlist if (!isset($this->allowedMethods[$action])) { throw new InvalidArgumentException('Invalid action specified'); } $methodName = $this->allowedMethods[$action]; // Safe: Only call pre-approved methods if (method_exists($this, $methodName)) { return $this->$methodName($data); } throw new Exception('Method not available'); } public function callSafeFunction($functionName, $data) { // Validate function against allowlist if (!in_array($functionName, $this->allowedFunctions, true)) { throw new InvalidArgumentException('Function not allowed'); } // Additional validation if (!function_exists($functionName)) { throw new Exception('Function does not exist'); } // Safe: Only call pre-approved functions return call_user_func($functionName, $data); } public function formatData($data) { return json_encode($data, JSON_HEX_TAG | JSON_HEX_AMP | JSON_HEX_APOS | JSON_HEX_QUOT); } public function validateData($data) { return filter_var($data, FILTER_SANITIZE_STRING); } public function cleanData($data) { return htmlspecialchars($data, ENT_QUOTES, 'UTF-8'); } } // Alternative: Switch-based approach function processUserAction($action, $data) { switch ($action) { case 'format': return json_encode($data); case 'clean': return htmlspecialchars($data, ENT_QUOTES, 'UTF-8'); case 'validate': return filter_var($data, FILTER_SANITIZE_STRING); default: throw new InvalidArgumentException('Invalid action'); } } // Secure usage try { $processor = new SecureDataProcessor(); $action = filter_input(INPUT_POST, 'action', FILTER_SANITIZE_STRING); $data = filter_input(INPUT_POST, 'data', FILTER_SANITIZE_STRING); if ($action && $data) { $result = $processor->processData($action, $data); } } catch (Exception $e) { error_log('Data processing error: ' . $e->getMessage()); // Handle error appropriately }

💡 Why This Fix Works

See fix suggestions for detailed explanation.

Why it happens

PHP applications accept user-controlled input specifying function names and directly pass them to call_user_func(), call_user_func_array(), or variable function syntax without validation. Code patterns like call_user_func($_GET['function'], $args) or $func = $_POST['callback']; $func($data); treat user input as trusted function identifiers, allowing attackers to specify arbitrary PHP functions including dangerous system functions (system(), exec(), shell_exec(), passthru(), proc_open()), file manipulation functions (unlink(), file_put_contents(), fopen()), or even eval() and assert() for code execution. Variable function syntax where function names come from variables: $functionName = $_REQUEST['action']; $result = $functionName($parameters); is particularly dangerous because it looks innocuous but executes arbitrary code when attackers control $functionName. Array callback syntax: call_user_func([$object, $_GET['method']], $args) allows method invocation on objects where attackers control the method name, potentially calling magic methods (__destruct(), __toString(), __call()) that trigger unintended side effects or security vulnerabilities. The vulnerability extends to indirect user control where database records, configuration files, or API responses containing attacker-influenced data determine callable names. Even seemingly safe wrapper functions that internally use call_user_func() become vulnerable when parameters flow from user input without validation.

Root causes

Using User Input Directly in call_user_func() or Variable Functions

PHP applications accept user-controlled input specifying function names and directly pass them to call_user_func(), call_user_func_array(), or variable function syntax without validation. Code patterns like call_user_func($_GET['function'], $args) or $func = $_POST['callback']; $func($data); treat user input as trusted function identifiers, allowing attackers to specify arbitrary PHP functions including dangerous system functions (system(), exec(), shell_exec(), passthru(), proc_open()), file manipulation functions (unlink(), file_put_contents(), fopen()), or even eval() and assert() for code execution. Variable function syntax where function names come from variables: $functionName = $_REQUEST['action']; $result = $functionName($parameters); is particularly dangerous because it looks innocuous but executes arbitrary code when attackers control $functionName. Array callback syntax: call_user_func([$object, $_GET['method']], $args) allows method invocation on objects where attackers control the method name, potentially calling magic methods (__destruct(), __toString(), __call()) that trigger unintended side effects or security vulnerabilities. The vulnerability extends to indirect user control where database records, configuration files, or API responses containing attacker-influenced data determine callable names. Even seemingly safe wrapper functions that internally use call_user_func() become vulnerable when parameters flow from user input without validation.

Dynamic Method Invocation with Unvalidated Class/Method Names

Object-oriented PHP applications dynamically instantiate classes or invoke methods based on user-supplied class names and method names without proper validation. Patterns like $className = $_GET['class']; $object = new $className(); or $method = $_POST['method']; $object->$method($args); allow attackers to instantiate arbitrary classes present in the application codebase, autoloaded dependencies, or PHP's standard library. Dangerous scenarios include instantiating PDO or other database classes with attacker-controlled connection strings extracting data to attacker-controlled servers, instantiating file handling classes (SplFileObject, DirectoryIterator) to read arbitrary files, or instantiating reflection classes (ReflectionClass, ReflectionFunction) to inspect internal application structure discovering additional attack surfaces. Framework-based applications using controller/action routing where URLs like /index.php?controller=UserController&action=deleteUser map directly to class/method calls: $controller = $_GET['controller']; $action = $_GET['action']; $instance = new $controller(); $instance->$action(); become vulnerable when attackers specify internal controller classes or sensitive methods (deleteAllUsers(), dropDatabase(), executeRawSQL()). Plugin and extension systems that dynamically load and instantiate classes based on plugin names from user configuration or URL parameters create similar risks. The issue compounds with autoloading where PSR-4 autoloaders can load any class from vendor directories, giving attackers access to entire dependency class namespace for exploitation.

Callback Functions Determined by User-Controlled Parameters

Applications implement callback-based architectures where callback function names come from user input, GET/POST parameters, or untrusted data sources. Common patterns include array processing functions with user-specified callbacks: array_filter($data, $_GET['filter_function']), array_map($_POST['transform_function'], $array), or array_reduce($values, $_REQUEST['reducer'], $initial) allowing attackers to specify dangerous functions as callbacks. Event handling systems that register and execute event handlers based on user-provided callback names: $eventManager->on($_GET['event'], $_GET['handler']) become code execution vectors when attackers register system functions as event handlers. Webhook implementations that store callback URLs or function names in databases then execute them: call_user_func($webhook['callback'], $data) are vulnerable when attackers can modify webhook configurations through SQL injection, insecure direct object references, or privileged access abuse. Template engines or view helpers that accept user-defined filter functions: $twig->addFilter($_POST['filter_name'], $_POST['filter_function']) expose code execution when attackers control filter definitions. Data validation frameworks using configurable validation callbacks: $validator->addRule($_GET['rule_name'], $_GET['validation_callback']) allow arbitrary function execution during validation. Serialized data containing callable references that get unserialized and executed: unserialize($userInput) followed by call_user_func($deserialized['callback']) creates exploitation paths when attackers craft malicious serialized payloads.

Missing Validation of Callable Names Before Execution

Development teams implement dynamic callable execution but fail to validate that specified function or method names are safe, either performing no validation or using inadequate blacklist-based approaches. Applications check only for function existence using function_exists($userFunction) before calling call_user_func($userFunction, $args), which confirms the function exists but doesn't verify it's safe to call—system(), exec(), and other dangerous functions exist and pass this check. Method existence checks: if (method_exists($object, $userMethod)) { $object->$userMethod($args); } similarly validate existence without safety, allowing calls to dangerous methods including magic methods, private methods (accessible through variable method syntax in older PHP versions), or destructive operations. Blacklist validation attempts like if (!in_array($function, ['system', 'exec', 'shell_exec'])) { call_user_func($function, $args); } fail because PHP has dozens of dangerous functions (passthru(), proc_open(), popen(), pcntl_exec(), mail() for SMTP injection, unlink(), rmdir(), etc.) making comprehensive blacklists impractical and easily bypassed. Regular expression filters: if (!preg_match('/system|exec|eval/', $function)) { call_user_func($function); } are bypassed through case variations (sYsTem), alternative functions, backtick operators, or shell escaping. The fundamental flaw is attempting to enumerate dangerous functions when PHP provides thousands of built-in functions, any of which could be dangerous in specific contexts—file_get_contents() for SSRF, copy() for arbitrary file operations, header() for response manipulation. Proper security requires allowlist validation where only explicitly permitted functions can execute.

Using User Input to Determine Which Functions to Execute

Application architecture treats function selection as a configuration or routing decision, accepting user input that determines execution flow through function dispatch mechanisms without recognizing the security implications. RESTful APIs implementing action-based routing where HTTP request parameters specify operations: /api/user?action=delete&id=123 maps 'delete' parameter to function calls like call_user_func('delete_user', $id), becoming vulnerable when attackers specify administrative functions (delete_all_users, drop_database, reset_password) accessible through the same dispatch mechanism. Batch processing or workflow engines accepting user-defined processing pipelines: foreach ($steps as $step) { call_user_func($step['function'], $data); } where pipeline definitions come from user uploads, API requests, or database records that attackers can modify. Report generators with user-specified formatter functions: call_user_func($report['formatter'], $data) intended for functions like format_csv(), format_pdf() but exploitable when attackers specify file_put_contents(), mail(), or system functions. Calculator or expression evaluator applications parsing user formulas that include function calls: eval('return ' . $userFormula . ';') or parsing function calls from formulas like SUM(A1:A10) and executing them via call_user_func(), exploitable when attackers inject PHP function names into formulas. Form builders or survey tools where field validation rules include callback function names stored in form definitions: call_user_func($field['validator'], $value), vulnerable through form definition manipulation. The root issue is architectural: treating function name as data rather than code, failing to recognize that function selection is a security-sensitive operation requiring strict control rather than flexible user configuration.

Fixes

1

Use Allowlists for Permitted Functions and Methods

Implement strict allowlist validation ensuring only explicitly approved functions and methods can be called dynamically. Create associative arrays mapping user-friendly action names to approved callables: $allowedFunctions = ['format' => 'json_encode', 'clean' => 'htmlspecialchars', 'validate' => 'filter_var']; $action = $_GET['action']; if (!isset($allowedFunctions[$action])) { throw new InvalidArgumentException('Invalid action'); } $result = call_user_func($allowedFunctions[$action], $data); This approach prevents attackers from specifying function names directly—they can only specify allowlist keys which you control. For method calls, use similar mapping: $allowedMethods = ['save' => 'saveRecord', 'update' => 'updateRecord', 'delete' => 'deleteRecord']; $method = $allowedMethods[$_POST['action']]; $object->$method($params); Document why each function/method is in the allowlist, review allowlists during security audits, and keep allowlists minimal including only functions necessary for legitimate use cases. For class instantiation, maintain allowlists of safe classes: $allowedClasses = ['User' => User::class, 'Product' => Product::class]; $className = $allowedClasses[$_GET['type']]; $instance = new $className(); Implement allowlist validation at the framework level creating base controller classes or middleware that enforce allowlist checks for all dynamic callable execution, making violations detectable through code review. Use type hints and return type declarations to ensure allowlist maintenance: function executeAction(string $action, array $allowedActions): Result { if (!isset($allowedActions[$action])) { throw new SecurityException(); } /* execute */ }. Version control allowlist changes requiring review and approval for any additions, treating allowlist expansion as security-sensitive changes.

2

Validate All Callable Names Against Predefined Safe Lists

For every dynamic callable execution, implement validation comparing the callable against comprehensive predefined safe lists before execution. Define safe callable lists with different categories: $safeFunctions = ['string' => ['strlen', 'substr', 'trim', 'strtolower'], 'array' => ['array_map', 'array_filter', 'array_reduce'], 'json' => ['json_encode', 'json_decode']]; Then validate: $category = $_GET['category']; $function = $_GET['function']; if (!isset($safeFunctions[$category]) || !in_array($function, $safeFunctions[$category], true)) { throw new SecurityException('Function not allowed'); } if (!function_exists($function)) { throw new RuntimeException('Function not available'); } $result = call_user_func($function, $args); Use strict comparison (=== or in_array with true as third parameter) preventing type juggling bypasses. Implement callable validation helper functions centralizing validation logic: function isCallableSafe($callable, array $safeList): bool { if (is_string($callable)) { return in_array($callable, $safeList, true); } if (is_array($callable) && count($callable) === 2) { [$class, $method] = $callable; return isset($safeList[$class]) && in_array($method, $safeList[$class], true); } return false; }. For callbacks stored in databases or configuration files, validate on read/deserialization not just execution preventing invalid callables from persisting. Create unit tests verifying validation rejects dangerous functions: test that isCallableSafe('system', $safeList) returns false, test that attempts to call unlisted functions throw exceptions. Use PHP's ReflectionFunction and ReflectionMethod to inspect callables before execution examining parameters, return types, docblocks for security warnings. Implement runtime monitoring logging all attempted callable executions with function names, parameters, call stack, and validation results for security analysis. Regularly audit safe lists removing unused functions, reviewing added functions for security implications, checking for deprecated or vulnerable functions requiring removal.

3

Implement Proper Input Validation Before Dynamic Function Calls

Apply comprehensive input validation to all data used in callable determination including format validation, type checking, length limits, and character allowlists. Validate function names match expected patterns: if (!preg_match('/^[a-zA-Z_][a-zA-Z0-9_]*$/', $functionName)) { throw new InvalidArgumentException('Invalid function name format'); } ensuring names conform to PHP identifier rules without special characters. Implement length limits preventing excessively long function names: if (strlen($functionName) > 50) { throw new InvalidArgumentException('Function name too long'); } mitigating potential buffer overflow or denial of service scenarios. For action/method names from URLs or forms, validate against enumerated values: $validActions = ['create', 'read', 'update', 'delete']; if (!in_array($_POST['action'], $validActions, true)) { throw new InvalidArgumentException('Invalid action'); }. Apply filter_input() for sanitization: $action = filter_input(INPUT_GET, 'action', FILTER_VALIDATE_REGEXP, ['options' => ['regexp' => '/^[a-z]+$/']]); rejecting any action not matching allowed patterns. Combine multiple validation layers: format validation, allowlist checking, existence verification, and privilege verification before execution. Implement validation at architectural boundaries: API gateway validates callable parameters before forwarding to application servers, controller layer validates before invoking services, service layer validates before executing business logic. Use validation libraries (Symfony Validator, Respect\Validation) providing declarative validation rules: use Respect\Validation\Validator as v; v::alnum()->noWhitespace()->length(3, 30)->assert($functionName);. Log validation failures for security monitoring: error_log('Callable validation failed: ' . $functionName . ' from IP: ' . $_SERVER['REMOTE_ADDR']); detecting attack attempts. Rate limit callable execution from same IP/user preventing brute force attacks attempting to discover valid function names or methods.

4

Use Switch Statements or Arrays Instead of Dynamic Callable Names

Replace dynamic callable execution with explicit control flow structures that enumerate all possible execution paths preventing arbitrary callable invocation. Implement switch statements mapping actions to concrete function calls: switch ($_GET['action']) { case 'format': return formatData($data); case 'validate': return validateData($data); case 'clean': return cleanData($data); default: throw new InvalidArgumentException('Unknown action'); } Every branch explicitly specifies which function executes making code reviewers see all possible function calls. Use associative arrays mapping actions to closures or specific callable arrays: $actionHandlers = ['create' => function($data) { return createRecord($data); }, 'update' => function($data) { return updateRecord($data); }, 'delete' => function($data) { return deleteRecord($data); }]; $action = $_GET['action']; if (!isset($actionHandlers[$action])) { throw new InvalidArgumentException('Invalid action'); } return $actionHandlers[$action]($data); Closures prevent callable name injection as the functions are defined in code not specified by users. For complex routing, implement Command pattern: interface Command { public function execute(array $params): mixed; } class CreateCommand implements Command { public function execute(array $params): mixed { /* implementation */ } } $commands = ['create' => new CreateCommand(), 'update' => new UpdateCommand()]; $command = $_GET['command']; if (!isset($commands[$command])) { throw new InvalidArgumentException(); } return $commands[$command]->execute($params); This makes all executable commands explicit classes requiring code changes to add new commands, preventing runtime callable injection. Use Strategy pattern for algorithm selection: interface DataFormatter { public function format($data): string; } class JsonFormatter implements DataFormatter { public function format($data): string { return json_encode($data); } } $formatters = ['json' => new JsonFormatter(), 'xml' => new XmlFormatter()]; $formatter = $formatters[$_GET['format']] ?? throw new InvalidArgumentException(); return $formatter->format($data); All formatting strategies are concrete classes defined in codebase not user-specified callable names. These patterns make security auditing straightforward: enumerate all switch cases, array entries, or concrete classes to understand complete attack surface.

5

Avoid User-Controlled Input in Function Name Determination

Architect applications to minimize or eliminate scenarios where user input influences which functions execute, treating function selection as internal application logic not user-configurable behavior. Refactor APIs to use resource-based routing instead of action-based routing: change from /api/user?action=delete&id=123 to DELETE /api/user/123 where HTTP method determines operation, eliminating action parameter. URL patterns map to specific controller methods through routing configuration: $router->delete('/user/{id}', [UserController::class, 'delete']); making callable determination framework responsibility not user input. For batch operations or workflows, define workflows in code or restricted admin interfaces not user-editable configurations: class DataPipeline { private $steps = ['validate', 'transform', 'save']; public function execute($data) { foreach ($this->steps as $method) { $data = $this->$method($data); } return $data; } } Workflow steps are hardcoded preventing users from injecting arbitrary functions. For plugin/extension systems, use plugin registration APIs requiring plugins to register specific hooks at defined extension points: $pluginManager->registerHook('before_save', [$plugin, 'beforeSaveCallback']); Plugin callback invocation happens through controlled plugin manager not user-specified callable names. Implement feature flags or configuration controlling which features are enabled but not which functions execute: if ($config['allow_export']) { return $this->exportData($data); } Feature toggles are boolean flags not callable names. For user-configurable behaviors (custom validation rules, data transformations), use domain-specific languages (DSL) or expression languages that parse user-defined rules into safe operations: use Symfony\Component\ExpressionLanguage\ExpressionLanguage; $language = new ExpressionLanguage(); $result = $language->evaluate($userExpression, ['data' => $data]); Expression language provides sandboxed execution with allowlisted functions preventing arbitrary PHP function calls. Document architecture decision records (ADRs) explaining why certain operations cannot be user-configurable due to security constraints, providing alternative extensibility mechanisms that don't require callable injection.

6

Implement Proper Access Control for Callable Operations

Even with allowlist-validated callable execution, implement access control ensuring users have authorization to invoke specific functions or methods based on roles and privileges. Define permission systems mapping callables to required privileges: $callablePermissions = ['deleteUser' => 'admin', 'exportData' => 'analyst', 'updateSettings' => 'manager']; $action = $_GET['action']; if (!isset($allowedActions[$action])) { throw new InvalidArgumentException('Invalid action'); } $requiredPermission = $callablePermissions[$allowedActions[$action]]; if (!$currentUser->hasPermission($requiredPermission)) { throw new UnauthorizedException('Insufficient privileges'); } $result = call_user_func($allowedActions[$action], $params); Separate callable validation (is this a valid function?) from authorization (may this user call it?). Implement role-based access control (RBAC): class SecureExecutor { private $permissions = []; public function addPermission(string $action, string $role): void { $this->permissions[$action] = $role; } public function execute(string $action, User $user, $params) { if (!isset($this->permissions[$action])) { throw new InvalidArgumentException(); } if (!$user->hasRole($this->permissions[$action])) { throw new UnauthorizedException(); } return $this->$action($params); } } For sensitive operations, require multi-factor authentication or additional approval: if ($action === 'deleteAllUsers') { if (!$mfa->verify($_POST['mfa_code'])) { throw new SecurityException('MFA required'); } if (!$approvalSystem->isApproved($action, $user)) { throw new SecurityException('Approval required'); } }. Implement audit logging for all callable executions recording user identity, timestamp, callable name, parameters, result, and authorization decision: $auditLog->record(['user' => $user->id, 'action' => $action, 'callable' => $functionName, 'authorized' => true, 'result' => 'success']); Use attribute-based access control (ABAC) for complex authorization rules considering context, time, location: if ($policy->evaluate($user, $action, $resource, $environment)) { /* execute */ }. Apply principle of least privilege: default deny all callable access, explicitly grant access only to required functions, periodically review permissions removing unused access. Monitor for privilege escalation attempts: alert when users attempt to call admin functions without admin role, detect patterns of unauthorized access attempts from same user or IP, implement temporary account locks after multiple authorization failures.

Detect This Vulnerability in Your Code

Sourcery automatically identifies php tainted callable injection and many other security issues in your codebase.