VYPR
Moderate severityNVD Advisory· Published Feb 9, 2026· Updated Feb 10, 2026

FileStore key canonicalization collisions allow response cache mixup/poisoning (ASCII ord + Unicode NFKD)

CVE-2026-25480

Description

Litestar is an Asynchronous Server Gateway Interface (ASGI) framework. Prior to 2.20.0, 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). This vulnerability is fixed in 2.20.0.

AI Insight

LLM-synthesized narrative grounded in this CVE's description and references.

Litestar FileStore uses Unicode NFKD normalization without separators, causing cache-key collisions exploitable by unauthenticated attackers for cache poisoning.

Vulnerability

[1] FileStore in Litestar prior to 2.20.0 normalizes cache keys into filenames using Unicode NFKD normalization followed by ord() substitution without separators. This process fails to distinguish between characters that normalize to the same base, e.g., the Kelvin sign (U+212A) and the regular letter 'K' both map to the same ASCII key. [3] Consequently, distinct cache keys can collide, producing identical filenames on disk.

Attack

Vector

[3] If FileStore is configured as the response-cache backend, an unauthenticated remote attacker can craft requests with specially encoded characters (e.g., using Unicode variants of ASCII letters) that normalize to the same key as a legitimate resource's cache entry. No authentication or special privileges are required; the attacker only needs network access to the application's cache endpoint. [4] The fix, introduced in version 2.20.0, addresses the root cause by improving key normalization to prevent such collisions.

Impact

[1][3] A successful collision causes the server to serve the cached response from an arbitrary URL in place of the intended response, leading to cache poisoning or cache mixup. An attacker could inject malicious content into a victim's response, potentially enabling cross-site scripting (XSS) or information disclosure if the victim views the poisoned cache entry.

Mitigation

[1] The vulnerability is patched in Litestar version 2.20.0 [1][4]. Users should upgrade immediately and ensure FileStore is not used as a cache backend with versions prior to 2.20.0. No workaround is documented; upgrading is the sole remediation.

AI Insight generated on May 19, 2026. Synthesized from this CVE's description and the cited reference URLs; citations are validated against the source bundle.

Affected packages

Versions sourced from the GitHub Security Advisory.

PackageAffected versionsPatched versions
litestarPyPI
>= 2.19.0, < 2.20.02.20.0

Affected products

2

Patches

1
85db6183a76f

fix(stores): Eliminate possibility of file name collisions during key normalisation

https://github.com/litestar-org/litestarJanek NouvertnéFeb 8, 2026via ghsa
2 files changed · +50 6
  • litestar/stores/file.py+12 4 modified
    @@ -1,8 +1,8 @@
     from __future__ import annotations
     
    +import hashlib
     import os
     import shutil
    -import unicodedata
     from tempfile import mkstemp
     from typing import TYPE_CHECKING
     
    @@ -21,12 +21,20 @@
     
     
     def _safe_file_name(name: str) -> str:
    -    name = unicodedata.normalize("NFKD", name)
    -    return "".join(c if c.isalnum() else str(ord(c)) for c in name)
    +    return hashlib.blake2s(name.encode()).hexdigest()
     
     
     class FileStore(NamespacedStore):
    -    """File based, thread and process safe, asynchronous key/value store."""
    +    """File based, thread and process safe, asynchronous key/value store.
    +
    +    .. note::
    +
    +        To ensure arbitrary keys can safely be stored on any file system without
    +        potentially causing issues due to path separators or collisions, they are
    +        hashed with BLAKE2 before being interpreted as a file path. This means that the
    +        cache key becomes opaque inside the store, and a key does not translate to a
    +        file with that name on the file system.
    +    """
     
         __slots__ = {"create_directories": "flag to create directories in path", "path": "file path"}
     
    
  • tests/unit/test_stores.py+38 2 modified
    @@ -369,7 +369,41 @@ async def test_file_init_subdirectory_negative(file_store_create_directories_fla
     async def test_file_path(file_store: FileStore) -> None:
         await file_store.set("foo", b"bar")
     
    -    assert await (file_store.path / "foo").exists()
    +    assert await (file_store.path / file_store._path_from_key("foo")).exists()
    +
    +
    +@pytest.mark.parametrize(
    +    "key_one, key_two",
    +    [
    +        ("foo/bar", "foobar"),  # os path separator
    +        ("foo\\bar", "foobar"),  # os path separator
    +        ("foo/../bar", "foobar"),  # directory traversal
    +        ("fook-", "fook45"),  # char encoding
    +        ("fooK", "fooK"),  # unicode confusable (Kelvin symbol vs K) # noqa: RUF001
    +        ("foo\\n", "foo92n"),  # a newline?
    +        ("foo\r\nbar", "foobar"),  # a CRLF
    +        ("foo\u0000bar", "foobar"),  # NUL byte stripping
    +        ("Foo", "foo"),  # lowercasing
    +        ("fooß", "fooss"),  # ß folding (relevant in de_DE)
    +        ("fooß", "fooẞ"),  # lower an uppercase ß
    +        ("café", "cafe\u0301"),  # composed vs decomposed é
    +        ("Å", "A\u030a"),  # ring-above normalization
    +        ("Å", "Å"),  # Angstrom sign vs A-ring
    +        ("fooΟ", "fooO"),  # greek omicron vs latin O  # noqa: RUF001
    +        ("fooа", "fooa"),  # cyrillic a vs Latin a  # noqa: RUF001
    +        ("foo\u00a0bar", "foo bar"),  # non-breaking space vs space
    +        ("foo\tbar", "foo bar"),  # tab normalization
    +        ("foo%2Fbar", "foo/bar"),  # percent-encoded slash
    +        ("foo%2e%2e%2fbar", "foo/../bar"),  # encoded traversal
    +        ("a" * 255 + "X", "a" * 255 + "Y"),  # filesystem length truncation
    +    ],
    +)
    +async def test_file_normalize_key(file_store: FileStore, key_one: str, key_two: str) -> None:
    +    await file_store.set(key_one, b"one")
    +    await file_store.set(key_two, b"two")
    +
    +    assert await file_store.get(key_one) == b"one"
    +    assert await file_store.get(key_two) == b"two"
     
     
     def test_file_with_namespace(file_store: FileStore) -> None:
    @@ -487,7 +521,9 @@ async def test_file_store_handle_rename_fail(file_store: FileStore, mocker: Mock
     
         await file_store.set("foo", "bar")
         mock_unlink.assert_called_once()
    -    assert Path(mock_unlink.call_args_list[0].args[0]).with_suffix("") == file_store.path.joinpath("foo")
    +    assert Path(mock_unlink.call_args_list[0].args[0]).with_suffix("") == file_store.path.joinpath(
    +        file_store._path_from_key("foo")
    +    )
     
     
     @pytest.mark.xdist_group("redis")
    

Vulnerability mechanics

Generated on May 9, 2026. Inputs: CWE entries + fix-commit diffs from this CVE's patches. Citations validated against bundle.

References

6

News mentions

0

No linked articles in our index yet.