Vulnerability research

CVE-2026-50006: unauthenticated arbitrary file write in anyquery server mode

CVE-2026-50006 turns SQLite ATTACH DATABASE into an unauthenticated file-write primitive in anyquery server mode. We reproduced it and verified the 0.4.5 fix.

CVE-2026-50006 is an unauthenticated arbitrary file write in anyquery server, published as GHSA-xrcf-6jh3-ggvx on July 14, 2026. The bug sits at an uncomfortable boundary: anyquery exposes a SQLite-backed query engine over the real MySQL wire protocol, then lets certain SQL statements pass straight to the underlying driver. One of those statements is ATTACH DATABASE, and in SQLite that is enough to create a real file on disk.

The fixed version, 0.4.5, was tagged and released on June 9, 2026, five weeks before the CVE was published. This was not an Emphere patch, and we are not claiming to have discovered the bug. What we did was read the vulnerable path, read the fix, and then verify both with a red run, a green run, and a control run that rules out coincidence.

Evidence status

advisoryGHSA-xrcf-6jh3-ggvx, published 2026-07-14
severityCVSS 3.1 9.1, AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:H/A:H
vulnerableanyquery < 0.4.5
fixedanyquery 0.4.5, tagged and released 2026-06-09
reporterGitHub handle Metincloup
preconditionoperator runs server mode with no --auth-file and the listener is network-reachable
01

The vulnerable path

The server handler has a hardcoded prefix table in namespace/mysql_handler.go. Statements matching those prefixes are sent down the ExecContext path rather than being treated as normal queries. In the vulnerable version, ATTACH DATABASE is one of those prefixes, and there is no authorization check between the MySQL client and SQLite.

namespace/mysql_handler.go
1var prefixExec = []string{
2 "CREATE VIRTUAL TABLE",
3 "CREATE TABLE",
4 "CREATE INDEX",
5 "CREATE TRIGGER",
6 "CREATE VIEW",
7 "ATTACH DATABASE",
8 "DETACH DATABASE",
9 ...
10}
11 
12func (h *handler) runSimpleQuery(connectionID uint32, query string, args ...any) (*sqltypes.Result, error) {
13 ...
14 if runWithQuery {
15 rows, err := conn.QueryContext(context.Background(), query, args...)
16 ...
17 } else {
18 res, err := conn.ExecContext(context.Background(), query, args...)
19 ...
20 }
21}

That dispatch table is still present at current HEAD, which matters. The patch does not make the MySQL handler smarter, and it does not rely on every call site remembering to filter dangerous SQL. The fix works lower in the stack, where SQLite can still see the operation after the statement has been parsed.

StepWhat happens
Connectanyquery server accepts the MySQL client when AuthFile and Users are empty, which is the default.
AttachThe client sends ATTACH DATABASE '<path>' AS x, routed to SQLite through ExecContext.
CreateIf <path> does not exist, SQLite creates it as an on-disk database file.
WriteCREATE TABLE x.t(a); INSERT INTO x.t VALUES('...') writes attacker-controlled content into that file.
ReuseTargets such as cron, sshd, or a PHP web root can tolerate the SQLite header before the line they care about.

The advisory PoC uses this exact property against /etc/cron.d/pwn, ~/.ssh/authorized_keys, and a PHP webshell in a web root. There is no second bug in the application layer that turns SQL into code execution. The administrative SQL command is the file-write primitive.

02

The obvious fix is a trap

The easy reaction is to block ATTACH DATABASE outright. That would close the exploit, but it would also break legitimate operator workflows, including in-memory attaches and intentionally allow-listed on-disk attaches. The important distinction is not "ATTACH is bad." It is "untrusted network clients should not be able to attach arbitrary local paths."

Fix shapeSecurity resultEngineering result
Leave the dispatch table aloneFails, unauthenticated file write remains reachableNo compatibility cost
Block every ATTACH DATABASECloses the primitiveBreaks legitimate in-memory and operator-approved attach use cases
Authorize attach paths inside SQLiteCloses arbitrary file write by defaultPreserves harmless and explicitly allowed attach behavior

The 0.4.5 patch takes the third route. It registers a SQLite authorizer after trusted internal setup has already run, then denies on-disk attaches unless server restrictions explicitly allow them and the resolved path is inside an allowlist. That is the right shape for this bug: narrow, path-aware, and default-deny for the network listener.

03

The fix

The fix commit is 27f84fc168310455eaf81ec4ba87eed20298670c, titled "Add sandboxing to remediate CVE-2026-47253 and CVE-2026-50006." The commit message names two CVEs, but the sandbox it adds actually closes several related issues in the same server surface. Assurance independently re-drove only CVE-2026-50006; the others are covered by the maintainer's own passing sandbox tests, not by our exploit reproduction.

