GHSA-vxqx-rh46-q2pg

Suggest an improvement
Source
https://github.com/advisories/GHSA-vxqx-rh46-q2pg
Import Source
https://github.com/github/advisory-database/blob/main/advisories/github-reviewed/2026/02/GHSA-vxqx-rh46-q2pg/GHSA-vxqx-rh46-q2pg.json
JSON Data
https://api.osv.dev/v1/vulns/GHSA-vxqx-rh46-q2pg
Aliases
Published
2026-02-09T17:19:06Z
Modified
2026-02-22T23:25:49.404374Z
Severity
  • 6.5 (Medium) CVSS_V3 - CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:L/I:L/A:N CVSS Calculator
Summary
Litestar's FileStore key canonicalization collisions allow response cache mixup/poisoning (ASCII ord + Unicode NFKD)
Details

Summary

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.safefile_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.

Database specific
{
    "nvd_published_at": "2026-02-09T20:15:57Z",
    "github_reviewed": true,
    "cwe_ids": [
        "CWE-176",
        "CWE-20"
    ],
    "github_reviewed_at": "2026-02-09T17:19:06Z",
    "severity": "MODERATE"
}
References

Affected packages

PyPI / litestar

Package

Affected ranges

Type
ECOSYSTEM
Events
Introduced
2.19.0
Fixed
2.20.0

Affected versions

2.*
2.19.0

Database specific

source
"https://github.com/github/advisory-database/blob/main/advisories/github-reviewed/2026/02/GHSA-vxqx-rh46-q2pg/GHSA-vxqx-rh46-q2pg.json"