Feb 9, 2026·5 min read·7 visits
Litestar's file-based caching mechanism used a lossy string replacement method to generate filenames from keys. It replaced special characters with their ASCII decimal values (e.g., '-' becomes '45') without separators. This allows attackers to craft URLs that resolve to the same cache file as legitimate URLs, leading to cache poisoning.
A critical flaw in Litestar's FileStore component allows remote attackers to poison the server-side cache by exploiting a naive filename sanitization algorithm. By crafting specific request paths containing non-alphanumeric characters or Unicode anomalies (like the Kelvin sign), an attacker can force a filename collision on the disk, overwriting legitimate cache entries with malicious content.
Caching is one of the two hardest problems in computer science (alongside naming things and off-by-one errors). When you build a web framework like Litestar, you eventually need to persist cached responses to disk. But filesystems are picky. You can't just take a raw URL like https://api.com/users/foo?query=bar and name a file that. The OS will scream at you about slashes, question marks, and length limits.
So, the developers implemented a helper function called _safe_file_name in the FileStore component. Its job was simple: take a dirty cache key and scrub it until it was shiny, alphanumeric, and safe for the filesystem. Ideally, this transformation should be injective—meaning every unique key produces a unique filename. If it doesn't, you get collisions. And in the world of caching, collisions mean you start serving User A's bank balance to User B.
Litestar didn't just have a collision; they had a predictable, calculable, and easily exploitable collision mechanism. It turns out that trying to invent your own encoding scheme using ASCII arithmetic instead of just using a standard hash function is a recipe for disaster. This vulnerability essentially turns the FileStore into a hash map where the attacker controls the bucket collisions.
The vulnerability lies in how _safe_file_name sanitized strings. The logic was twofold. First, it normalized Unicode characters using NFKD form. This is generally good practice to handle things like accents, but it has side effects. For example, the Kelvin sign (K, U+212A) normalizes to the letter K. This means a key containing the Kelvin sign and a key containing a capital 'K' are treated as identical before they even hit the disk.
But the real horror show was the second step. The code iterated through the string and kept alphanumeric characters as-is. If a character wasn't alphanumeric, it replaced it with the string representation of its ASCII ordinal value (ord(c)). Crucially, it did this without delimiters.
Let's do the math. The hyphen character (-) has an ASCII value of 45. If your key is data-set, the function converts - to 45 and concatenates it. The result is data45set. Now, consider a legitimate key named data45set. The function sees that 4 and 5 are alphanumeric, so it keeps them. The result? data45set. Two completely different inputs resolve to the exact same filename. This isn't a rare hash collision; it's a structural guarantee.
Here is the vulnerable implementation found in litestar/stores/file.py prior to version 2.20.0. It's a textbook example of "clever" code going wrong.
# The Vulnerable Implementation
import unicodedata
def _safe_file_name(name: str) -> str:
# Step 1: Normalize (Kelvin sign becomes K)
name = unicodedata.normalize("NFKD", name)
# Step 2: The fatal flaw
# No separators between the original chars and the ord() values
return "".join(c if c.isalnum() else str(ord(c)) for c in name)If we run this code with a few examples, the problem becomes obvious:
# legitimate_key usually maps to a resource ID like 45
key_legit = "user45"
# malicious_key uses a hyphen (ord 45) to mimic the ID
key_malicious = "user-"
print(_safe_file_name(key_legit)) # Output: user45
print(_safe_file_name(key_malicious)) # Output: user45Because the FileStore uses this output directly as the filename to write data to, writing to user- overwrites user45. The system cannot distinguish between the two.
To exploit this, an attacker doesn't need authentication; they just need the target application to use ResponseCacheMiddleware with a FileStore backend. The goal is to poison the cache for a victim URL.
The Scenario:
Imagine an endpoint /api/news/45 that serves a specific news article. The attacker wants to replace this content with their own payload (perhaps a defacement or a malicious redirect).
The Attack Chain:
/api/news/45 is cached. They deduce the ID 45 corresponds to the ASCII character - (hyphen)./api/news/-. This might seem like an invalid ID, but if the application logic simply reflects the input or returns a generic "Not Found" that gets cached, the damage is done./api/news/-. The _safe_file_name function converts - to 45. It writes the response for the invalid request to the file .../api_news_45./api/news/45. Litestar computes the key, looks for .../api_news_45, finds the file created by the attacker, and serves it.If the application caches 404s or error messages, the attacker effectively performs a Denial of Service by permanently caching an error page for a valid resource.
The fix provided by the Litestar team in version 2.20.0 is straightforward and correct: abandon the custom string sanitization logic entirely. Instead of trying to preserve the readability of the filename, they switched to a cryptographic hash.
Hashing guarantees a fixed-length, filesystem-safe string (hex digest) and, for all practical purposes, eliminates collisions. Here is the patched code:
# The Fixed Implementation
import hashlib
def _safe_file_name(name: str) -> str:
# BLAKE2s is fast and secure enough for this purpose
return hashlib.blake2s(name.encode()).hexdigest()This change ensures that user- becomes something like a1b2... and user45 becomes c3d4.... They no longer collide. The only downside is that looking at the cache directory manually is now cryptic, but that is a small price to pay for not serving random users each other's data.
CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:L/I:L/A:N| Product | Affected Versions | Fixed Version |
|---|---|---|
Litestar litestar-org | < 2.20.0 | 2.20.0 |
| Attribute | Detail |
|---|---|
| CWE ID | CWE-178 |
| Attack Vector | Network |
| CVSS Score | 6.5 |
| Impact | Cache Poisoning |
| Exploit Status | Proof of Concept Available |
| KEV Status | Not Listed |
Improper Handling of Case Sensitivity