From 1c4bf04783b4915a908a1f7f09a4f4fe2f115ed6 Mon Sep 17 00:00:00 2001 From: Ilia Alshanetsky Date: Thu, 16 Jul 2026 08:30:45 -0400 Subject: [PATCH 1/4] ext/soap: fix NULL deref on malformed HTTP status line make_http_soap_request() looked for the reason phrase with strstr(tmp, " ") even when the first strstr() found no space, which happens for a status line carrying a version but no status code, such as "HTTP/1.1". tmp is NULL there, so strstr() dereferenced it and the client crashed. Only parse the reason phrase when a status code field was present. Closes GH-22761 --- ext/soap/php_http.c | 14 ++++----- ext/soap/tests/http_status_line_no_code.phpt | 33 ++++++++++++++++++++ 2 files changed, 40 insertions(+), 7 deletions(-) create mode 100644 ext/soap/tests/http_status_line_no_code.phpt diff --git a/ext/soap/php_http.c b/ext/soap/php_http.c index e9fd68007d26..125c92582008 100644 --- a/ext/soap/php_http.c +++ b/ext/soap/php_http.c @@ -956,14 +956,14 @@ int make_http_soap_request(zval *this_ptr, if (tmp != NULL) { tmp++; http_status = atoi(tmp); - } - tmp = strstr(tmp," "); - if (tmp != NULL) { - tmp++; - if (http_msg) { - efree(http_msg); + tmp = strstr(tmp," "); + if (tmp != NULL) { + tmp++; + if (http_msg) { + efree(http_msg); + } + http_msg = estrdup(tmp); } - http_msg = estrdup(tmp); } efree(http_version); diff --git a/ext/soap/tests/http_status_line_no_code.phpt b/ext/soap/tests/http_status_line_no_code.phpt new file mode 100644 index 000000000000..de943571a492 --- /dev/null +++ b/ext/soap/tests/http_status_line_no_code.phpt @@ -0,0 +1,33 @@ +--TEST-- +SoapClient: malformed HTTP status line without status code must not crash +--EXTENSIONS-- +soap +--FILE-- + 'http://{{ ADDR }}', + 'uri' => 'http://testuri.org', + 'connection_timeout' => 3, +]); +var_dump($client->__doRequest('', 'http://{{ ADDR }}', 'T', 1)); +CODE; + +include sprintf('%s/../../openssl/tests/ServerClientTestCase.inc', __DIR__); +ServerClientTestCase::getInstance()->run($clientCode, $serverCode); +?> +--EXPECT-- +string(0) "" From b750803b766f1e2d8e5a480b2111e5500b1a63a2 Mon Sep 17 00:00:00 2001 From: Ilia Alshanetsky Date: Thu, 16 Jul 2026 08:01:05 -0400 Subject: [PATCH 2/4] ext/standard: fix out-of-bounds access in the ftp:// directory stream php_ftp_dirstream_read() sized the basename copy with MIN(sizeof(ent->d_name), ZSTR_LEN(basename) - 1) and wrote the terminator at [tmp_len - 1]. An empty listing line leaves a basename of length 1, so tmp_len is 0 and the NUL lands one byte before d_name; a slash-only line leaves length 0, so the subtraction underflows to SIZE_MAX and memcpy() reads 4096 bytes past the interned empty string. The same off-by-one truncated entry names that do not end in CRLF. Closes GH-22768 --- ext/ftp/tests/server.inc | 2 +- ext/standard/ftp_fopen_wrapper.c | 4 +-- .../tests/streams/ftp_dirstream_oob.phpt | 33 +++++++++++++++++++ ext/standard/tests/streams/opendir-002.phpt | 2 +- ext/standard/tests/streams/opendir-004.phpt | 2 +- 5 files changed, 38 insertions(+), 5 deletions(-) create mode 100644 ext/standard/tests/streams/ftp_dirstream_oob.phpt diff --git a/ext/ftp/tests/server.inc b/ext/ftp/tests/server.inc index 4c9d2a754bff..add8c3ae70ac 100644 --- a/ext/ftp/tests/server.inc +++ b/ext/ftp/tests/server.inc @@ -290,7 +290,7 @@ if ($pid) { } if (empty($m[1]) || $m[1] !== 'emptydir') { - fputs($fs, "file1\r\nfile1\r\nfile\nb0rk\r\n"); + fputs($fs, $nlst_data ?? "file1\r\nfile1\r\nfile\nb0rk\r\n"); } fputs($s, "226 Closing data Connection.\r\n"); diff --git a/ext/standard/ftp_fopen_wrapper.c b/ext/standard/ftp_fopen_wrapper.c index 7b18e17f0d8f..fd7ecec02f19 100644 --- a/ext/standard/ftp_fopen_wrapper.c +++ b/ext/standard/ftp_fopen_wrapper.c @@ -625,9 +625,9 @@ static ssize_t php_ftp_dirstream_read(php_stream *stream, char *buf, size_t coun basename = php_basename(ent->d_name, tmp_len, NULL, 0); - tmp_len = MIN(sizeof(ent->d_name), ZSTR_LEN(basename) - 1); + tmp_len = MIN(sizeof(ent->d_name) - 1, ZSTR_LEN(basename)); memcpy(ent->d_name, ZSTR_VAL(basename), tmp_len); - ent->d_name[tmp_len - 1] = '\0'; + ent->d_name[tmp_len] = '\0'; zend_string_release_ex(basename, 0); ent->d_type = DT_UNKNOWN; diff --git a/ext/standard/tests/streams/ftp_dirstream_oob.phpt b/ext/standard/tests/streams/ftp_dirstream_oob.phpt new file mode 100644 index 000000000000..9e184f3c8ac6 --- /dev/null +++ b/ext/standard/tests/streams/ftp_dirstream_oob.phpt @@ -0,0 +1,33 @@ +--TEST-- +opendir() with 'ftp://' stream must not go out of bounds on an empty or slash-only listing line +--SKIPIF-- + +--FILE-- + +--EXPECTF-- +resource(%d) of type (stream) +string(5) "file1" +string(0) "" +string(4) "b0rk" +string(0) "" diff --git a/ext/standard/tests/streams/opendir-002.phpt b/ext/standard/tests/streams/opendir-002.phpt index 4978c8affa3d..0a5712564e2e 100644 --- a/ext/standard/tests/streams/opendir-002.phpt +++ b/ext/standard/tests/streams/opendir-002.phpt @@ -25,5 +25,5 @@ closedir($ds); resource(%d) of type (stream) string(5) "file1" string(5) "file1" -string(3) "fil" +string(4) "file" string(4) "b0rk" diff --git a/ext/standard/tests/streams/opendir-004.phpt b/ext/standard/tests/streams/opendir-004.phpt index 9ccb86b4f4b4..3f076e0d8386 100644 --- a/ext/standard/tests/streams/opendir-004.phpt +++ b/ext/standard/tests/streams/opendir-004.phpt @@ -27,5 +27,5 @@ while ($fn=readdir($ds)) { resource(%d) of type (stream) string(5) "file1" string(5) "file1" -string(3) "fil" +string(4) "file" string(4) "b0rk" From f9b6254375f4da163439daee1a564798250535ac Mon Sep 17 00:00:00 2001 From: Ilia Alshanetsky Date: Thu, 16 Jul 2026 12:41:51 -0400 Subject: [PATCH 3/4] ext/odbc: do not mutate the caller's DSN string odbc_sqlconnect() stripped a trailing semicolon by writing a NUL into the Z_PARAM_STRING buffer, which is a zend_string. Shared variables, interned literals, and const values used as the DSN were permanently altered (strlen unchanged, last byte became NUL). Compute the base length instead and pass it to spprintf with %.*s, matching the non-mutating approach used when assembling UID/PWD into the connect string. Closes GH-22776 --- ext/odbc/php_odbc.c | 12 ++++++------ ext/odbc/tests/gh_odbc_dsn_immutability.phpt | 20 ++++++++++++++++++++ 2 files changed, 26 insertions(+), 6 deletions(-) create mode 100644 ext/odbc/tests/gh_odbc_dsn_immutability.phpt diff --git a/ext/odbc/php_odbc.c b/ext/odbc/php_odbc.c index 16617078d3c1..8b78308f9426 100644 --- a/ext/odbc/php_odbc.c +++ b/ext/odbc/php_odbc.c @@ -2217,9 +2217,9 @@ bool odbc_sqlconnect(zval *zv, char *db, char *uid, char *pwd, int cur_opt, bool /* Force UID and PWD to be set in the DSN */ if (use_uid_arg || use_pwd_arg) { - db_end--; - if ((unsigned char)*(db_end) == ';') { - *db_end = '\0'; + size_t base_len = db_len; + if (base_len > 0 && db[base_len - 1] == ';') { + base_len--; } char *uid_quoted = NULL, *pwd_quoted = NULL; @@ -2235,7 +2235,7 @@ bool odbc_sqlconnect(zval *zv, char *db, char *uid, char *pwd, int cur_opt, bool } if (!use_pwd_arg) { - spprintf(&ldb, 0, "%s;UID=%s;", db, uid_quoted); + spprintf(&ldb, 0, "%.*s;UID=%s;", (int) base_len, db, uid_quoted); } } @@ -2250,12 +2250,12 @@ bool odbc_sqlconnect(zval *zv, char *db, char *uid, char *pwd, int cur_opt, bool } if (!use_uid_arg) { - spprintf(&ldb, 0, "%s;PWD=%s;", db, pwd_quoted); + spprintf(&ldb, 0, "%.*s;PWD=%s;", (int) base_len, db, pwd_quoted); } } if (use_uid_arg && use_pwd_arg) { - spprintf(&ldb, 0, "%s;UID=%s;PWD=%s;", db, uid_quoted, pwd_quoted); + spprintf(&ldb, 0, "%.*s;UID=%s;PWD=%s;", (int) base_len, db, uid_quoted, pwd_quoted); } if (uid_quoted && should_quote_uid) { diff --git a/ext/odbc/tests/gh_odbc_dsn_immutability.phpt b/ext/odbc/tests/gh_odbc_dsn_immutability.phpt new file mode 100644 index 000000000000..e3023b387c70 --- /dev/null +++ b/ext/odbc/tests/gh_odbc_dsn_immutability.phpt @@ -0,0 +1,20 @@ +--TEST-- +odbc_connect() must not mutate the caller's DSN string +--EXTENSIONS-- +odbc +--FILE-- + +--EXPECT-- +string(37) "Driver=DoesNotExist;Database=x;SS001;" +string(43) "Driver=DoesNotExist;Database=x;SS001_CONST;" From 36ba49eaf2e83026f184b61fd023faf73057c1c3 Mon Sep 17 00:00:00 2001 From: Ilia Alshanetsky Date: Thu, 16 Jul 2026 08:05:16 -0400 Subject: [PATCH 4/4] ext/standard: reject a dechunk chunk size that overflows size_t php_dechunk() accumulated the hex chunk size with chunk_size * 16 + digit and never checked the multiply, so a size of 2^64 wrapped to 0. A zero size reads as the terminating chunk, so the filter stopped there and dropped the body it had been handed. Error out instead, as the other malformed-size cases already do. --- ext/standard/filters.c | 13 +++++-- .../tests/filters/dechunk_size_overflow.phpt | 38 +++++++++++++++++++ 2 files changed, 48 insertions(+), 3 deletions(-) create mode 100644 ext/standard/tests/filters/dechunk_size_overflow.phpt diff --git a/ext/standard/filters.c b/ext/standard/filters.c index a7c0a035a239..0c08600648cd 100644 --- a/ext/standard/filters.c +++ b/ext/standard/filters.c @@ -1715,12 +1715,14 @@ static size_t php_dechunk(char *buf, size_t len, php_chunked_filter_data *data) data->chunk_size = 0; case CHUNK_SIZE: while (p < end) { + size_t digit; + if (*p >= '0' && *p <= '9') { - data->chunk_size = (data->chunk_size * 16) + (*p - '0'); + digit = *p - '0'; } else if (*p >= 'A' && *p <= 'F') { - data->chunk_size = (data->chunk_size * 16) + (*p - 'A' + 10); + digit = *p - 'A' + 10; } else if (*p >= 'a' && *p <= 'f') { - data->chunk_size = (data->chunk_size * 16) + (*p - 'a' + 10); + digit = *p - 'a' + 10; } else if (data->state == CHUNK_SIZE_START) { data->state = CHUNK_ERROR; break; @@ -1728,6 +1730,11 @@ static size_t php_dechunk(char *buf, size_t len, php_chunked_filter_data *data) data->state = CHUNK_SIZE_EXT; break; } + if (data->chunk_size > (SIZE_MAX / 16)) { + data->state = CHUNK_ERROR; + break; + } + data->chunk_size = (data->chunk_size * 16) + digit; data->state = CHUNK_SIZE; p++; } diff --git a/ext/standard/tests/filters/dechunk_size_overflow.phpt b/ext/standard/tests/filters/dechunk_size_overflow.phpt new file mode 100644 index 000000000000..ac42733e16e0 --- /dev/null +++ b/ext/standard/tests/filters/dechunk_size_overflow.phpt @@ -0,0 +1,38 @@ +--TEST-- +dechunk filter must reject a chunk size that overflows size_t +--SKIPIF-- + +--INI-- +allow_url_fopen=1 +--FILE-- + +--EXPECTF-- +string(%d) "%s +BODYDATA +0 +" +string(%d) "%s +BODYDATA +0 +" +string(5) "hello"