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.
- Abrir una conexión con el driver fijando explícitamente
allowMultiQueriesafalse(que además es el valor por defecto):
sql::Properties props({{"user","root"},{"password","[REDACTADO]"}});
props["allowMultiQueries"] = "false";
- 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
- Llamar a
setSavepointcon un nombre que contenga SQL:
conn->setSavepoint("sp; CREATE TABLE pocdb.canary_b (id INT); -- ");
- Revisar el log general del servidor. La sentencia que envió el driver es exactamente esta (el
0final es el contador que añadetoString, que queda dentro del comentario--):
SAVEPOINT sp; CREATE TABLE pocdb.canary_b (id INT); -- 0
- 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
- Control negativo: con un nombre de savepoint normal, el driver emite un
SAVEPOINTcorriente 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=falseno 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 unSELECT 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
quoteIdentifierdependía de unreplacedefectuoso y no habría protegido aunque se hubiera usado.