Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions NEWS
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,10 @@ PHP NEWS
. Fixed password_get_info() and password_needs_rehash() accepting malformed
bcrypt costs. (Ilia Alshanetsky)

- Windows:
. Fixed DOS device prefixed paths not being canonicalized, so junctions were
not resolved and open_basedir saw a different path. (Jakub Zelenka)

- Zip:
. Fixed use-after-free when re-entering ZipArchive during destruction or
a close warning, and rejected opening streams while closing. (jvoisin)
Expand Down
18 changes: 17 additions & 1 deletion Zend/zend_virtual_cwd.c
Original file line number Diff line number Diff line change
Expand Up @@ -1029,6 +1029,22 @@ CWD_API int virtual_file_ex(cwd_state *state, const char *path, verify_path_func
#endif

#ifdef ZEND_WIN32
/* Strip the DOS device prefix from \\.\C:\ and \\?\C:\ paths and rewrite
* \\?\UNC\ paths to \\server\share\, so they resolve like plain paths. */
if (path_length > 4 && IS_SLASH(path[0]) && IS_SLASH(path[1])
&& (path[2] == '.' || path[2] == '?') && IS_SLASH(path[3])) {
if (path_length > 6 && isalpha((unsigned char)path[4]) && path[5] == ':' && IS_SLASH(path[6])) {
path += 4;
path_length -= 4;
} else if (path_length > 8 && strncasecmp(path + 4, "UNC", 3) == 0 && IS_SLASH(path[7])) {
resolved_path[0] = DEFAULT_SLASH;
resolved_path[1] = DEFAULT_SLASH;
memcpy(resolved_path + 2, path + 8, path_length - 8 + 1);
path = resolved_path;
path_length -= 6;
}
}

switch (php_win32_ioutil_path_kind_a(path, path_length)) {
case PHP_WIN32_IOUTIL_PATH_RESERVED:
SET_ERRNO_FROM_WIN32_CODE(ERROR_INVALID_NAME);
Expand Down Expand Up @@ -1103,7 +1119,7 @@ CWD_API int virtual_file_ex(cwd_state *state, const char *path, verify_path_func
resolved_path[2] = DEFAULT_SLASH;
memcpy(resolved_path + 3, path + 2, path_length - 1);
path_length++;
} else
} else if (path != resolved_path) /* already rewritten from \\?\UNC\ */
#endif
memcpy(resolved_path, path, path_length + 1);
}
Expand Down
87 changes: 87 additions & 0 deletions ext/standard/tests/file/dos_device_prefix-win32.phpt
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
--TEST--
DOS device prefixed paths resolve junctions and respect open_basedir
--SKIPIF--
<?php
if (substr(PHP_OS, 0, 3) !== "WIN") {
die("skip Windows only");
}
?>
--FILE--
<?php

$sep = DIRECTORY_SEPARATOR;
$dir = __DIR__ . $sep . 'dos_device_prefix';
$allowed = $dir . $sep . 'allowed';
$secret = $dir . $sep . 'secret';
$junction = $allowed . $sep . 'junction';

mkdir($allowed, 0777, true);
mkdir($secret);
file_put_contents($allowed . $sep . 'ok.txt', 'ok');
file_put_contents($secret . $sep . 'file.txt', 'secret');
exec(sprintf('mklink /J "%s" "%s"', $junction, $secret), $output, $status);
if ($status !== 0) {
die("mklink failed: " . implode("\n", $output));
}

$plain = $junction . $sep . 'file.txt';
$spellings = [
'plain' => $plain,
'dot' => '\\\\.\\' . $plain,
'question' => '\\\\?\\' . $plain,
];
$expected = $secret . $sep . 'file.txt';

echo "Junction resolves through all spellings:\n";
foreach ($spellings as $name => $path) {
echo "$name: ";
var_dump(realpath($path) === $expected, file_get_contents($path));
}

echo "Prefixed paths without a drive letter are left alone:\n";
$h = fopen('\\\\.\\NUL', 'wb');
var_dump(is_resource($h));
fclose($h);

echo "Reserved names are still rejected:\n";
var_dump(@fopen('\\\\.\\' . $allowed . $sep . 'NUL', 'wb') === false);
var_dump(file_exists('\\\\?\\' . $allowed . $sep . 'NUL'));

echo "open_basedir sees the same path for all spellings:\n";
ini_set('open_basedir', $allowed);
foreach ($spellings as $name => $path) {
echo "$name: ";
var_dump(@file_get_contents('\\\\.\\' . $allowed . $sep . 'ok.txt') === 'ok');
var_dump(@file_get_contents($path) === false);
var_dump(@file_get_contents(str_replace($junction, $secret, $path)) === false);
}

?>
--CLEAN--
<?php
$dir = __DIR__ . DIRECTORY_SEPARATOR . 'dos_device_prefix';
exec(sprintf('rmdir /S /Q "%s"', $dir));
?>
--EXPECT--
Junction resolves through all spellings:
plain: bool(true)
string(6) "secret"
dot: bool(true)
string(6) "secret"
question: bool(true)
string(6) "secret"
Prefixed paths without a drive letter are left alone:
bool(true)
Reserved names are still rejected:
bool(true)
bool(false)
open_basedir sees the same path for all spellings:
plain: bool(true)
bool(true)
bool(true)
dot: bool(true)
bool(true)
bool(true)
question: bool(true)
bool(true)
bool(true)
Loading