diff --git a/NEWS b/NEWS index 942a8e9b0651..1e1c1c1bf2f0 100644 --- a/NEWS +++ b/NEWS @@ -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) diff --git a/Zend/zend_virtual_cwd.c b/Zend/zend_virtual_cwd.c index 48f817310d2a..0a5d41f3dd36 100644 --- a/Zend/zend_virtual_cwd.c +++ b/Zend/zend_virtual_cwd.c @@ -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); @@ -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); } diff --git a/ext/standard/tests/file/dos_device_prefix-win32.phpt b/ext/standard/tests/file/dos_device_prefix-win32.phpt new file mode 100644 index 000000000000..74e87f0dbc76 --- /dev/null +++ b/ext/standard/tests/file/dos_device_prefix-win32.phpt @@ -0,0 +1,87 @@ +--TEST-- +DOS device prefixed paths resolve junctions and respect open_basedir +--SKIPIF-- + +--FILE-- + $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-- + +--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)