What's the vulnerability?

yii\base\View::renderPhpFile($_file_, $_params_) calls extract($_params_, EXTR_OVERWRITE) before the require $_file_ that loads the template. Because EXTR_OVERWRITE is allowed to clobber existing local variables, a _file_ key in the params array overwrites the local $_file_ immediately before the require — redirecting the include to any path on disk the PHP process can read.

This is local file inclusion: an attacker who controls (any subset of) the params array passed to renderPhpFile can read arbitrary files, and execute arbitrary PHP if the included file's contents are PHP.

Root Cause Analysis

# Root Cause Analysis: CVE-2026-39850

## Summary

CVE-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.

## Impact

- **Package**: `yiisoft/yii2`
- **Affected versions**: `<= 2.0.54`
- **Fixed version**: `2.0.55`
- **CWE**: CWE-98 / CWE-94 (LFI / Code Injection)
- **Risk level**: High (CVSS 3.1 base 7.4)
- **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.

## Root Cause

In `yii\base\View::renderPhpFile()`, the vulnerable code sequence was:

```php
extract($_params_, EXTR_OVERWRITE);
require $_file_;
```

Because `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.

The fix (commit `109878b491dbffa541032bc99fb5e26d12cd0375`) isolates the `extract` and `require` inside a closure:

```php
$_renderer_ = function () {
    extract(func_get_arg(1), EXTR_OVERWRITE);
    require func_get_arg(0);
};
call_user_func_array($_renderer_, [$_file_, $_params_]);
```

Because `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.

A similar fix was applied to `yii\web\ErrorHandler::renderFile()`.

## Reproduction Steps

The reproduction script is `repro/reproduction_steps.sh`.

What the script does:
1. Clones the `yiisoft/yii2` repository.
2. Creates a benign template (`safe.php`) and a secret file (`secret.txt`).
3. Checks out the vulnerable tag `2.0.54`.
4. Runs a PHP CLI script that instantiates `yii\base\View` and calls `renderPhpFile('safe.php', ['_file_' => 'secret.txt'])`.
5. Captures the output.
6. Checks out the fixed tag `2.0.55` and repeats the test.

Expected evidence:
- On **2.0.54** (vulnerable), the output contains `SECRET_DATA_LINE` from the secret file, proving LFI.
- On **2.0.55** (fixed), the output contains `SAFE OUTPUT` from the intended template, proving the fix works.

## Evidence

### Vulnerable build (2.0.54)
- **Log**: `logs/vulnerable_2.0.54.log`
- **Excerpt**:
  ```
  === RESULT ===
  SECRET_DATA_LINE
  === END ===
  VULNERABLE: _file_ param was able to override local variable and include secret file
  ```

### Fixed build (2.0.55)
- **Log**: `logs/fixed_2.0.55.log`
- **Excerpt**:
  ```
  === RESULT ===
  SAFE OUTPUT
  === END ===
  FIXED: _file_ param did not override local variable, safe.php was rendered
  ```

### Environment
- PHP 8.4.19 (cli)
- Composer 2.8.12
- Git 2.x
- Yii2 cloned from https://github.com/yiisoft/yii2

## Recommendations / Next Steps

- **Immediate**: Upgrade `yiisoft/yii2` to `>= 2.0.55`.
- **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`.
- **Testing**: Add regression tests that pass `_file_` and other internal variable names in view params to ensure future changes do not reintroduce scope leakage.

## Additional Notes

- **Idempotency**: The reproduction script was run twice consecutively and produced identical results both times.
- **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.
- **No web server required**: The issue is reproducible entirely via PHP CLI, making automated testing straightforward.
One Command

Verify with pruva-verify

Run the Pruva CLI to automatically fetch and execute the reproduction script.

pruva-verify REPRO-2026-00156
or pruva-verify GHSA-5vpg-rj7q-qpw2
or pruva-verify CVE-2026-39850
Install: curl -fsSL https://pruva.dev/install.sh | sh

Or Run Manually

1

Download the script

curl -O https://pruva.dev/api/v1/reproductions/REPRO-2026-00156/artifacts/reproduction_steps.sh
2

Make executable

chmod +x reproduction_steps.sh
3

Run the script

./reproduction_steps.sh
Run in a VM, container, or disposable environment. This exploits a real vulnerability.

How Pruva Reproduced This

Watch the AI agent's step-by-step process.

Loading session...

Artifacts

No artifacts available