Dangerous Dynamic Variables
PHP lets a variable’s name be the value of another variable. $$name
references the variable whose name is the current value of $name. With
attacker-controlled $name, you read or overwrite any local in scope.
This bug class lives alongside extract()
— same blast radius, different sink.
Why
$name = $_GET['k'];
$$name = $_GET['v'];
// ?k=isAdmin&v=1 → $isAdmin = 1Adjacent footguns:
parse_str($input)(1-arg form) — writes to local scope. Removed in PHP 8.0 but legacy 7.x code still has it.mb_parse_str($input)— same.import_request_variables()— removed in 5.4; still seen.- Fake
register_globalsshims — early-2000s code emulating the long-removed feature byforeach ($_REQUEST as $k => $v) $$k = $v;. Rare but lethal.
Search patterns
# $$var — actual variable variables
rg -n '\$\$\w+' --type=php
rg -n '\$\{\$' --type=php # ${$var} form
# parse_str / mb_parse_str — 1-arg (dangerous) form
rg -n '(parse_str|mb_parse_str)\(\s*\$[^,)]+\s*\)' --type=php
# Legacy shims
rg -n 'register_globals|import_request_variables' --type=php
rg -n 'foreach\s*\(\s*\$_(GET|POST|REQUEST)' --type=php # candidate fake-globalsTest inputs
Identical to extract — the
attack is whatever variable name the surrounding scope uses:
- Auth flags:
?isAdmin=1,?is_admin=1,?logged_in=1 - Identity:
?user_id=<other>,?account_id=… - Roles:
?role=admin,?permission=write - Config:
?db_host=attacker.com,?template=../../etc/passwd - Any local literal the file’s scope uses elsewhere
When you have source access, grep the same file for $variable names
already in use and target those.
Audit focus
For each variable-variable / scope-polluting call:
- Source of the name — attacker-controlled?
- Scope contents — what auth / config / control-flow variables exist in the same scope?
- Use downstream — is the result of an overwritten variable used in a security decision?
- Legacy patterns — fake
register_globalsshims at the top of bootstrap files are sometimes left behind for “compatibility.”
Frameworks rarely use these patterns, but plugins / modules / themes for WordPress, Joomla, Drupal, and old PHP-CMS systems do — especially older ones. Audit any extension code you import.
Fix
Refactor to an explicit array. $data['isAdmin'], never $$key.
For parse_str, always use the 2-arg form:
parse_str($_SERVER['QUERY_STRING'], $params);
$role = $params['role'] ?? null;For dynamic dispatch on a name, use an allowlist:
$handlers = [
'list' => fn() => list_items(),
'show' => fn() => show_item(),
'create' => fn() => create_item(),
];
$action = $_GET['action'] ?? 'list';
($handlers[$action] ?? $handlers['list'])();