File php7-CVE-2025-1217.patch of Package php7.37963
Index: php-7.4.33/ext/standard/http_fopen_wrapper.c
===================================================================
--- php-7.4.33.orig/ext/standard/http_fopen_wrapper.c
+++ php-7.4.33/ext/standard/http_fopen_wrapper.c
@@ -30,6 +30,7 @@
#include <stdio.h>
#include <stdlib.h>
+#include <stdbool.h>
#include <errno.h>
#include <sys/types.h>
#include <sys/stat.h>
@@ -82,6 +83,22 @@
#define HTTP_WRAPPER_HEADER_INIT 1
#define HTTP_WRAPPER_REDIRECTED 2
+zend_string *zend_string_concat3(
+ const char *str1, size_t str1_len,
+ const char *str2, size_t str2_len,
+ const char *str3, size_t str3_len)
+{
+ size_t len = str1_len + str2_len + str3_len;
+ zend_string *res = zend_string_alloc(len, 0);
+
+ memcpy(ZSTR_VAL(res), str1, str1_len);
+ memcpy(ZSTR_VAL(res) + str1_len, str2, str2_len);
+ memcpy(ZSTR_VAL(res) + str1_len + str2_len, str3, str3_len);
+ ZSTR_VAL(res)[len] = '\0';
+
+ return res;
+}
+
static inline void strip_header(char *header_bag, char *lc_header_bag,
const char *lc_header_name)
{
@@ -116,6 +133,171 @@ static zend_bool check_has_header(const
return 0;
}
+typedef struct _php_stream_http_response_header_info {
+ php_stream_filter *transfer_encoding;
+ size_t file_size;
+ bool follow_location;
+ char location[HTTP_HEADER_BLOCK_SIZE];
+} php_stream_http_response_header_info;
+
+static void php_stream_http_response_header_info_init(
+ php_stream_http_response_header_info *header_info)
+{
+ header_info->transfer_encoding = NULL;
+ header_info->file_size = 0;
+ header_info->follow_location = 1;
+ header_info->location[0] = '\0';
+}
+
+/* Trim white spaces from response header line and update its length */
+static bool php_stream_http_response_header_trim(char *http_header_line,
+ size_t *http_header_line_length)
+{
+ char *http_header_line_end = http_header_line + *http_header_line_length - 1;
+ while (http_header_line_end >= http_header_line &&
+ (*http_header_line_end == '\n' || *http_header_line_end == '\r')) {
+ http_header_line_end--;
+ }
+
+ /* The primary definition of an HTTP header in RFC 7230 states:
+ * > Each header field consists of a case-insensitive field name followed
+ * > by a colon (":"), optional leading whitespace, the field value, and
+ * > optional trailing whitespace. */
+
+ /* Strip trailing whitespace */
+ bool space_trim = (*http_header_line_end == ' ' || *http_header_line_end == '\t');
+ if (space_trim) {
+ do {
+ http_header_line_end--;
+ } while (http_header_line_end >= http_header_line &&
+ (*http_header_line_end == ' ' || *http_header_line_end == '\t'));
+ }
+ http_header_line_end++;
+ *http_header_line_end = '\0';
+ *http_header_line_length = http_header_line_end - http_header_line;
+
+ return space_trim;
+}
+
+/* Process folding headers of the current line and if there are none, parse last full response
+ * header line. It returns NULL if the last header is finished, otherwise it returns updated
+ * last header line. */
+static zend_string *php_stream_http_response_headers_parse(php_stream *stream,
+ php_stream_context *context, int options, zend_string *last_header_line_str,
+ char *header_line, size_t *header_line_length, int response_code,
+ zval *response_header, php_stream_http_response_header_info *header_info)
+{
+ char *last_header_line = ZSTR_VAL(last_header_line_str);
+ size_t last_header_line_length = ZSTR_LEN(last_header_line_str);
+ char *last_header_line_end = ZSTR_VAL(last_header_line_str) + ZSTR_LEN(last_header_line_str) - 1;
+
+ /* Process non empty header line. */
+ if (header_line && (*header_line != '\n' && *header_line != '\r')) {
+ /* Removing trailing white spaces. */
+ if (php_stream_http_response_header_trim(header_line, header_line_length) &&
+ *header_line_length == 0) {
+ /* Only spaces so treat as an empty folding header. */
+ return last_header_line_str;
+ }
+
+ /* Process folding headers if starting with a space or a tab. */
+ if (header_line && (*header_line == ' ' || *header_line == '\t')) {
+ char *http_folded_header_line = header_line;
+ size_t http_folded_header_line_length = *header_line_length;
+ /* Remove the leading white spaces. */
+ while (*http_folded_header_line == ' ' || *http_folded_header_line == '\t') {
+ http_folded_header_line++;
+ http_folded_header_line_length--;
+ }
+ /* It has to have some characters because it would get returned after the call
+ * php_stream_http_response_header_trim above. */
+ ZEND_ASSERT(http_folded_header_line_length > 0);
+ /* Concatenate last header line, space and current header line. */
+ zend_string *extended_header_str = zend_string_concat3(
+ last_header_line, last_header_line_length,
+ " ", 1,
+ http_folded_header_line, http_folded_header_line_length);
+ zend_string_efree(last_header_line_str);
+ last_header_line_str = extended_header_str;
+ /* Return new header line. */
+ return last_header_line_str;
+ }
+ }
+
+ /* Find header separator position. */
+ char *last_header_value = memchr(last_header_line, ':', last_header_line_length);
+ if (last_header_value) {
+ last_header_value++; /* Skip ':'. */
+
+ /* Strip leading whitespace. */
+ while (last_header_value < last_header_line_end
+ && (*last_header_value == ' ' || *last_header_value == '\t')) {
+ last_header_value++;
+ }
+ } else {
+ /* There is no colon. Set the value to the end of the header line, which is effectively
+ * an empty string. */
+ last_header_value = last_header_line_end;
+ }
+
+ bool store_header = true;
+ zval *tmpzval = NULL;
+
+ if (!strncasecmp(last_header_line, "Location:", sizeof("Location:")-1)) {
+ /* Check if the location should be followed. */
+ if (context && (tmpzval = php_stream_context_get_option(context, "http", "follow_location")) != NULL) {
+ header_info->follow_location = zval_is_true(tmpzval);
+ } else if (!((response_code >= 300 && response_code < 304)
+ || 307 == response_code || 308 == response_code)) {
+ /* The redirection should not be automatic if follow_location is not set and
+ * response_code not in (300, 301, 302, 303 and 307)
+ * see http://www.w3.org/Protocols/rfc2616/rfc2616-sec10.html#sec10.3.1
+ * RFC 7238 defines 308: http://tools.ietf.org/html/rfc7238 */
+ header_info->follow_location = 0;
+ }
+ strlcpy(header_info->location, last_header_value, sizeof(header_info->location));
+ } else if (!strncasecmp(last_header_line, "Content-Type:", sizeof("Content-Type:")-1)) {
+ php_stream_notify_info(context, PHP_STREAM_NOTIFY_MIME_TYPE_IS, last_header_value, 0);
+ } else if (!strncasecmp(last_header_line, "Content-Length:", sizeof("Content-Length:")-1)) {
+ header_info->file_size = atoi(last_header_value);
+ php_stream_notify_file_size(context, header_info->file_size, last_header_line, 0);
+ } else if (
+ !strncasecmp(last_header_line, "Transfer-Encoding:", sizeof("Transfer-Encoding:")-1)
+ && !strncasecmp(last_header_value, "Chunked", sizeof("Chunked")-1)
+ ) {
+ /* Create filter to decode response body. */
+ if (!(options & STREAM_ONLY_GET_HEADERS)) {
+ zend_long decode = 1;
+
+ if (context && (tmpzval = php_stream_context_get_option(context, "http", "auto_decode")) != NULL) {
+ decode = zend_is_true(tmpzval);
+ }
+ if (decode) {
+ if (header_info->transfer_encoding != NULL) {
+ /* Prevent a memory leak in case there are more transfer-encoding headers. */
+ php_stream_filter_free(header_info->transfer_encoding);
+ }
+ header_info->transfer_encoding = php_stream_filter_create(
+ "dechunk", NULL, php_stream_is_persistent(stream));
+ if (header_info->transfer_encoding != NULL) {
+ /* Do not store transfer-encoding header. */
+ store_header = false;
+ }
+ }
+ }
+ }
+
+ if (store_header) {
+ zval http_header;
+ ZVAL_NEW_STR(&http_header, last_header_line_str);
+ zend_hash_next_index_insert(Z_ARRVAL_P(response_header), &http_header);
+ } else {
+ zend_string_efree(last_header_line_str);
+ }
+
+ return NULL;
+}
+
static php_stream *php_stream_url_wrap_http_ex(php_stream_wrapper *wrapper,
const char *path, const char *mode, int options, zend_string **opened_path,
php_stream_context *context, int redirect_max, int flags,
@@ -128,11 +310,12 @@ static php_stream *php_stream_url_wrap_h
zend_string *tmp = NULL;
char *ua_str = NULL;
zval *ua_zval = NULL, *tmpzval = NULL, ssl_proxy_peer_name;
- char location[HTTP_HEADER_BLOCK_SIZE];
int reqok = 0;
char *http_header_line = NULL;
+ zend_string *last_header_line_str = NULL;
+ php_stream_http_response_header_info header_info;
char tmp_line[128];
- size_t chunk_size = 0, file_size = 0;
+ size_t chunk_size = 0;
int eol_detect = 0;
char *transport_string;
zend_string *errstr = NULL;
@@ -143,8 +326,6 @@ static php_stream *php_stream_url_wrap_h
char *user_headers = NULL;
int header_init = ((flags & HTTP_WRAPPER_HEADER_INIT) != 0);
int redirected = ((flags & HTTP_WRAPPER_REDIRECTED) != 0);
- zend_bool follow_location = 1;
- php_stream_filter *transfer_encoding = NULL;
int response_code;
smart_str req_buf = {0};
zend_bool custom_request_method;
@@ -657,8 +838,6 @@ finish:
/* send it */
php_stream_write(stream, ZSTR_VAL(req_buf.s), ZSTR_LEN(req_buf.s));
- location[0] = '\0';
-
if (Z_ISUNDEF_P(response_header)) {
array_init(response_header);
}
@@ -736,125 +915,96 @@ finish:
}
}
- /* read past HTTP headers */
+ php_stream_http_response_header_info_init(&header_info);
+ /* read past HTTP headers */
while (!php_stream_eof(stream)) {
size_t http_header_line_length;
if (http_header_line != NULL) {
efree(http_header_line);
}
- if ((http_header_line = php_stream_get_line(stream, NULL, 0, &http_header_line_length)) && *http_header_line != '\n' && *http_header_line != '\r') {
- char *e = http_header_line + http_header_line_length - 1;
- char *http_header_value;
-
- while (e >= http_header_line && (*e == '\n' || *e == '\r')) {
- e--;
- }
-
- /* The primary definition of an HTTP header in RFC 7230 states:
- * > Each header field consists of a case-insensitive field name followed
- * > by a colon (":"), optional leading whitespace, the field value, and
- * > optional trailing whitespace. */
-
- /* Strip trailing whitespace */
- while (e >= http_header_line && (*e == ' ' || *e == '\t')) {
- e--;
- }
-
- /* Terminate header line */
- e++;
- *e = '\0';
- http_header_line_length = e - http_header_line;
-
- http_header_value = memchr(http_header_line, ':', http_header_line_length);
- if (http_header_value) {
- http_header_value++; /* Skip ':' */
-
- /* Strip leading whitespace */
- while (http_header_value < e
- && (*http_header_value == ' ' || *http_header_value == '\t')) {
- http_header_value++;
- }
+ if ((http_header_line = php_stream_get_line(stream, NULL, 0, &http_header_line_length))) {
+ bool last_line;
+ if (*http_header_line == '\r') {
+ if (http_header_line[1] != '\n') {
+ php_stream_close(stream);
+ stream = NULL;
+ php_stream_wrapper_log_error(wrapper, options,
+ "HTTP invalid header name (cannot start with CR character)!");
+ goto out;
+ }
+ last_line = true;
+ } else if (*http_header_line == '\n') {
+ last_line = true;
} else {
- /* There is no colon. Set the value to the end of the header line, which is
- * effectively an empty string. */
- http_header_value = e;
+ last_line = false;
}
- if (!strncasecmp(http_header_line, "Location:", sizeof("Location:")-1)) {
- if (context && (tmpzval = php_stream_context_get_option(context, "http", "follow_location")) != NULL) {
- follow_location = zval_is_true(tmpzval);
- } else if (!((response_code >= 300 && response_code < 304)
- || 307 == response_code || 308 == response_code)) {
- /* we shouldn't redirect automatically
- if follow_location isn't set and response_code not in (300, 301, 302, 303 and 307)
- see http://www.w3.org/Protocols/rfc2616/rfc2616-sec10.html#sec10.3.1
- RFC 7238 defines 308: http://tools.ietf.org/html/rfc7238 */
- follow_location = 0;
- }
- strlcpy(location, http_header_value, sizeof(location));
- } else if (!strncasecmp(http_header_line, "Content-Type:", sizeof("Content-Type:")-1)) {
- php_stream_notify_info(context, PHP_STREAM_NOTIFY_MIME_TYPE_IS, http_header_value, 0);
- } else if (!strncasecmp(http_header_line, "Content-Length:", sizeof("Content-Length:")-1)) {
- file_size = atoi(http_header_value);
- php_stream_notify_file_size(context, file_size, http_header_line, 0);
- } else if (
- !strncasecmp(http_header_line, "Transfer-Encoding:", sizeof("Transfer-Encoding:")-1)
- && !strncasecmp(http_header_value, "Chunked", sizeof("Chunked")-1)
- ) {
-
- /* create filter to decode response body */
- if (!(options & STREAM_ONLY_GET_HEADERS)) {
- zend_long decode = 1;
-
- if (context && (tmpzval = php_stream_context_get_option(context, "http", "auto_decode")) != NULL) {
- decode = zend_is_true(tmpzval);
- }
- if (decode) {
- transfer_encoding = php_stream_filter_create("dechunk", NULL, php_stream_is_persistent(stream));
- if (transfer_encoding) {
- /* don't store transfer-encodeing header */
- continue;
- }
- }
- }
- }
-
- {
- zval http_header;
- ZVAL_STRINGL(&http_header, http_header_line, http_header_line_length);
- zend_hash_next_index_insert(Z_ARRVAL_P(response_header), &http_header);
- }
+ if (last_header_line_str != NULL) {
+ /* Parse last header line. */
+ last_header_line_str = php_stream_http_response_headers_parse(stream, context,
+ options, last_header_line_str, http_header_line, &http_header_line_length,
+ response_code, response_header, &header_info);
+ if (last_header_line_str != NULL) {
+ /* Folding header present so continue. */
+ continue;
+ }
+ } else if (!last_line) {
+ /* The first line cannot start with spaces. */
+ if (*http_header_line == ' ' || *http_header_line == '\t') {
+ php_stream_close(stream);
+ stream = NULL;
+ php_stream_wrapper_log_error(wrapper, options,
+ "HTTP invalid response format (folding header at the start)!");
+ goto out;
+ }
+ /* Trim the first line if it is not the last line. */
+ php_stream_http_response_header_trim(http_header_line, &http_header_line_length);
+ }
+ if (last_line) {
+ /* For the last line the last header line must be NULL. */
+ ZEND_ASSERT(last_header_line_str == NULL);
+ break;
+ }
+ /* Save current line as the last line so it gets parsed in the next round. */
+ last_header_line_str = zend_string_init(http_header_line, http_header_line_length, 0);
} else {
break;
}
}
- if (!reqok || (location[0] != '\0' && follow_location)) {
- if (!follow_location || (((options & STREAM_ONLY_GET_HEADERS) || ignore_errors) && redirect_max <= 1)) {
+ /* If the stream was closed early, we still want to process the last line to keep BC. */
+ if (last_header_line_str != NULL) {
+ php_stream_http_response_headers_parse(stream, context, options, last_header_line_str,
+ NULL, NULL, response_code, response_header, &header_info);
+ }
+
+ if (!reqok || (header_info.location[0] != '\0' && header_info.follow_location)) {
+ if (!header_info.follow_location || (((options & STREAM_ONLY_GET_HEADERS) || ignore_errors) && redirect_max <= 1)) {
goto out;
}
- if (location[0] != '\0')
- php_stream_notify_info(context, PHP_STREAM_NOTIFY_REDIRECTED, location, 0);
+ if (header_info.location[0] != '\0')
+ php_stream_notify_info(context, PHP_STREAM_NOTIFY_REDIRECTED, header_info.location, 0);
php_stream_close(stream);
stream = NULL;
- if (location[0] != '\0') {
+ if (header_info.location[0] != '\0') {
char new_path[HTTP_HEADER_BLOCK_SIZE];
char loc_path[HTTP_HEADER_BLOCK_SIZE];
*new_path='\0';
- if (strlen(location)<8 || (strncasecmp(location, "http://", sizeof("http://")-1) &&
- strncasecmp(location, "https://", sizeof("https://")-1) &&
- strncasecmp(location, "ftp://", sizeof("ftp://")-1) &&
- strncasecmp(location, "ftps://", sizeof("ftps://")-1)))
+ if (strlen(header_info.location) < 8 ||
+ (strncasecmp(header_info.location, "http://", sizeof("http://")-1) &&
+ strncasecmp(header_info.location, "https://", sizeof("https://")-1) &&
+ strncasecmp(header_info.location, "ftp://", sizeof("ftp://")-1) &&
+ strncasecmp(header_info.location, "ftps://", sizeof("ftps://")-1)))
{
- if (*location != '/') {
- if (*(location+1) != '\0' && resource->path) {
+ if (*header_info.location != '/') {
+ if (*(header_info.location+1) != '\0' && resource->path) {
char *s = strrchr(ZSTR_VAL(resource->path), '/');
if (!s) {
s = ZSTR_VAL(resource->path);
@@ -870,15 +1020,17 @@ finish:
if (resource->path &&
ZSTR_VAL(resource->path)[0] == '/' &&
ZSTR_VAL(resource->path)[1] == '\0') {
- snprintf(loc_path, sizeof(loc_path) - 1, "%s%s", ZSTR_VAL(resource->path), location);
+ snprintf(loc_path, sizeof(loc_path) - 1, "%s%s",
+ ZSTR_VAL(resource->path), header_info.location);
} else {
- snprintf(loc_path, sizeof(loc_path) - 1, "%s/%s", ZSTR_VAL(resource->path), location);
+ snprintf(loc_path, sizeof(loc_path) - 1, "%s/%s",
+ ZSTR_VAL(resource->path), header_info.location);
}
} else {
- snprintf(loc_path, sizeof(loc_path) - 1, "/%s", location);
+ snprintf(loc_path, sizeof(loc_path) - 1, "/%s", header_info.location);
}
} else {
- strlcpy(loc_path, location, sizeof(loc_path));
+ strlcpy(loc_path, header_info.location, sizeof(loc_path));
}
if ((use_ssl && resource->port != 443) || (!use_ssl && resource->port != 80)) {
snprintf(new_path, sizeof(new_path) - 1, "%s://%s:%d%s", ZSTR_VAL(resource->scheme), ZSTR_VAL(resource->host), resource->port, loc_path);
@@ -886,7 +1038,7 @@ finish:
snprintf(new_path, sizeof(new_path) - 1, "%s://%s%s", ZSTR_VAL(resource->scheme), ZSTR_VAL(resource->host), loc_path);
}
} else {
- strlcpy(new_path, location, sizeof(new_path));
+ strlcpy(new_path, header_info.location, sizeof(new_path));
}
php_url_free(resource);
@@ -939,7 +1091,7 @@ out:
if (header_init) {
ZVAL_COPY(&stream->wrapperdata, response_header);
}
- php_stream_notify_progress_init(context, 0, file_size);
+ php_stream_notify_progress_init(context, 0, header_info.file_size);
/* Restore original chunk size now that we're done with headers */
if (options & STREAM_WILL_CAST)
@@ -955,12 +1107,8 @@ out:
/* restore mode */
strlcpy(stream->mode, mode, sizeof(stream->mode));
- if (transfer_encoding) {
- php_stream_filter_append(&stream->readfilters, transfer_encoding);
- }
- } else {
- if (transfer_encoding) {
- php_stream_filter_free(transfer_encoding);
+ if (header_info.transfer_encoding) {
+ php_stream_filter_append(&stream->readfilters, header_info.transfer_encoding);
}
}