QueryProxy

MySQL and MariaDB access control and query approval

MySQL and MariaDB decide which accounts can reach which schemas and tables. They do not decide whether this particular statement, typed by a person at 3 a.m., should run. QueryProxy adds that decision: developers submit SQL, the statement is parsed and checked before it can enter the queue, a DBA approves it, and the result is masked before it is stored. The database password stays in QueryProxy’s vault and is never handed to a developer.

What does GRANT leave open?

GRANT scopes an account to schemas, tables and columns, and it should be the first thing you tighten. It still leaves three gaps that matter in production:

  • An account that may UPDATE a table can run an UPDATE with no WHERE (see how to stop UPDATE and DELETE without WHERE, which also covers MySQL safe updates).
  • An account that may SELECT the users table receives every email address in it, unmasked, on a laptop.
  • Nobody else sees the statement before it runs.

The MySQL 8.4 reference shows how far one privilege can reach. The FILE privilege lets an account read and write files on the server host, through LOAD DATA, SELECT ... INTO OUTFILE and LOAD_FILE(), and secure_file_priv limits where (read on 2026-10-13). Those are the statements QueryProxy refuses outright. What database access control is explains why roles alone do not close these gaps.

What does QueryProxy block on MySQL?

Every submission is parsed before it reaches the approval queue, and a request that breaks a guard is rejected at submission with an explicit message. The SQL guards reference lists the rules per dialect. The ones that matter most here:

  • UPDATE and DELETE without a WHERE, or with one that is always true (WHERE 1 = 1), are rejected. A SELECT with no LIMIT gets LIMIT 1000 injected, and a limit above 10000 is clamped.
  • SELECT ... INTO OUTFILE, INTO DUMPFILE, LOAD DATA and LOAD_FILE() are blocked. SELECT ... INTO @var is a plain read.
  • CREATE FUNCTION ... SONAME, which loads a UDF from a shared library, is blocked, as are CREATE USER, ALTER USER, DROP USER and SHUTDOWN.
  • SET GLOBAL, SET PERSIST and SET PERSIST_ONLY on guarded settings such as general_log, plugin_dir, secure_file_priv and local_infile are refused. A SESSION write to these settings is not guarded.
  • get_lock, release_lock, sleep and benchmark are classified as writes, so they go through approval instead of passing as reads.
  • PREPARE s FROM '<DML without WHERE>' is blocked, and PREPARE s FROM @q is blocked because the text sits in a variable.
  • SET sql_mode in any scope is refused.
DELETE FROM orders;                           -- rejected at submission
DELETE FROM orders WHERE id = 42;             -- allowed, still needs a DBA
SELECT * FROM users INTO OUTFILE '/tmp/u';    -- rejected at submission

The table names are examples; the verdicts follow the documented rules.

Why does sql_mode matter?

The guard reads quotes and backslashes the default way. If a session’s sql_mode contains NO_BACKSLASH_ESCAPES or ANSI_QUOTES, the server would read the same text differently, and a string the guard saw as harmless could be code. So before QueryProxy runs SQL on a mysql or mariadb connection it reads @@SESSION.sql_mode, and if either flag is present the request fails and nothing runs, with a message naming the flag. QueryProxy sets a strict sql_mode when it connects; if the check still fires, look for something that changes the mode after connect, such as a proxy in between. The configuration guide describes this.

What is different on MariaDB?

