mirror of
https://github.com/php/php-src.git
synced 2025-08-15 13:38:49 +02:00
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:
parent
fba659abb9
commit
c595455300
18 changed files with 1792 additions and 21 deletions
|
@ -50,11 +50,46 @@ struct st_mysqlnd_perm_bind mysqlnd_ps_fetch_functions[MYSQL_TYPE_LAST + 1];
|
|||
#define MYSQLND_PS_SKIP_RESULT_W_LEN -1
|
||||
#define MYSQLND_PS_SKIP_RESULT_STR -2
|
||||
|
||||
static inline void ps_fetch_over_read_error(const zend_uchar ** row)
|
||||
{
|
||||
php_error_docref(NULL, E_WARNING, "Malformed server packet. Field length pointing after the end of packet");
|
||||
*row = NULL;
|
||||
}
|
||||
|
||||
static inline bool ps_fetch_is_packet_over_read_with_variable_length(const unsigned int pack_len,
|
||||
const zend_uchar ** row, const zend_uchar *p, unsigned int length)
|
||||
{
|
||||
if (pack_len == 0) {
|
||||
return false;
|
||||
}
|
||||
size_t length_len = *row - p;
|
||||
if (length_len > pack_len || length > pack_len - length_len) {
|
||||
ps_fetch_over_read_error(row);
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
static inline bool ps_fetch_is_packet_over_read_with_static_length(const unsigned int pack_len,
|
||||
const zend_uchar ** row, unsigned int length)
|
||||
{
|
||||
if (pack_len > 0 && length > pack_len) {
|
||||
ps_fetch_over_read_error(row);
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
|
||||
/* {{{ ps_fetch_from_1_to_8_bytes */
|
||||
void
|
||||
ps_fetch_from_1_to_8_bytes(zval * zv, const MYSQLND_FIELD * const field, const unsigned int pack_len,
|
||||
const zend_uchar ** row, unsigned int byte_count)
|
||||
{
|
||||
if (UNEXPECTED(ps_fetch_is_packet_over_read_with_static_length(pack_len, row, byte_count))) {
|
||||
return;
|
||||
}
|
||||
|
||||
bool is_bit = field->type == MYSQL_TYPE_BIT;
|
||||
DBG_ENTER("ps_fetch_from_1_to_8_bytes");
|
||||
DBG_INF_FMT("zv=%p byte_count=%u", zv, byte_count);
|
||||
|
@ -174,6 +209,11 @@ ps_fetch_float(zval * zv, const MYSQLND_FIELD * const field, const unsigned int
|
|||
float fval;
|
||||
double dval;
|
||||
DBG_ENTER("ps_fetch_float");
|
||||
|
||||
if (UNEXPECTED(ps_fetch_is_packet_over_read_with_static_length(pack_len, row, 4))) {
|
||||
return;
|
||||
}
|
||||
|
||||
float4get(fval, *row);
|
||||
(*row)+= 4;
|
||||
DBG_INF_FMT("value=%f", fval);
|
||||
|
@ -196,6 +236,11 @@ ps_fetch_double(zval * zv, const MYSQLND_FIELD * const field, const unsigned int
|
|||
{
|
||||
double value;
|
||||
DBG_ENTER("ps_fetch_double");
|
||||
|
||||
if (UNEXPECTED(ps_fetch_is_packet_over_read_with_static_length(pack_len, row, 8))) {
|
||||
return;
|
||||
}
|
||||
|
||||
float8get(value, *row);
|
||||
ZVAL_DOUBLE(zv, value);
|
||||
(*row)+= 8;
|
||||
|
@ -211,9 +256,14 @@ ps_fetch_time(zval * zv, const MYSQLND_FIELD * const field, const unsigned int p
|
|||
{
|
||||
struct st_mysqlnd_time t;
|
||||
zend_ulong length; /* First byte encodes the length */
|
||||
const zend_uchar *p = *row;
|
||||
DBG_ENTER("ps_fetch_time");
|
||||
|
||||
if ((length = php_mysqlnd_net_field_length(row))) {
|
||||
if (UNEXPECTED(ps_fetch_is_packet_over_read_with_variable_length(pack_len, row, p, length))) {
|
||||
return;
|
||||
}
|
||||
|
||||
const zend_uchar * to = *row;
|
||||
|
||||
t.time_type = MYSQLND_TIMESTAMP_TIME;
|
||||
|
@ -256,9 +306,14 @@ ps_fetch_date(zval * zv, const MYSQLND_FIELD * const field, const unsigned int p
|
|||
{
|
||||
struct st_mysqlnd_time t = {0};
|
||||
zend_ulong length; /* First byte encodes the length*/
|
||||
const zend_uchar *p = *row;
|
||||
DBG_ENTER("ps_fetch_date");
|
||||
|
||||
if ((length = php_mysqlnd_net_field_length(row))) {
|
||||
if (UNEXPECTED(ps_fetch_is_packet_over_read_with_variable_length(pack_len, row, p, length))) {
|
||||
return;
|
||||
}
|
||||
|
||||
const zend_uchar * to = *row;
|
||||
|
||||
t.time_type = MYSQLND_TIMESTAMP_DATE;
|
||||
|
@ -288,9 +343,14 @@ ps_fetch_datetime(zval * zv, const MYSQLND_FIELD * const field, const unsigned i
|
|||
{
|
||||
struct st_mysqlnd_time t;
|
||||
zend_ulong length; /* First byte encodes the length*/
|
||||
const zend_uchar *p = *row;
|
||||
DBG_ENTER("ps_fetch_datetime");
|
||||
|
||||
if ((length = php_mysqlnd_net_field_length(row))) {
|
||||
if (UNEXPECTED(ps_fetch_is_packet_over_read_with_variable_length(pack_len, row, p, length))) {
|
||||
return;
|
||||
}
|
||||
|
||||
const zend_uchar * to = *row;
|
||||
|
||||
t.time_type = MYSQLND_TIMESTAMP_DATETIME;
|
||||
|
@ -332,7 +392,11 @@ ps_fetch_datetime(zval * zv, const MYSQLND_FIELD * const field, const unsigned i
|
|||
static void
|
||||
ps_fetch_string(zval * zv, const MYSQLND_FIELD * const field, const unsigned int pack_len, const zend_uchar ** row)
|
||||
{
|
||||
const zend_uchar *p = *row;
|
||||
const zend_ulong length = php_mysqlnd_net_field_length(row);
|
||||
if (UNEXPECTED(ps_fetch_is_packet_over_read_with_variable_length(pack_len, row, p, length))) {
|
||||
return;
|
||||
}
|
||||
DBG_ENTER("ps_fetch_string");
|
||||
DBG_INF_FMT("len = " ZEND_ULONG_FMT, length);
|
||||
DBG_INF("copying from the row buffer");
|
||||
|
@ -348,7 +412,11 @@ ps_fetch_string(zval * zv, const MYSQLND_FIELD * const field, const unsigned int
|
|||
static void
|
||||
ps_fetch_bit(zval * zv, const MYSQLND_FIELD * const field, const unsigned int pack_len, const zend_uchar ** row)
|
||||
{
|
||||
const zend_uchar *p = *row;
|
||||
const zend_ulong length = php_mysqlnd_net_field_length(row);
|
||||
if (UNEXPECTED(ps_fetch_is_packet_over_read_with_variable_length(pack_len, row, p, length))) {
|
||||
return;
|
||||
}
|
||||
ps_fetch_from_1_to_8_bytes(zv, field, pack_len, row, length);
|
||||
}
|
||||
/* }}} */
|
||||
|
|
|
@ -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 =
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue