These notes are public, opinionated, and evolving — read abdelkader.ma for the long-form posts.
PHP SecurityDynamic Variables ($$)

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 = 1

Adjacent 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_globals shims — early-2000s code emulating the long-removed feature by foreach ($_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-globals

Test 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:

  1. Source of the name — attacker-controlled?
  2. Scope contents — what auth / config / control-flow variables exist in the same scope?
  3. Use downstream — is the result of an overwritten variable used in a security decision?
  4. Legacy patterns — fake register_globals shims 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'])();