Variable Variables ($$var) Risks
debt(d5/e5/b4/t7)
Closest to 'specialist tool catches' (d5), since detection_hints lists phpstan and semgrep with a clear regex pattern \$\$[a-zA-Z_] that flags usage, but it's not a default linter rule and won't always trace indirect taint.
Closest to 'touches multiple files / significant refactor' (e5), since quick_fix recommends replacing variable variables with explicit associative arrays — a refactor across each usage site, not a single-line swap, especially when loops and templates are involved.
Closest to 'localised tax' (b3) leaning toward persistent (b4), because while $$var usage is typically scattered in specific components (templates, dynamic dispatch), it creates an ongoing review/audit burden wherever it appears across web and cli contexts.
Closest to 'serious trap' (t7), because the misconception explicitly states developers think it's only risky with direct $_GET use, but any indirect user-influenced path is exploitable — contradicting the intuition that sanitization elsewhere protects you.
TL;DR
Explanation
$$var is PHP's variable variable syntax: $varName = 'username'; $$varName = 'admin' creates $username = 'admin'. This is almost always a design smell and a security risk when $varName comes from user input. Attackers can read or overwrite any variable in scope. Extract() internally uses the same mechanism. Variable variables are occasionally legitimate (template engines, serialisation) but require a strict whitelist. PHPStan flags them as suspicious. In PHP 7, $$var['key'] was disambiguated: ${$var['key']} vs ${$var}['key'].
Common Misconception
Why It Matters
Common Mistakes
- Using $$key from a loop over user-supplied array keys.
- Not whitelisting allowed variable names before using $$var.
- Using variable variables in template code that processes user content.
Code Examples
// User sends: field=password&value=attackerPassword
$field = $_GET['field'];
$$field = $_GET['value']; // Sets $password = 'attackerPassword'
// Whitelist approach:
$allowed = ['title', 'body', 'author'];
$field = $_GET['field'] ?? '';
if (in_array($field, $allowed, true)) {
$$field = sanitize($_GET['value'] ?? '');
}
// Better: explicit mapping
$data = [
'title' => sanitize($_GET['title'] ?? ''),
'body' => sanitize($_GET['body'] ?? ''),
];