{"repro_id":"REPRO-2026-00156","version":8,"title":"Yii2: local file inclusion via View::renderPhpFile extract() of caller-controlled params","repro_type":"security","status":"published","severity":"high","description":"`yii\\base\\View::renderPhpFile($_file_, $_params_)` calls\n`extract($_params_, EXTR_OVERWRITE)` **before** the `require $_file_` that\nloads the template. Because `EXTR_OVERWRITE` is allowed to clobber existing\nlocal variables, a `_file_` key in the params array overwrites the local\n`$_file_` immediately before the `require` — redirecting the include to any\npath on disk the PHP process can read.\n\nThis is local file inclusion: an attacker who controls (any subset of) the\nparams array passed to `renderPhpFile` can read arbitrary files, and execute\narbitrary PHP if the included file's contents are PHP.","root_cause":"# Root Cause Analysis: CVE-2026-39850\n\n## Summary\n\nCVE-2026-39850 is a Local File Inclusion (LFI) vulnerability in Yii2's `View::renderPhpFile()` method. The method uses `extract($_params_, EXTR_OVERWRITE)` before `require $_file_`, which allows a malicious `_file_` key in the params array to overwrite the local `$_file_` variable. This redirects the `require` statement to an attacker-controlled file path, enabling arbitrary file inclusion.\n\n## Impact\n\n- **Package**: `yiisoft/yii2`\n- **Affected versions**: `<= 2.0.54`\n- **Fixed version**: `2.0.55`\n- **CWE**: CWE-98 / CWE-94 (LFI / Code Injection)\n- **Risk level**: High (CVSS 3.1 base 7.4)\n- **Consequences**: Attackers who control any part of the params array passed to `renderPhpFile` can read arbitrary files and potentially execute arbitrary PHP code if the included file contains PHP.\n\n## Root Cause\n\nIn `yii\\base\\View::renderPhpFile()`, the vulnerable code sequence was:\n\n```php\nextract($_params_, EXTR_OVERWRITE);\nrequire $_file_;\n```\n\nBecause `EXTR_OVERWRITE` is used, variables in `$_params_` overwrite existing local variables with the same name. If `$_params_` contains a key named `_file_`, the local `$_file_` variable (which holds the intended template path) is overwritten with the attacker-supplied value immediately before the `require` executes.\n\nThe fix (commit `109878b491dbffa541032bc99fb5e26d12cd0375`) isolates the `extract` and `require` inside a closure:\n\n```php\n$_renderer_ = function () {\n    extract(func_get_arg(1), EXTR_OVERWRITE);\n    require func_get_arg(0);\n};\ncall_user_func_array($_renderer_, [$_file_, $_params_]);\n```\n\nBecause `extract` runs inside the closure's local scope, it can only overwrite variables within that scope, not the `$_file_` variable in the parent `renderPhpFile` scope. Thus the intended template path is preserved.\n\nA similar fix was applied to `yii\\web\\ErrorHandler::renderFile()`.\n\n## Reproduction Steps\n\nThe reproduction script is `repro/reproduction_steps.sh`.\n\nWhat the script does:\n1. Clones the `yiisoft/yii2` repository.\n2. Creates a benign template (`safe.php`) and a secret file (`secret.txt`).\n3. Checks out the vulnerable tag `2.0.54`.\n4. Runs a PHP CLI script that instantiates `yii\\base\\View` and calls `renderPhpFile('safe.php', ['_file_' => 'secret.txt'])`.\n5. Captures the output.\n6. Checks out the fixed tag `2.0.55` and repeats the test.\n\nExpected evidence:\n- On **2.0.54** (vulnerable), the output contains `SECRET_DATA_LINE` from the secret file, proving LFI.\n- On **2.0.55** (fixed), the output contains `SAFE OUTPUT` from the intended template, proving the fix works.\n\n## Evidence\n\n### Vulnerable build (2.0.54)\n- **Log**: `logs/vulnerable_2.0.54.log`\n- **Excerpt**:\n  ```\n  === RESULT ===\n  SECRET_DATA_LINE\n  === END ===\n  VULNERABLE: _file_ param was able to override local variable and include secret file\n  ```\n\n### Fixed build (2.0.55)\n- **Log**: `logs/fixed_2.0.55.log`\n- **Excerpt**:\n  ```\n  === RESULT ===\n  SAFE OUTPUT\n  === END ===\n  FIXED: _file_ param did not override local variable, safe.php was rendered\n  ```\n\n### Environment\n- PHP 8.4.19 (cli)\n- Composer 2.8.12\n- Git 2.x\n- Yii2 cloned from https://github.com/yiisoft/yii2\n\n## Recommendations / Next Steps\n\n- **Immediate**: Upgrade `yiisoft/yii2` to `>= 2.0.55`.\n- **Defense in depth**: Sanitize or validate user-controlled data before passing it as view params. Avoid passing raw user input directly to `renderPhpFile` or `renderFile`.\n- **Testing**: Add regression tests that pass `_file_` and other internal variable names in view params to ensure future changes do not reintroduce scope leakage.\n\n## Additional Notes\n\n- **Idempotency**: The reproduction script was run twice consecutively and produced identical results both times.\n- **Edge cases**: The same vulnerability pattern existed in `yii\\web\\ErrorHandler::renderFile()`, which was fixed in the same commit. The reproduction focuses on `View::renderPhpFile()` as the primary attack surface.\n- **No web server required**: The issue is reproducible entirely via PHP CLI, making automated testing straightforward.\n","ghsa_id":"GHSA-5vpg-rj7q-qpw2","cve_id":"CVE-2026-39850","cwe_id":"CWE-98 / CWE-94 (LFI / Code Injection)","source_url":"https://github.com/yiisoft/yii2","package":{"name":"yiisoft/yii2","ecosystem":"composer","affected_versions":"<= 2.0.54","fixed_version":"2.0.55"},"reproduced_at":"2026-05-23T06:29:49.478590+00:00","duration_secs":911.16392660141,"tool_calls":120,"turns":98,"handoffs":3,"total_cost_usd":0.6194072199999998,"agent_costs":{"repro":0.13518389,"support":0.03924804,"vuln_variant":0.44497529},"cost_breakdown":{"repro":{"accounts/fireworks/models/kimi-k2p6":0.13518389},"support":{"accounts/fireworks/models/kimi-k2p6":0.03924804},"vuln_variant":{"accounts/fireworks/models/kimi-k2p6":0.44497529}},"quality":{"idempotent_verified":false,"community_verifications":0},"published_at":"2026-05-23T06:29:51.317713+00:00","retracted":false,"artifacts":[{"path":"repro/rca_report.md","filename":"rca_report.md","size":4080,"category":"analysis"},{"path":"repro/reproduction_steps.sh","filename":"reproduction_steps.sh","size":2356,"category":"reproduction_script"},{"path":"vuln_variant/rca_report.md","filename":"rca_report.md","size":5728,"category":"analysis"},{"path":"vuln_variant/reproduction_steps.sh","filename":"reproduction_steps.sh","size":5874,"category":"reproduction_script"},{"path":"bundle/context.json","filename":"context.json","size":2871,"category":"other"},{"path":"bundle/metadata.json","filename":"metadata.json","size":645,"category":"other"},{"path":"bundle/ticket.md","filename":"ticket.md","size":3203,"category":"ticket"},{"path":"repro/validation_verdict.json","filename":"validation_verdict.json","size":155,"category":"other"},{"path":"vuln_variant/root_cause_equivalence.json","filename":"root_cause_equivalence.json","size":776,"category":"other"},{"path":"vuln_variant/patch_analysis.md","filename":"patch_analysis.md","size":3574,"category":"documentation"},{"path":"vuln_variant/variant_manifest.json","filename":"variant_manifest.json","size":2440,"category":"other"},{"path":"vuln_variant/validation_verdict.json","filename":"validation_verdict.json","size":1855,"category":"other"},{"path":"vuln_variant/source_identity.json","filename":"source_identity.json","size":580,"category":"other"},{"path":"logs/composer_2.0.55.log","filename":"composer_2.0.55.log","size":75,"category":"log"},{"path":"logs/vulnerable_2.0.54.log","filename":"vulnerable_2.0.54.log","size":129,"category":"log"},{"path":"logs/fixed_2.0.55.log","filename":"fixed_2.0.55.log","size":114,"category":"log"},{"path":"logs/vuln_variant/variant2_vulnerable.log","filename":"variant2_vulnerable.log","size":127,"category":"log"},{"path":"logs/vuln_variant/variant1_vulnerable.log","filename":"variant1_vulnerable.log","size":116,"category":"log"},{"path":"logs/vuln_variant/variant2_fixed.log","filename":"variant2_fixed.log","size":115,"category":"log"},{"path":"logs/vuln_variant/variant_verdict.json","filename":"variant_verdict.json","size":311,"category":"other"},{"path":"logs/vuln_variant/variant3_vulnerable.log","filename":"variant3_vulnerable.log","size":123,"category":"log"},{"path":"logs/vuln_variant/variant1_fixed.log","filename":"variant1_fixed.log","size":104,"category":"log"},{"path":"logs/vuln_variant/variant3_fixed.log","filename":"variant3_fixed.log","size":107,"category":"log"},{"path":"logs/vuln_variant/fixed_version.txt","filename":"fixed_version.txt","size":207,"category":"other"},{"path":"logs/validation_verdict.json","filename":"validation_verdict.json","size":155,"category":"other"},{"path":"logs/composer_2.0.54.log","filename":"composer_2.0.54.log","size":75,"category":"log"}]}