Fix GHSA-h35g-vwh6-m678: Mysqlnd - various heap buffer over-reads

This fixes issues causing buffer over-read that leak heap content:
- RESP packet field default left over for COM_LIST
- RESP packet upsert filename
- OK packet message
- RESP packet for stmt row data
  - ps_fetch_from_1_to_8_bytes
  - ps_fetch_float
  - ps_fetch_double
  - ps_fetch_time
  - ps_fetch_date
  - ps_fetch_datetime
  - ps_fetch_string
  - ps_fetch_bit
- RESP packet for query row data (just possible overflow on 32bit)

It also adds various protocol tests using a new fake server.
This commit is contained in:
Jakub Zelenka 2024-10-08 16:17:53 +01:00
parent fba659abb9
commit c595455300
No known key found for this signature in database
GPG key ID: 1C0779DC5C0A9DE4
18 changed files with 1792 additions and 21 deletions

View file

@ -721,7 +721,14 @@ php_mysqlnd_auth_response_read(MYSQLND_CONN_DATA * conn, void * _packet)
/* There is a message */
if (packet->header.size > (size_t) (p - buf) && (net_len = php_mysqlnd_net_field_length(&p))) {
packet->message_len = MIN(net_len, buf_len - (p - begin));
/* p can get past packet size when getting field length so it needs to be checked first
* and after that it can be checked that the net_len is not greater than the packet size */
if ((p - buf) > packet->header.size || packet->header.size - (p - buf) < net_len) {
DBG_ERR_FMT("OK packet message length is past the packet size");
php_error_docref(NULL, E_WARNING, "OK packet message length is past the packet size");
DBG_RETURN(FAIL);
}
packet->message_len = net_len;
packet->message = mnd_pestrndup((char *)p, packet->message_len, FALSE);
} else {
packet->message = NULL;
@ -1105,6 +1112,17 @@ php_mysqlnd_rset_header_read(MYSQLND_CONN_DATA * conn, void * _packet)
BAIL_IF_NO_MORE_DATA;
/* Check for additional textual data */
if (packet->header.size > (size_t) (p - buf) && (len = php_mysqlnd_net_field_length(&p))) {
/* p can get past packet size when getting field length so it needs to be checked first
* and after that it can be checked that the len is not greater than the packet size */
if ((p - buf) > packet->header.size || packet->header.size - (p - buf) < len) {
size_t local_file_name_over_read = ((p - buf) - packet->header.size) + len;
DBG_ERR_FMT("RSET_HEADER packet additional data length is past %zu bytes the packet size",
local_file_name_over_read);
php_error_docref(NULL, E_WARNING,
"RSET_HEADER packet additional data length is past %zu bytes the packet size",
local_file_name_over_read);
DBG_RETURN(FAIL);
}
packet->info_or_local_file.s = mnd_emalloc(len + 1);
memcpy(packet->info_or_local_file.s, p, len);
packet->info_or_local_file.s[len] = '\0';
@ -1255,23 +1273,16 @@ php_mysqlnd_rset_field_read(MYSQLND_CONN_DATA * conn, void * _packet)
meta->flags |= NUM_FLAG;
}
/*
def could be empty, thus don't allocate on the root.
NULL_LENGTH (0xFB) comes from COM_FIELD_LIST when the default value is NULL.
Otherwise the string is length encoded.
*/
/* COM_FIELD_LIST is no longer supported so def should not be present */
if (packet->header.size > (size_t) (p - buf) &&
(len = php_mysqlnd_net_field_length(&p)) &&
len != MYSQLND_NULL_LENGTH)
{
BAIL_IF_NO_MORE_DATA;
DBG_INF_FMT("Def found, length " ZEND_ULONG_FMT, len);
meta->def = packet->memory_pool->get_chunk(packet->memory_pool, len + 1);
memcpy(meta->def, p, len);
meta->def[len] = '\0';
meta->def_length = len;
p += len;
DBG_ERR_FMT("Protocol error. Server sent default for unsupported field list");
php_error_docref(NULL, E_WARNING,
"Protocol error. Server sent default for unsupported field list (mysqlnd_wireprotocol.c:%u)",
__LINE__);
DBG_RETURN(FAIL);
}
root_ptr = meta->root = packet->memory_pool->get_chunk(packet->memory_pool, total_len);
@ -1434,8 +1445,10 @@ php_mysqlnd_rowp_read_binary_protocol(MYSQLND_ROW_BUFFER * row_buffer, zval * fi
const unsigned int field_count, const MYSQLND_FIELD * const fields_metadata,
const bool as_int_or_float, MYSQLND_STATS * const stats)
{
unsigned int i;
const zend_uchar * p = row_buffer->ptr;
unsigned int i, j;
size_t rbs = row_buffer->size;
const zend_uchar * rbp = row_buffer->ptr;
const zend_uchar * p = rbp;
const zend_uchar * null_ptr;
zend_uchar bit;
zval *current_field, *end_field, *start_field;
@ -1468,7 +1481,21 @@ php_mysqlnd_rowp_read_binary_protocol(MYSQLND_ROW_BUFFER * row_buffer, zval * fi
statistic = STAT_BINARY_TYPE_FETCHED_NULL;
} else {
enum_mysqlnd_field_types type = fields_metadata[i].type;
mysqlnd_ps_fetch_functions[type].func(current_field, &fields_metadata[i], 0, &p);
size_t row_position = p - rbp;
if (rbs <= row_position) {
for (j = 0, current_field = start_field; j < i; current_field++, j++) {
zval_ptr_dtor(current_field);
}
php_error_docref(NULL, E_WARNING, "Malformed server packet. No packet space left for the field");
DBG_RETURN(FAIL);
}
mysqlnd_ps_fetch_functions[type].func(current_field, &fields_metadata[i], rbs - row_position, &p);
if (p == NULL) {
for (j = 0, current_field = start_field; j < i; current_field++, j++) {
zval_ptr_dtor(current_field);
}
DBG_RETURN(FAIL);
}
if (MYSQLND_G(collect_statistics)) {
switch (fields_metadata[i].type) {
@ -1525,7 +1552,7 @@ php_mysqlnd_rowp_read_text_protocol(MYSQLND_ROW_BUFFER * row_buffer, zval * fiel
unsigned int field_count, const MYSQLND_FIELD * fields_metadata,
bool as_int_or_float, MYSQLND_STATS * stats)
{
unsigned int i;
unsigned int i, j;
zval *current_field, *end_field, *start_field;
zend_uchar * p = row_buffer->ptr;
const size_t data_size = row_buffer->size;
@ -1546,9 +1573,11 @@ php_mysqlnd_rowp_read_text_protocol(MYSQLND_ROW_BUFFER * row_buffer, zval * fiel
/* NULL or NOT NULL, this is the question! */
if (len == MYSQLND_NULL_LENGTH) {
ZVAL_NULL(current_field);
} else if ((p + len) > packet_end) {
php_error_docref(NULL, E_WARNING, "Malformed server packet. Field length pointing %zu"
" bytes after end of packet", (p + len) - packet_end - 1);
} else if (p > packet_end || len > packet_end - p) {
php_error_docref(NULL, E_WARNING, "Malformed server packet. Field length pointing after end of packet");
for (j = 0, current_field = start_field; j < i; current_field++, j++) {
zval_ptr_dtor(current_field);
}
DBG_RETURN(FAIL);
} else {
struct st_mysqlnd_perm_bind perm_bind =