Skip to content

Commit e8bb0a8

Browse files
committed
Merge branch 'PHP-8.3' into PHP-8.4
* PHP-8.3: Harden proc_open() against cmd.exe hijacking
2 parents 0f5cc82 + 5cbdd5f commit e8bb0a8

File tree

6 files changed

+101
-3
lines changed

6 files changed

+101
-3
lines changed

NEWS

+3
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,9 @@ PHP NEWS
2020
. Fixed bug GH-17037 (UAF in user filter when adding existing filter name due
2121
to incorrect error handling). (nielsdos)
2222

23+
- Windows:
24+
. Hardened proc_open() against cmd.exe hijacking. (cmb)
25+
2326
05 Dec 2024, PHP 8.4.2
2427

2528
- BcMath:

ext/standard/Makefile.frag.w32

+4
Original file line numberDiff line numberDiff line change
@@ -7,3 +7,7 @@ ext\standard\url_scanner_ex.c: ext\standard\url_scanner_ex.re
77
$(RE2C) $(RE2C_FLAGS) -b -o ext/standard/url_scanner_ex.c ext/standard/url_scanner_ex.re
88

99
$(BUILD_DIR)\ext\standard\basic_functions.obj: $(PHP_SRC_DIR)\Zend\zend_language_parser.h
10+
11+
$(PHP_SRC_DIR)\ext\standard\tests\helpers\bad_cmd.exe: $(PHP_SRC_DIR)\ext\standard\tests\helpers\bad_cmd.c
12+
cd $(PHP_SRC_DIR)\ext\standard\tests\helpers
13+
$(PHP_CL) /nologo bad_cmd.c

ext/standard/proc_open.c

+57-2
Original file line numberDiff line numberDiff line change
@@ -698,22 +698,77 @@ static void init_process_info(PROCESS_INFORMATION *pi)
698698
memset(&pi, 0, sizeof(pi));
699699
}
700700

701+
/* on success, returns length of *comspec, which then needs to be efree'd by caller */
702+
static size_t find_comspec_nt(wchar_t **comspec)
703+
{
704+
zend_string *path = NULL;
705+
wchar_t *pathw = NULL;
706+
wchar_t *bufp = NULL;
707+
DWORD buflen = MAX_PATH, len = 0;
708+
709+
path = php_getenv("PATH", 4);
710+
if (path == NULL) {
711+
goto out;
712+
}
713+
pathw = php_win32_cp_any_to_w(ZSTR_VAL(path));
714+
if (pathw == NULL) {
715+
goto out;
716+
}
717+
bufp = emalloc(buflen * sizeof(wchar_t));
718+
do {
719+
/* the first call to SearchPathW() fails if the buffer is too small,
720+
* what is unlikely but possible; to avoid an explicit second call to
721+
* SeachPathW() and the error handling, we're looping */
722+
len = SearchPathW(pathw, L"cmd.exe", NULL, buflen, bufp, NULL);
723+
if (len == 0) {
724+
goto out;
725+
}
726+
if (len < buflen) {
727+
break;
728+
}
729+
buflen = len;
730+
bufp = erealloc(bufp, buflen * sizeof(wchar_t));
731+
} while (1);
732+
*comspec = bufp;
733+
734+
out:
735+
if (path != NULL) {
736+
zend_string_release(path);
737+
}
738+
if (pathw != NULL) {
739+
free(pathw);
740+
}
741+
if (bufp != NULL && bufp != *comspec) {
742+
efree(bufp);
743+
}
744+
return len;
745+
}
746+
701747
static zend_result convert_command_to_use_shell(wchar_t **cmdw, size_t cmdw_len)
702748
{
703-
size_t len = sizeof(COMSPEC_NT) + sizeof(" /s /c ") + cmdw_len + 3;
749+
wchar_t *comspec;
750+
size_t len = find_comspec_nt(&comspec);
751+
if (len == 0) {
752+
php_error_docref(NULL, E_WARNING, "Command conversion failed");
753+
return FAILURE;
754+
}
755+
len += sizeof(" /s /c ") + cmdw_len + 3;
704756
wchar_t *cmdw_shell = (wchar_t *)malloc(len * sizeof(wchar_t));
705757

706758
if (cmdw_shell == NULL) {
759+
efree(comspec);
707760
php_error_docref(NULL, E_WARNING, "Command conversion failed");
708761
return FAILURE;
709762
}
710763

711-
if (_snwprintf(cmdw_shell, len, L"%hs /s /c \"%s\"", COMSPEC_NT, *cmdw) == -1) {
764+
if (_snwprintf(cmdw_shell, len, L"%s /s /c \"%s\"", comspec, *cmdw) == -1) {
765+
efree(comspec);
712766
free(cmdw_shell);
713767
php_error_docref(NULL, E_WARNING, "Command conversion failed");
714768
return FAILURE;
715769
}
716770

771+
efree(comspec);
717772
free(*cmdw);
718773
*cmdw = cmdw_shell;
719774

Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
--TEST--
2+
Harden against cmd.exe hijacking
3+
--SKIPIF--
4+
<?php
5+
if (PHP_OS_FAMILY !== "Windows") die("skip only for Windows");
6+
?>
7+
--FILE--
8+
<?php
9+
copy(__DIR__ . "/../helpers/bad_cmd.exe", "cmd.exe");
10+
$spec = [["pipe", "r"], ["pipe", "w"], ["pipe", "w"]];
11+
var_dump($proc = proc_open("@echo hello", $spec, $pipes, null));
12+
$read = [$pipes[1], $pipes[2]];
13+
$write = $except = null;
14+
if (($num = stream_select($read, $write, $except, 1000)) === false) {
15+
echo "stream_select() failed\n";
16+
} elseif ($num > 0) {
17+
foreach ($read as $stream) {
18+
fpassthru($stream);
19+
}
20+
}
21+
?>
22+
--EXPECTF--
23+
resource(%d) of type (process)
24+
hello
25+
--CLEAN--
26+
<?php
27+
@unlink("cmd.exe");
28+
?>

ext/standard/tests/helpers/bad_cmd.c

+7
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
#include <stdio.h>
2+
3+
int main()
4+
{
5+
printf("pwnd!\n");
6+
return 0;
7+
}

win32/build/Makefile

+2-1
Original file line numberDiff line numberDiff line change
@@ -54,7 +54,7 @@ DEBUGGER_CMD=
5454
DEBUGGER_ARGS=
5555
!endif
5656

57-
all: generated_files $(EXT_TARGETS) $(PECL_TARGETS) $(SAPI_TARGETS)
57+
all: generated_files $(EXT_TARGETS) $(PECL_TARGETS) $(SAPI_TARGETS) test_helpers
5858

5959
build_dirs: $(BUILD_DIR) $(BUILD_DIRS_SUB) $(BUILD_DIR_DEV)
6060

@@ -184,6 +184,7 @@ clean-pgo: clean-all
184184
-del /f /q $(BUILD_DIR)\$(DIST_ZIP_PECL)
185185
-del /f /q $(BUILD_DIR)\$(DIST_ZIP_TEST_PACK)
186186

187+
test_helpers: $(PHP_SRC_DIR)\ext\standard\tests\helpers\bad_cmd.exe
187188

188189
!if $(PHP_TEST_INI_PATH) == ""
189190
test: set-tmp-env

0 commit comments

Comments
 (0)