Summary
anyquery's server-mode sandbox (added to fix CVE-2026-50006/CVE-2026-47253) gates dangerous SQL functions using a fail-open denylist on the SQLite SQLITE_FUNCTION authorizer branch. When the server is started with --dev, three developer UDFs (load_dev_plugin, reload_dev_plugin, unload_dev_plugin) are registered on every connection but are absent from that denylist, and --dev does not disable the sandbox. An unauthenticated MySQL client (default auth is NewAuthServerNone) can therefore call load_dev_plugin('x', '<any path>'), whose implementation performs os.ReadFile on the raw path with no AllowedDirs/CheckFileRead guard — bypassing the file-confinement the sandbox exists to provide.
Root Cause
The SQLITE_FUNCTION authorizer branch (namespace/namespace.go:466-474) denies only if deniedSandboxFunctions[lower(arg2)] is set, otherwise returns SQLITE_OK (fail-open). deniedSandboxFunctions (namespace.go:32-38) = {load_file, load_file_bytes, clear_plugin_cache, clear_file_cache, load_extension}. The dev UDFs are registered when n.devMode is true (namespace.go:366-379), independently of the sandbox restrictions, and are not in the denylist. LoadDevPlugin does os.ReadFile(pluginManifestPath) (developer.go:67) on the raw attacker argument with no path restriction (unlike read_* modules and load_file).
Impact
GUARANTEED (no staging): an unauthenticated network client obtains a whole-filesystem existence/readability oracle via differential error returns (developer.go:69/75/79: "error reading manifest" vs "error reading json manifest" vs "error validating manifest"), plus partial file-content disclosure — hjson parse errors echo a ~20-char content window of the target file. CONDITIONAL escalation (requires attacker-staged schema-valid manifest on disk): exec.Command(build_command).CombinedOutput() (developer.go:98) → RCE, and os.OpenFile(log_file, O_CREATE|O_WRONLY|O_APPEND) (developer.go:107) → arbitrary file create/append outside AllowedDirs.
Proof of Concept
# victim (developer server exposed to network; sandbox still ON):
anyquery server --dev --host 0.0.0.0 --port 8070
# attacker (no credentials — NewAuthServerNone default):
mysql -h VICTIM -P 8070 main
SELECT load_dev_plugin('p','/etc/shadow'); -- "error validating manifest" => exists + readable
SELECT load_dev_plugin('p','/root/.ssh/id_rsa'); -- "permission denied" => exists, unreadable
SELECT load_dev_plugin('p','/nonexistent'); -- "no such file or directory" => absent
# differential responses = unauthenticated filesystem existence/readability oracle (C:L)
Attack Chain
- Entry: MySQL protocol, unauthenticated. Guard: MySQL authentication. Bypass proof:
namespace/mysql.go:123-126 — if s.AuthFile=="" && s.Users==nil { authServer = mysql.NewAuthServerNone() }; default anyquery server sets neither → no auth (same premise as CVE-2026-50006/CVE-2026-47253).
- Rewriter:
SELECT load_dev_plugin('probe','/path'). Guard: MySQL query rewriter could strip/neutralize the unknown function. Bypass proof: namespace/mysql_rewriter.go:592-624 — load_dev_plugin is absent from both remapper maps; default is "we don't know the function, we don't rewrite it and we let SQLite handle it." Call reaches SQLite intact.
- Authorizer: SQLite invokes the sandbox authorizer for the function. Guard:
SQLITE_FUNCTION authorizer + per-function file check. Bypass proof: namespace/namespace.go:466-474 — denies only if deniedSandboxFunctions[lower(arg2)], otherwise return SQLITE_OK. load_dev_plugin is absent from the 5-entry denylist → SQLITE_OK (fail-open). The dev UDF is still registered because registration (namespace.go:366-379) is gated on n.devMode and the authorizer registration (namespace.go:458) on n.restrictions != nil — independent, both true under server --dev.
- Sink:
LoadDevPlugin executes. Guard: AllowedDirs/CheckFileRead/symlink-resolution confinement (applied by read_* modules and load_file). Bypass proof: namespace/developer.go:67 — os.ReadFile(pluginManifestPath) on the raw attacker arg with no restrictions reference.
- Impact (guaranteed): unauthenticated filesystem oracle + partial file-content disclosure.
- Impact (conditional, staged manifest):
developer.go:98 exec.Command(...).CombinedOutput() → RCE; developer.go:107 os.OpenFile(..., O_CREATE|O_WRONLY|O_APPEND) → arbitrary file create/append.
Bypass Evidence
- Denylist = {load_file, load_file_bytes, clear_plugin_cache, clear_file_cache, load_extension}; dev UDFs not among them → fail-open
SQLITE_OK (verified on 0.4.6 and HEAD).
load_dev_plugin absent from both rewriter remapper maps (grep exit 1) → passes through to SQLite.
os.ReadFile in developer.go has no CheckFileRead/AllowedDirs (grep of developer.go shows only os.ReadFile).
- Empirically verified differential errors; hjson v4.5.0 echoes a ~20-char content window on trailing-character parse errors.
- NOT the "operator-disabled-control" FP:
--dev help text ("Run the program in developer mode") carries no security warning and does NOT disable the sandbox (contrast --no-sandbox, documented UNSAFE); docs recommend anyquery server --dev. The sandbox stays ON and is logged as enabled, so this is a genuine trust-boundary crossing.
Affected Versions
>= 0.4.5, <= 0.4.6 — the sandbox (the control being bypassed) was introduced in 0.4.5 via commit 27f84fc; the dev-UDF denylist omission is present on 0.4.6 and unfixed on HEAD/main.
Suggested Fix
Register the dev UDFs only when n.restrictions == nil, OR add load_dev_plugin/reload_dev_plugin/unload_dev_plugin to deniedSandboxFunctions, OR (most robust) invert the SQLITE_FUNCTION branch to a fail-closed allowlist mirroring the PRAGMA gate.
Dedup
Not covered by any existing advisory — none of the 7 published anyquery advisories (file-URI sandbox bypass, clear_plugin_cache traversal, AppleScript injection, LFR/SSRF/AFW via read modules in server mode, unauth HTTP API v0.4.3) mention --dev, developer mode, or load_dev_plugin. The sandbox fix commit (27f84fc) itself introduced the fail-open gap. This is framed as a distinct denylist oversight, not a "CVE fix bypass".
Reported by zx (Jace) — GitHub: @manus-use
Summary
anyquery's server-mode sandbox (added to fix CVE-2026-50006/CVE-2026-47253) gates dangerous SQL functions using a fail-open denylist on the SQLite
SQLITE_FUNCTIONauthorizer branch. When the server is started with--dev, three developer UDFs (load_dev_plugin,reload_dev_plugin,unload_dev_plugin) are registered on every connection but are absent from that denylist, and--devdoes not disable the sandbox. An unauthenticated MySQL client (default auth isNewAuthServerNone) can therefore callload_dev_plugin('x', '<any path>'), whose implementation performsos.ReadFileon the raw path with noAllowedDirs/CheckFileReadguard — bypassing the file-confinement the sandbox exists to provide.Root Cause
The
SQLITE_FUNCTIONauthorizer branch (namespace/namespace.go:466-474) denies only ifdeniedSandboxFunctions[lower(arg2)]is set, otherwise returnsSQLITE_OK(fail-open).deniedSandboxFunctions(namespace.go:32-38) = {load_file, load_file_bytes, clear_plugin_cache, clear_file_cache, load_extension}. The dev UDFs are registered whenn.devModeis true (namespace.go:366-379), independently of the sandbox restrictions, and are not in the denylist.LoadDevPlugindoesos.ReadFile(pluginManifestPath)(developer.go:67) on the raw attacker argument with no path restriction (unlikeread_*modules andload_file).Impact
GUARANTEED (no staging): an unauthenticated network client obtains a whole-filesystem existence/readability oracle via differential error returns (
developer.go:69/75/79: "error reading manifest" vs "error reading json manifest" vs "error validating manifest"), plus partial file-content disclosure — hjson parse errors echo a ~20-char content window of the target file. CONDITIONAL escalation (requires attacker-staged schema-valid manifest on disk):exec.Command(build_command).CombinedOutput()(developer.go:98) → RCE, andos.OpenFile(log_file, O_CREATE|O_WRONLY|O_APPEND)(developer.go:107) → arbitrary file create/append outside AllowedDirs.Proof of Concept
Attack Chain
namespace/mysql.go:123-126—if s.AuthFile=="" && s.Users==nil { authServer = mysql.NewAuthServerNone() }; defaultanyquery serversets neither → no auth (same premise as CVE-2026-50006/CVE-2026-47253).SELECT load_dev_plugin('probe','/path'). Guard: MySQL query rewriter could strip/neutralize the unknown function. Bypass proof:namespace/mysql_rewriter.go:592-624—load_dev_pluginis absent from both remapper maps; default is "we don't know the function, we don't rewrite it and we let SQLite handle it." Call reaches SQLite intact.SQLITE_FUNCTIONauthorizer + per-function file check. Bypass proof:namespace/namespace.go:466-474— denies only ifdeniedSandboxFunctions[lower(arg2)], otherwisereturn SQLITE_OK.load_dev_pluginis absent from the 5-entry denylist →SQLITE_OK(fail-open). The dev UDF is still registered because registration (namespace.go:366-379) is gated onn.devModeand the authorizer registration (namespace.go:458) onn.restrictions != nil— independent, both true underserver --dev.LoadDevPluginexecutes. Guard:AllowedDirs/CheckFileRead/symlink-resolution confinement (applied byread_*modules andload_file). Bypass proof:namespace/developer.go:67—os.ReadFile(pluginManifestPath)on the raw attacker arg with no restrictions reference.developer.go:98exec.Command(...).CombinedOutput()→ RCE;developer.go:107os.OpenFile(..., O_CREATE|O_WRONLY|O_APPEND)→ arbitrary file create/append.Bypass Evidence
SQLITE_OK(verified on 0.4.6 and HEAD).load_dev_pluginabsent from both rewriter remapper maps (grep exit 1) → passes through to SQLite.os.ReadFileindeveloper.gohas noCheckFileRead/AllowedDirs(grep of developer.go shows onlyos.ReadFile).--devhelp text ("Run the program in developer mode") carries no security warning and does NOT disable the sandbox (contrast--no-sandbox, documented UNSAFE); docs recommendanyquery server --dev. The sandbox stays ON and is logged as enabled, so this is a genuine trust-boundary crossing.Affected Versions
>= 0.4.5, <= 0.4.6— the sandbox (the control being bypassed) was introduced in 0.4.5 via commit 27f84fc; the dev-UDF denylist omission is present on 0.4.6 and unfixed on HEAD/main.Suggested Fix
Register the dev UDFs only when
n.restrictions == nil, OR addload_dev_plugin/reload_dev_plugin/unload_dev_plugintodeniedSandboxFunctions, OR (most robust) invert theSQLITE_FUNCTIONbranch to a fail-closed allowlist mirroring the PRAGMA gate.Dedup
Not covered by any existing advisory — none of the 7 published anyquery advisories (file-URI sandbox bypass, clear_plugin_cache traversal, AppleScript injection, LFR/SSRF/AFW via read modules in server mode, unauth HTTP API v0.4.3) mention
--dev, developer mode, orload_dev_plugin. The sandbox fix commit (27f84fc) itself introduced the fail-open gap. This is framed as a distinct denylist oversight, not a "CVE fix bypass".Reported by zx (Jace) — GitHub: @manus-use