Register a MariaDB server with the mariadb driver, not the mysql driver. The SQL guards reference gives the reason: the driver decides how executable comments are read.

  • Executable comments. /*M!100000 ... */ is code on MariaDB. On the mariadb driver the guard reads the body as SQL; on the mysql driver it is a plain comment. When the target is unknown, /*M! is rejected outright.

Two MariaDB-specific statement forms are checked on both drivers, so they are not a reason to pick one:

  • ANALYZE statements. MariaDB’s ANALYZE UPDATE ... is classified as a write and ANALYZE DELETE FROM t without a WHERE is blocked, while ANALYZE SELECT ... is judged as the SELECT inside it. ANALYZE DROP TABLE, ANALYZE TRUNCATE and ANALYZE SET are blocked.
  • SET STATEMENT ... FOR <statement>. The statement after FOR is judged by its own rules. Without one, the request is blocked.

Everything else in the previous section applies to MariaDB as well.

How do you connect a MySQL or MariaDB database?

A DBA registers the database under Connections with the mysql or mariadb driver. The default port is 3306 (DbDriver.php, v0.2.5). The default Docker image ships pdo_mysql, so no image change is needed. Credentials are encrypted at rest with AES-256, and a developer sees a connection only after a DBA grants it.

For read-only teams, register a separate connection with a database user that has SELECT privileges only, and point it at a replica where you can. The production hardening guide recommends this.

What happens to the result?

Rows are masked while they stream from the cursor to the result file, so an unmasked copy never exists on disk. Masking rules match a column name (such as *email*) or the content of a value, and a value that no rule matches comes back unchanged. Dynamic data masking explains the idea, and the data masking documentation lists the rule types and strategies. Error messages go through masking too, because messages such as Duplicate entry '...' can echo a value. Every step, from submission to download, is written to an immutable audit log.

What are the limits on MySQL and MariaDB?

  • The denylist is incomplete. It names dangerous syntax known today, and a spelling it does not name passes. The SQL guards reference says so itself: what authorizes a statement is the DBA approval, not the guard.
  • Not yet run against live servers. The reference states that the MySQL and MariaDB behaviour is verified against the guard’s unit tests, and that these dialects have not been run against live servers.
  • No read-only transaction or statement timeout on the database side yet. Both are listed as planned for v0.3.0. Until then, use a SELECT-only database user for read connections.
  • Code stored on the server is not covered. A stored procedure or function that already exists in the database runs what it contains.
  • Masking is rule-based. A value that no rule matches is returned as it is, so QueryProxy does not claim to mask all personal data.

When do you not need QueryProxy?

If one person owns the database and is the only one who connects, there is no second person to approve anything, and GRANT plus a read-only account is enough. The same holds for a development database that holds no real data. If your problem is only that applications keep passwords in config files, a secrets manager or IAM database authentication solves that without an approval queue. QueryProxy earns its place when people need ad-hoc access to data that matters, and you want a second pair of eyes and a record of what ran.

Read the approval workflow for how a request moves, or compare QueryProxy with a bastion host. PostgreSQL teams can start from PostgreSQL access control.

Frequently asked

How do I give developers access to a production MySQL database without sharing the password?
Register the database once in QueryProxy, where the credential is stored AES-256 encrypted, and grant developers the connection instead of the password. They submit SQL, a DBA approves it, and a worker runs it with the stored credential. Developers never see the password.
Does QueryProxy support MariaDB as well as MySQL?
Yes. Register a MariaDB server with the mariadb driver and a MySQL server with the mysql driver. The drivers differ in how the guard reads executable comments (/*M!100000 ... */), and the guard checks MariaDB statements such as ANALYZE UPDATE and SET STATEMENT ... FOR on both drivers.
Can a developer export a table to a file through QueryProxy?
Not with SQL the guard recognizes. SELECT ... INTO OUTFILE, INTO DUMPFILE, LOAD DATA and LOAD_FILE() are refused at submission. Results come back through the portal, masked, as a viewer or a CSV download.
Why does a request fail with a sql_mode message?
Before it runs SQL on a mysql or mariadb connection, QueryProxy reads the session sql_mode. If it contains NO_BACKSLASH_ESCAPES or ANSI_QUOTES, the guard would read quotes differently from the server, so the request fails and nothing runs.
Is a database-side read-only transaction available for MySQL and MariaDB connections?
Not yet. A database-side read-only transaction and statement timeout are listed as not covered and planned for v0.3.0. Until then, give read connections a database user with SELECT privileges only.

Arrow keys to move, Enter to open.