Resumen
El fallo estaba en QueryProtocol::handleStateChange (src/protocol/capi/QueryProtocol.cpp), la función de MariaDB Connector/C++ que procesa los cambios de estado de sesión que el servidor anuncia dentro de un paquete OK.
Cuando el servidor marca en sus flags de estado SERVER_SESSION_STATE_CHANGED_, el conector llama a esa función (línea 1512):
if ((serverStatus & ServerStatus::SERVER_SESSION_STATE_CHANGED_)!=0) {
handleStateChange(results);
}
Dentro, para las entradas de tipo SESSION_TRACK_SYSTEM_VARIABLES, el código hacía esto (líneas 1520-1540):
if (mysql_session_track_get_first(connection, static_cast<enum capi::enum_session_state_type>(type), &value, &len) == 0)
{
std::string str(value, len);
switch (type) {
case StateChange::SESSION_TRACK_SYSTEM_VARIABLES:
if (str.compare("auto_increment_increment") == 0)
{
autoIncrementIncrement= std::stoi(str);
El problema es de lógica pura: en este tipo de entrada el protocolo envía primero el nombre de la variable y después su valor. mysql_session_track_get_first devuelve solo el nombre; el valor habría que pedirlo con mysql_session_track_get_next, y esa llamada no existía. Así que el código comprueba que str vale exactamente el texto "auto_increment_increment" y, acto seguido, intenta convertir ese mismo texto en un número. Entrar en esa rama significa, siempre y sin excepción, que std::stoi lanza std::invalid_argument.
Lo que convierte un error de parseo en la caída del proceso es la jerarquía de excepciones. La excepción del conector está definida así (include/conncpp/Exception.hpp:32):
class SQLException : public ::std::runtime_error
std::invalid_argument hereda de std::logic_error, que es una rama hermana de std::runtime_error, no una hija. Ni los manejadores internos del conector (catch(SQLException&) y catch(std::runtime_error&) en la ruta de ejecución) ni una aplicación que capture sql::SQLException, como indica la documentación, la detienen. La excepción sube hasta salir de main y el proceso termina.
No hace falta ninguna opción especial en el cliente: libmariadb anuncia el soporte de session tracking en su conjunto de capacidades por defecto. Además, el investigador señala que un servidor legítimo también provocaría el fallo si un administrador añade auto_increment_increment a session_track_system_variables.
En HackerOne figura como consumo de recursos no controlado, con severidad media, y está marcado como resuelto.
Pasos de reproducción
- Levantar un servidor falso que haga un handshake normal anunciando
CLIENT_SESSION_TRACKy que responda a cualquier sentencia con un paquete OK cuyos flags incluyanSERVER_SESSION_STATE_CHANGEDy cuyo bloquesession_state_changeslleve una entradaSESSION_TRACK_SYSTEM_VARIABLESparaauto_increment_increment. En la prueba del investigador:
python3 poc/rogue_server.py --port 13742 --session-track
- Conectar a ese servidor con un cliente basado en Connector/C++ con opciones por defecto, ejecutar una sentencia cualquiera y capturar únicamente
sql::SQLException, tal como pide la API.
- Como control negativo, repetir con el mismo servidor pero sin la carga de session tracking. El cliente termina bien:
-- negative control: identical server, no session-track payload --
[client] completed normally
exit=0
- Activar la carga. El proceso aborta con SIGABRT y genera volcado de memoria:
-- positive: SESSION_TRACK_SYSTEM_VARIABLES=auto_increment_increment --
terminate called after throwing an instance of 'std::invalid_argument'
what(): stoi
exit=134 (signal 6 = SIGABRT)
- Si el cliente captura también
std::exception(algo que la API no exige), se ve el tipo que se escapa:
[client] std::exception (St16invalid_argument): stoi
El investigador adjuntó como pruebas poc/poc_evidence.txt, poc/poc_sessiontrack.cpp y poc/rogue_server.py. El código afectado era la línea 1535 de QueryProtocol.cpp, alcanzada desde la 1512, en el commit c57f17f.
Impacto
Quien controle el servidor de base de datos, o pueda situarse en medio de la conexión (MITM), puede matar el proceso cliente cuando quiera, justo después del handshake y antes de que la aplicación haga nada útil. Bastan dos paquetes.
La aplicación no tiene forma de protegerse si sigue el contrato documentado, porque el tipo lanzado queda fuera de él. En un servicio de larga duración que mantiene un pool de conexiones, el resultado no es una consulta fallida sino la caída completa del proceso. Afecta a cualquier aplicación que se conecte a un servidor que un atacante controle o pueda interceptar, con la configuración por defecto.
Remediación
El reporte no detalla el parche que aplicó MariaDB. El investigador propuso:
- Obtener el valor con
mysql_session_track_get_nextantes de convertirlo. - Convertirlo con una función que devuelva error en lugar de lanzar excepción, o envolver la conversión para que solo pueda escapar
SQLException. - Revisar el resto del conector en busca de llamadas a
std::stoiostd::string::atsobre datos que vienen del servidor, porque comparten la misma vía de escape.
Qué aprender de este caso
- En drivers y clientes de protocolo, busca conversiones que lanzan (
std::stoi,std::stol,std::string::at) aplicadas a datos del servidor: cada una es una posible caída si su excepción no pertenece a la jerarquía que el usuario de la librería sabe capturar. - Desconfía de una comparación con un literal seguida de un parseo de la misma variable: suele delatar que se está leyendo la clave cuando se quería el valor, como pasó aquí con
mysql_session_track_get_firstfrente aget_next. - Si una librería documenta que solo lanza un tipo de excepción, conviene que todos sus puntos de entrada públicos traduzcan cualquier otra excepción a ese tipo; si no, la promesa de la API se rompe con el primer dato inesperado.
- Al auditar un cliente de base de datos, no des por fiable al servidor: los paquetes OK, las capacidades del handshake y los bloques opcionales como el session tracking son superficie de ataque si alguien puede suplantar el servidor o interceptar la conexión.