namespace/namespace.go
1+ if n.restrictions != nil {
2+ conn.RegisterAuthorizer(func(op int, arg1, arg2, arg3 string) int {
3+ switch op {
4+ case sqlite3.SQLITE_ATTACH:
5+ if n.restrictions.AllowAttachPath(arg1) {
6+ return sqlite3.SQLITE_OK
7+ }
8+ return sqlite3.SQLITE_DENY
9+ case sqlite3.SQLITE_FUNCTION:
10+ if deniedSandboxFunctions[strings.ToLower(arg2)] {
11+ return sqlite3.SQLITE_DENY
12+ }
13+ return sqlite3.SQLITE_OK
14+ case sqlite3.SQLITE_PRAGMA:
15+ if allowedSandboxPragmas[strings.ToLower(arg1)] {
16+ return sqlite3.SQLITE_OK
17+ }
18+ return sqlite3.SQLITE_DENY
19+ default:
20+ return sqlite3.SQLITE_OK
21+ }
22+ })
23+ }

The key behavior is in AllowAttachPath. In-memory databases are allowed, empty paths are rejected, and on-disk attaches are denied unless AllowAttach is set and the final local path passes the directory restriction check. That keeps the useful SQLite feature without leaving a network-exposed file-write primitive behind.

module/restrictions.go
1+func (r *Restrictions) AllowAttachPath(filename string) bool {
2+ if r == nil {
3+ return true // unrestricted
4+ }
5+ filename = strings.TrimSpace(filename)
6+ if filename == "" {
7+ return false
8+ }
9+ if isInMemoryDB(filename) {
10+ return true
11+ }
12+ if !r.AllowAttach {
13+ return false
14+ }
15+ return r.checkLocalPath(attachPathToFile(filename)) == nil
16+}

For anyquery server, sandboxing is on by default and can be disabled with --no-sandbox. For local CLI use, sandboxing is opt-in with --sandbox. That split is the important operational default: a local CLI user is usually the operator, while a MySQL-compatible network listener has to treat its client as untrusted unless authentication and network exposure prove otherwise.

04

How we verified it

Assurance ran the vulnerable and patched targets as real OS processes, connected with a real MySQL wire client, and let real SQLite create real files on disk. No handler was mocked, no database layer was stubbed, and the exploit was not reduced to a unit test around the authorizer. The red run proves exploitability in 0.4.4; the green run proves the default 0.4.5 behavior denies the primitive; the --no-sandbox control proves the authorizer is the difference.

Evidence status

target, vulnerabletag 0.4.4, commit 0abd46087f7085339914c79592cdf44ebd3ce5a1
target, patchedtag 0.4.5, commit 66d5e684cd0a1a4086758aadaf3b22aee4e6e498
fix commit27f84fc168310455eaf81ec4ba87eed20298670c
red run, 0.4.4unauthenticated client: ATTACH DATABASE + CREATE TABLE + INSERT all accepted; 8192-byte real SQLite file written to disk, attacker string read back on a second connection
green run, 0.4.5 default flagsidentical query returns Error 1105 (HY000): not authorized; no file created
control, 0.4.5 --no-sandboxvulnerability reproduces identically, proving the authorizer is the actual fix, not a coincidental change
engineering gatefull 0.4.5 build clean; patch's own test suite passes, including TestMySQLServerSandbox, TestSandboxArbitraryFileWrite, and TestAllowAttachPath; legitimate SHOW TABLES, information_schema, and allowed-dir CSV reads unaffected

We also checked the regression surface against 0.4.4. The full suite had zero new failures in 0.4.5 compared with the vulnerable version. Three failures appeared in both versions and were environmental: a stale remote fixture and a go-sql-driver/go1.24 argument-count quirk, unrelated to the sandbox patch.

05

Scope

The 0.4.5 sandbox closes a cluster of anyquery server issues at once. We took the most severe, the unauthenticated ATTACH DATABASE file write that reaches remote code execution, and proved it end to end on real binaries: a red run, a green run, and a control. The rest of the cluster is closed by the same authorizer and the maintainer's passing sandbox tests: a clear_plugin_cache path traversal that deletes directories (CVE-2026-47253), and local file read and SSRF through the read_* virtual-table modules (CVE-2026-54629, CVE-2026-54628). We scoped the proof to the file-write primitive deliberately, because it is the one that turns a query engine into code execution.

Two points of precision. The real exposure is server mode with no --auth-file reachable by an untrusted client, not the localhost default bind, which predates the fix and is unrelated to it. And run the latest release rather than 0.4.5 specifically: 0.4.6 later closed a separate file-URI sandbox bypass on the read path (repository advisory GHSA-j4wj-p47h-f9cc), a different surface from the ATTACH authorizer verified here.

06

Close

"Upgrade to 0.4.5" is the right operational advice. It is not, on its own, proof that the patch holds, and that proof is what makes the advice worth trusting. For CVE-2026-50006 it reduces to one statement: before 0.4.5, an unauthenticated ATTACH DATABASE could create a file anywhere the process could write; after 0.4.5, the same statement is denied before a file exists.