From c7249aeadabe95d19bd172707c59a919d1127acd Mon Sep 17 00:00:00 2001 From: AMMAAR-IC Date: Sat, 24 Oct 2026 03:14:39 +0530 Subject: [PATCH 1/8] ext/soap: Do not send credentials to another origin after a redirect SoapClient follows Location redirects to any host, and kept sending the Authorization header built from the login and password options, as well as the Authorization and Cookie headers of the http stream context, to the redirect target. A redirect to another origin, including an https to http downgrade, thus leaked the credentials. Withhold them once a redirect leaves the origin, the same way the http stream wrapper does since GHSA-fpwc-w8rq-cr92. Digest authentication is not attempted against the new origin either. --- ext/soap/php_http.c | 32 ++++++- .../bugs/redirect_strip_credentials.phpt | 90 +++++++++++++++++++ 2 files changed, 119 insertions(+), 3 deletions(-) create mode 100644 ext/soap/tests/bugs/redirect_strip_credentials.phpt diff --git a/ext/soap/php_http.c b/ext/soap/php_http.c index d64a110cb1c8..3545b1bd5f78 100644 --- a/ext/soap/php_http.c +++ b/ext/soap/php_http.c @@ -347,6 +347,25 @@ static bool in_domain(const zend_string *host, const zend_string *domain) } } +/* The port of from must already be resolved, the port of to may still be 0 */ +static bool same_origin(const php_uri *from, const php_uri *to) +{ + if (from->scheme == NULL || to->scheme == NULL || from->host == NULL || to->host == NULL) { + return false; + } + + if (!zend_string_equals_ci(from->scheme, to->scheme) || !zend_string_equals_ci(from->host, to->host)) { + return false; + } + + zend_long to_port = to->port; + if (to_port == 0) { + to_port = zend_string_equals_literal_ci(to->scheme, "https") ? 443 : 80; + } + + return from->port == to_port; +} + bool make_http_soap_request( zval *this_ptr, zend_string *buf, zend_string *location, char *soapaction, int soap_version, zend_string *uri_parser_class, zval *return_value @@ -376,6 +395,8 @@ bool make_http_soap_request( bool has_authorization = false; bool has_proxy_authorization = false; bool has_cookies = false; + /* Set once a redirect left the origin, credentials must not follow it */ + bool strip_auth = false; if (this_ptr == NULL || Z_TYPE_P(this_ptr) != IS_OBJECT) { return false; @@ -687,7 +708,7 @@ bool make_http_soap_request( /* HTTP Authentication */ login = Z_CLIENT_LOGIN_P(this_ptr); - if (Z_TYPE_P(login) == IS_STRING) { + if (Z_TYPE_P(login) == IS_STRING && !strip_auth) { zval *digest = Z_CLIENT_DIGEST_P(this_ptr); has_authorization = true; @@ -899,7 +920,9 @@ bool make_http_soap_request( smart_str_append_const(&soap_headers, "\r\n"); } - http_context_headers(context, has_authorization, has_proxy_authorization, has_cookies, &soap_headers); + /* Passing true for the credential headers makes it skip them */ + http_context_headers(context, has_authorization || strip_auth, has_proxy_authorization, + has_cookies || strip_auth, &soap_headers); smart_str_append_const(&soap_headers, "\r\n"); smart_str_0(&soap_headers); @@ -1191,6 +1214,9 @@ bool make_http_soap_request( } } } + if (!same_origin(uri, new_uri)) { + strip_auth = true; + } uri = new_uri; if (--redirect_max < 1) { @@ -1210,7 +1236,7 @@ bool make_http_soap_request( zval *password = Z_CLIENT_PASSWORD_P(this_ptr); char *auth = get_http_header_value(http_headers, "WWW-Authenticate:"); if (auth && strstr(auth, "Digest") == auth && Z_TYPE_P(digest) != IS_ARRAY - && Z_TYPE_P(login) == IS_STRING && Z_TYPE_P(password) == IS_STRING) { + && Z_TYPE_P(login) == IS_STRING && Z_TYPE_P(password) == IS_STRING && !strip_auth) { char *s; zval digest; diff --git a/ext/soap/tests/bugs/redirect_strip_credentials.phpt b/ext/soap/tests/bugs/redirect_strip_credentials.phpt new file mode 100644 index 000000000000..89e554c71082 --- /dev/null +++ b/ext/soap/tests/bugs/redirect_strip_credentials.phpt @@ -0,0 +1,90 @@ +--TEST-- +SOAP client does not send credentials to another origin after a redirect +--EXTENSIONS-- +soap +--SKIPIF-- + +--FILE-- +' + . '' + . 'ok' + . ''; +$ok = "data://text/plain,HTTP/1.1 200 OK\r\nContent-Type: text/xml\r\nContent-Length: " . strlen($response) . "\r\n\r\n" . $response; + +$ctx = stream_context_create(['http' => [ + 'header' => "Authorization: Bearer SECRET\r\nCookie: sid=abc\r\nX-Custom: keep-me", +]]); + +function call(string $location, $ctx): void { + $client = new SoapClient(null, [ + 'location' => $location, + 'uri' => 'test-uri', + 'login' => 'user', + 'password' => 'secret', + 'stream_context' => $ctx, + 'keep_alive' => false, + ]); + var_dump($client->__soapCall('foo', [])); +} + +/* B is on another port than A, so the hop from A to B is cross-origin; B then + * redirects to itself, credentials must stay withheld for that hop too */ +['pid' => $pidB, 'uri' => $uriB] = http_server([ + "data://text/plain,HTTP/1.1 302 Found\r\nLocation: /second\r\nContent-Length: 0\r\n\r\n", + $ok, +], $captureB); +['pid' => $pidA, 'uri' => $uriA] = http_server([ + "data://text/plain,HTTP/1.1 302 Found\r\nLocation: $uriB/first\r\nContent-Length: 0\r\n\r\n", +], $captureA); + +call("$uriA/src", $ctx); + +http_server_kill($pidA); +http_server_kill($pidB); + +rewind($captureA); +rewind($captureB); +report('--- origin A (1 request) ---', stream_get_contents($captureA)); +report('--- origin B (2 requests) ---', stream_get_contents($captureB)); + +/* Same origin throughout: credentials are sent on both hops */ +['pid' => $pidC, 'uri' => $uriC] = http_server([ + "data://text/plain,HTTP/1.1 302 Found\r\nLocation: /next\r\nContent-Length: 0\r\n\r\n", + $ok, +], $captureC); + +call("$uriC/first", $ctx); + +http_server_kill($pidC); + +rewind($captureC); +report('--- origin C (2 requests) ---', stream_get_contents($captureC)); +?> +--EXPECT-- +string(2) "ok" +--- origin A (1 request) --- + Authorization: 1 + Cookie: 1 + X-Custom: 1 +--- origin B (2 requests) --- + Authorization: 0 + Cookie: 0 + X-Custom: 2 +string(2) "ok" +--- origin C (2 requests) --- + Authorization: 2 + Cookie: 2 + X-Custom: 2 From 7ff9f1d055b45f8b44ed65567b79dad5ee1cbb1a Mon Sep 17 00:00:00 2001 From: AMMAAR-IC Date: Sat, 24 Oct 2026 03:18:18 +0530 Subject: [PATCH 2/8] ext/soap: Truncate Digest authentication values containing newlines The Digest Authorization header was built from the login option and the realm, nonce, opaque and algorithm values of the server challenge without the newline check that the other SoapClient request headers go through since f6fe838e364. A CR/LF in the login thus injected arbitrary request headers, while Basic authentication base64-encodes it. Emit these values through soap_smart_str_append_header_value() as well. --- ext/soap/php_http.c | 10 ++-- .../tests/digest_auth_header_injection.phpt | 49 +++++++++++++++++++ 2 files changed, 54 insertions(+), 5 deletions(-) create mode 100644 ext/soap/tests/digest_auth_header_injection.phpt diff --git a/ext/soap/php_http.c b/ext/soap/php_http.c index 3545b1bd5f78..81bbdaf5e548 100644 --- a/ext/soap/php_http.c +++ b/ext/soap/php_http.c @@ -811,16 +811,16 @@ bool make_http_soap_request( make_digest(response, hash); smart_str_append_const(&soap_headers, "Authorization: Digest username=\""); - smart_str_append(&soap_headers, Z_STR_P(login)); + soap_smart_str_append_header_value(&soap_headers, Z_STR_P(login), "Authorization"); if ((tmp = zend_hash_str_find(Z_ARRVAL_P(digest), "realm", sizeof("realm")-1)) != NULL && Z_TYPE_P(tmp) == IS_STRING) { smart_str_append_const(&soap_headers, "\", realm=\""); - smart_str_append(&soap_headers, Z_STR_P(tmp)); + soap_smart_str_append_header_value(&soap_headers, Z_STR_P(tmp), "Authorization"); } if ((tmp = zend_hash_str_find(Z_ARRVAL_P(digest), "nonce", sizeof("nonce")-1)) != NULL && Z_TYPE_P(tmp) == IS_STRING) { smart_str_append_const(&soap_headers, "\", nonce=\""); - smart_str_append(&soap_headers, Z_STR_P(tmp)); + soap_smart_str_append_header_value(&soap_headers, Z_STR_P(tmp), "Authorization"); } smart_str_append_const(&soap_headers, "\", uri=\""); if (uri->path) { @@ -850,12 +850,12 @@ bool make_http_soap_request( if ((tmp = zend_hash_str_find(Z_ARRVAL_P(digest), "opaque", sizeof("opaque")-1)) != NULL && Z_TYPE_P(tmp) == IS_STRING) { smart_str_append_const(&soap_headers, "\", opaque=\""); - smart_str_append(&soap_headers, Z_STR_P(tmp)); + soap_smart_str_append_header_value(&soap_headers, Z_STR_P(tmp), "Authorization"); } if ((tmp = zend_hash_str_find(Z_ARRVAL_P(digest), "algorithm", sizeof("algorithm")-1)) != NULL && Z_TYPE_P(tmp) == IS_STRING) { smart_str_append_const(&soap_headers, "\", algorithm=\""); - smart_str_append(&soap_headers, Z_STR_P(tmp)); + soap_smart_str_append_header_value(&soap_headers, Z_STR_P(tmp), "Authorization"); } smart_str_append_const(&soap_headers, "\"\r\n"); } else { diff --git a/ext/soap/tests/digest_auth_header_injection.phpt b/ext/soap/tests/digest_auth_header_injection.phpt new file mode 100644 index 000000000000..0db49958fa2c --- /dev/null +++ b/ext/soap/tests/digest_auth_header_injection.phpt @@ -0,0 +1,49 @@ +--TEST-- +SoapClient must truncate Digest authentication values that contain newline characters +--EXTENSIONS-- +soap +--SKIPIF-- + +--FILE-- +' + . '' + . 'ok' + . ''; + +['pid' => $pid, 'uri' => $uri] = http_server([ + "data://text/plain,HTTP/1.1 401 Unauthorized\r\nWWW-Authenticate: Digest realm=\"r\rX-Realm: yes\", nonce=\"n\", qop=\"auth\"\r\nContent-Length: 0\r\n\r\n", + "data://text/plain,HTTP/1.1 200 OK\r\nContent-Type: text/xml\r\nContent-Length: " . strlen($response) . "\r\n\r\n" . $response, +], $capture); + +$client = new SoapClient(null, [ + 'location' => "$uri/", + 'uri' => 'test-uri', + 'login' => "user\r\nX-Injected: yes", + 'password' => 'secret', + 'authentication' => SOAP_AUTHENTICATION_DIGEST, + 'keep_alive' => false, +]); +var_dump($client->__soapCall('foo', [])); + +http_server_kill($pid); + +rewind($capture); +$requests = stream_get_contents($capture); +var_dump(preg_match_all('/^Authorization: Digest username="user", realm="r", nonce="n"/m', $requests)); +var_dump(preg_match('/^X-Injected:/m', $requests)); +var_dump(str_contains($requests, "\rX-Realm")); +?> +--EXPECTF-- +Warning: SoapClient::__doRequest(): Header Authorization value contains newline characters and has been truncated in %s on line %d + +Warning: SoapClient::__doRequest(): Header Authorization value contains newline characters and has been truncated in %s on line %d +string(2) "ok" +int(1) +int(0) +bool(false) From 83d7376e8932875c1d9a1e9839fc07a60344a6a2 Mon Sep 17 00:00:00 2001 From: AMMAAR-IC Date: Sat, 24 Oct 2026 03:19:49 +0530 Subject: [PATCH 3/8] ext/mysqlnd: Check that a binary row packet holds its NULL bitmap php_mysqlnd_rowp_read_binary_protocol() skipped the status byte and the (field_count + 9) / 8 bytes of the NULL bitmap and then read the bitmap without checking that the row packet is that long. A malicious server could send a shorter row packet and make the client read past it. Fail with a warning when the packet cannot hold the status byte and the bitmap, as is done for the field values since GHSA-r6x9-5r99-36j7. --- ext/mysqli/tests/fake_server.inc | 24 ++++++++++ .../mysqlnd_stmt_row_no_null_bitmap.phpt | 45 +++++++++++++++++++ ext/mysqlnd/mysqlnd_wireprotocol.c | 6 +++ 3 files changed, 75 insertions(+) create mode 100644 ext/mysqli/tests/mysqlnd_stmt_row_no_null_bitmap.phpt diff --git a/ext/mysqli/tests/fake_server.inc b/ext/mysqli/tests/fake_server.inc index 2a4d17f946e8..26bcf7d346ee 100644 --- a/ext/mysqli/tests/fake_server.inc +++ b/ext/mysqli/tests/fake_server.inc @@ -805,6 +805,30 @@ function my_mysqli_test_stmt_response_row_short_length( $conn->read(65536); } +function my_mysqli_test_stmt_response_row_no_null_bitmap(my_mysqli_fake_server_conn $conn): void +{ + $rh = $conn->packet_generator->server_stmt_execute_data_response('intval'); + + // The row packet ends right after the status byte, before the NULL bitmap + $row = new my_mysqli_fake_packet(); + $row->packet_length = '010000'; + $row->packet_number = '05'; + $row->packet_type = '00'; + $rh[4] = $row; + + $conn->send_server_greetings(); + $conn->read(); + $conn->send_server_ok(); + $conn->read(); + $conn->send_server_stmt_prepare_data_response('intval'); + $conn->read(); + $conn->send( + $conn->packets_to_bytes($rh), + "Malicious Stmt Response for data intval [no NULL bitmap]" + ); + $conn->read(65536); +} + function my_mysqli_test_stmt_response_row_short_length_date(my_mysqli_fake_server_conn $conn): void { my_mysqli_test_stmt_response_row_short_length($conn, 'datval'); diff --git a/ext/mysqli/tests/mysqlnd_stmt_row_no_null_bitmap.phpt b/ext/mysqli/tests/mysqlnd_stmt_row_no_null_bitmap.phpt new file mode 100644 index 000000000000..21ed51887f1c --- /dev/null +++ b/ext/mysqli/tests/mysqlnd_stmt_row_no_null_bitmap.phpt @@ -0,0 +1,45 @@ +--TEST-- +mysqlnd rejects a binary protocol row packet too short for its NULL bitmap +--EXTENSIONS-- +mysqli +--FILE-- +wait(); + +$conn = new mysqli($servername, $username, $password, "", $process->getPort()); + +echo "[*] Prepare and execute on the fake server...\n"; +$stmt = $conn->prepare("SELECT strval, intval FROM data"); +$stmt->execute(); +$result = $stmt->get_result(); +while ($row = $result->fetch_row()) { + var_dump($row); +} +$stmt->close(); +$conn->close(); + +$process->terminate(); + +print "done!"; +?> +--EXPECTF-- +[*] Server started on 127.0.0.1:%d +[*] Connection established +[*] Sending - Server Greeting: 580000000a352e352e352d31302e352e31382d4d6172696144420003000000473e3f6047257c6700fef7080200ff81150000000000000f0000006c6b55463f49335f686c6431006d7973716c5f6e61746976655f70617373776f7264 +[*] Received: 6900000185a21a00000000c0080000000000000000000000000000000000000000000000726f6f7400006d7973716c5f6e61746976655f70617373776f7264002c0c5f636c69656e745f6e616d65076d7973716c6e640c5f7365727665725f686f7374093132372e302e302e31 +[*] Sending - Server OK: 0700000200000002000000 +[*] Prepare and execute on the fake server... +[*] Received: 200000001653454c4543542073747276616c2c20696e7476616c2046524f4d2064617461 +[*] Sending - Stmt prepare data intval: 0c0000010001000000020000000000003200000203646566087068705f74657374046461746104646174610673747276616c0673747276616c0ce000c8000000fd01100000003200000303646566087068705f746573740464617461046461746106696e7476616c06696e7476616c0c3f000b00000003011000000005000004fe00000200 +[*] Received: 0a00000017010000000001000000 +[*] Sending - Malicious Stmt Response for data intval [no NULL bitmap]: 01000001023200000203646566087068705f74657374046461746104646174610673747276616c0673747276616c0ce000c8000000fd01100000003200000303646566087068705f746573740464617461046461746106696e7476616c06696e7476616c0c3f000b00000003011000000005000004fe00002200010000050005000006fe00002200 + +Warning: mysqli_result::fetch_row(): Malformed server packet. No packet space left for the NULL bitmap in %s on line %d +done! diff --git a/ext/mysqlnd/mysqlnd_wireprotocol.c b/ext/mysqlnd/mysqlnd_wireprotocol.c index 65b0bceae591..46b308742df7 100644 --- a/ext/mysqlnd/mysqlnd_wireprotocol.c +++ b/ext/mysqlnd/mysqlnd_wireprotocol.c @@ -1546,6 +1546,12 @@ php_mysqlnd_rowp_read_binary_protocol(MYSQLND_ROW_BUFFER * row_buffer, zval * fi end_field = (start_field = fields) + field_count; + /* The status byte and the NULL bitmap must fit in the packet */ + if (UNEXPECTED(rbs < 1 + (field_count + 9) / 8)) { + php_error_docref(NULL, E_WARNING, "Malformed server packet. No packet space left for the NULL bitmap"); + DBG_RETURN(FAIL); + } + /* skip the first byte, not EODATA_MARKER -> 0x0, status */ p++; null_ptr= p; From b62e41f12c3145c5a4ef2d4f65aecec202abafab Mon Sep 17 00:00:00 2001 From: AMMAAR-IC Date: Sat, 24 Oct 2026 03:30:15 +0530 Subject: [PATCH 4/8] fpm: Encode all request values on the full status page The full status page encoded the request URI and the query string of the processes for the html, xml and json formats since GHSA-7qg2-v9fj-4mwv, but still printed the request method, the user and the script verbatim. The user is the name of the HTTP Basic credentials sent by the client, so any client could inject markup into the page an administrator views, or break the JSON output. Encode these values the same way, through a helper shared by all of them. --- sapi/fpm/fpm/fpm_status.c | 113 ++++++++++-------- .../status-full-encode-request-values.phpt | 64 ++++++++++ 2 files changed, 129 insertions(+), 48 deletions(-) create mode 100644 sapi/fpm/tests/status-full-encode-request-values.phpt diff --git a/sapi/fpm/fpm/fpm_status.c b/sapi/fpm/fpm/fpm_status.c index 4e92198b757d..533550d4b0dd 100644 --- a/sapi/fpm/fpm/fpm_status.c +++ b/sapi/fpm/fpm/fpm_status.c @@ -122,6 +122,33 @@ int fpm_status_export_to_zval(zval *status) } /* }}} */ +/* Encodes a request value for the output format. The encoded string, if any, + * is stored in tmp and must be freed by the caller. */ +static char *fpm_status_encode_value(char *value, bool encode_html, bool encode_json, zend_string **tmp) +{ + char *encoded; + + *tmp = NULL; + if (encode_html) { + *tmp = php_escape_html_entities_ex((const unsigned char *) value, strlen(value), 1, + ENT_DISALLOWED | ENT_HTML_DOC_XML1 | ENT_COMPAT, NULL, /* double_encode */ 1, /* quiet */ 0); + return ZSTR_VAL(*tmp); + } + + if (encode_json) { + *tmp = php_json_encode_string(value, strlen(value), PHP_JSON_INVALID_UTF8_IGNORE); + encoded = ZSTR_VAL(*tmp); + /* remove quotes around the string */ + if (ZSTR_LEN(*tmp) >= 2) { + encoded[ZSTR_LEN(*tmp) - 1] = '\0'; + ++encoded; + } + return encoded; + } + + return value; +} + int fpm_status_handle_request(void) /* {{{ */ { struct fpm_scoreboard_s *scoreboard_p; @@ -522,8 +549,10 @@ int fpm_status_handle_request(void) /* {{{ */ if (full_syntax) { unsigned int i; int first; - zend_string *tmp_query_string, *tmp_request_uri_string; - char *query_string, *request_uri_string; + zend_string *tmp_request_method_string, *tmp_request_uri_string, *tmp_query_string; + zend_string *tmp_auth_user_string, *tmp_script_filename_string; + char *request_method_string, *request_uri_string, *query_string; + char *auth_user_string, *script_filename_string; struct timeval duration, now; float cpu; @@ -548,51 +577,30 @@ int fpm_status_handle_request(void) /* {{{ */ } } - request_uri_string = NULL; - tmp_request_uri_string = NULL; + /* All of these come from the client */ + request_method_string = request_uri_string = auth_user_string = script_filename_string = "-"; + query_string = NULL; + tmp_request_method_string = tmp_request_uri_string = tmp_query_string = NULL; + tmp_auth_user_string = tmp_script_filename_string = NULL; + if (proc->request_method[0] != '\0') { + request_method_string = fpm_status_encode_value(proc->request_method, + encode_html, encode_json, &tmp_request_method_string); + } if (proc->request_uri[0] != '\0') { - if (encode_html) { - tmp_request_uri_string = php_escape_html_entities_ex( - (const unsigned char *) proc->request_uri, - strlen(proc->request_uri), 1, ENT_DISALLOWED | ENT_HTML_DOC_XML1 | ENT_COMPAT, - NULL, /* double_encode */ 1, /* quiet */ 0); - request_uri_string = ZSTR_VAL(tmp_request_uri_string); - } else if (encode_json) { - tmp_request_uri_string = php_json_encode_string(proc->request_uri, - strlen(proc->request_uri), PHP_JSON_INVALID_UTF8_IGNORE); - request_uri_string = ZSTR_VAL(tmp_request_uri_string); - /* remove quotes around the string */ - if (ZSTR_LEN(tmp_request_uri_string) >= 2) { - request_uri_string[ZSTR_LEN(tmp_request_uri_string) - 1] = '\0'; - ++request_uri_string; - } - } else { - request_uri_string = proc->request_uri; - } + request_uri_string = fpm_status_encode_value(proc->request_uri, + encode_html, encode_json, &tmp_request_uri_string); } - - query_string = NULL; - tmp_query_string = NULL; if (proc->query_string[0] != '\0') { - if (encode_html) { - tmp_query_string = php_escape_html_entities_ex( - (const unsigned char *) proc->query_string, - strlen(proc->query_string), 1, ENT_DISALLOWED | ENT_HTML_DOC_XML1 | ENT_COMPAT, - NULL, /* double_encode */ 1, /* quiet */ 0); - } else if (encode_json) { - tmp_query_string = php_json_encode_string(proc->query_string, - strlen(proc->query_string), PHP_JSON_INVALID_UTF8_IGNORE); - } else { - query_string = proc->query_string; - } - if (tmp_query_string) { - query_string = ZSTR_VAL(tmp_query_string); - /* remove quotes around the string */ - if (encode_json && ZSTR_LEN(tmp_query_string) >= 2) { - query_string[ZSTR_LEN(tmp_query_string) - 1] = '\0'; - ++query_string; - } - } + query_string = fpm_status_encode_value(proc->query_string, + encode_html, encode_json, &tmp_query_string); + } + if (proc->auth_user[0] != '\0') { + auth_user_string = fpm_status_encode_value(proc->auth_user, + encode_html, encode_json, &tmp_auth_user_string); + } + if (proc->script_filename[0] != '\0') { + script_filename_string = fpm_status_encode_value(proc->script_filename, + encode_html, encode_json, &tmp_script_filename_string); } /* prevent NaN */ @@ -615,24 +623,33 @@ int fpm_status_handle_request(void) /* {{{ */ (unsigned long) (now_epoch - proc->start_epoch), proc->requests, (unsigned long) (duration.tv_sec * 1000000UL + duration.tv_usec), - proc->request_method[0] != '\0' ? proc->request_method : "-", - request_uri_string ? request_uri_string : "-", + request_method_string, + request_uri_string, query_string ? "?" : "", query_string ? query_string : "", proc->content_length, - proc->auth_user[0] != '\0' ? proc->auth_user : "-", - proc->script_filename[0] != '\0' ? proc->script_filename : "-", + auth_user_string, + script_filename_string, proc->request_stage == FPM_REQUEST_ACCEPTING ? cpu : 0., proc->request_stage == FPM_REQUEST_ACCEPTING ? proc->memory : 0); PUTS(buffer); efree(buffer); + if (tmp_request_method_string) { + zend_string_free(tmp_request_method_string); + } if (tmp_request_uri_string) { zend_string_free(tmp_request_uri_string); } if (tmp_query_string) { zend_string_free(tmp_query_string); } + if (tmp_auth_user_string) { + zend_string_free(tmp_auth_user_string); + } + if (tmp_script_filename_string) { + zend_string_free(tmp_script_filename_string); + } } if (full_post) { diff --git a/sapi/fpm/tests/status-full-encode-request-values.phpt b/sapi/fpm/tests/status-full-encode-request-values.phpt new file mode 100644 index 000000000000..ce7e0257027f --- /dev/null +++ b/sapi/fpm/tests/status-full-encode-request-values.phpt @@ -0,0 +1,64 @@ +--TEST-- +FPM: status encodes the request method, user and script of the processes +--SKIPIF-- + +--FILE-- +start(); +$tester->expectLogStartNotices(); +foreach (['html' => 'text/html', 'xml' => 'text/xml', 'json' => 'application/json'] as $format => $type) { + $responses = $tester + ->multiRequest([ + ['headers' => [ + 'REQUEST_METHOD' => 'GET', + 'HTTP_AUTHORIZATION' => 'Basic ' . base64_encode('":pass'), + ]], + ['uri' => '/status', 'query' => "full&$format", 'delay' => 100000], + ]); + $body = $responses[1]->getBody($type); + echo $format, ": "; + if ($format === 'json') { + $processes = json_decode($body, true)['processes']; + var_dump(in_array('GET"', array_column($processes, 'user'), true)); + } else { + var_dump(strpos($body, '