CVEReports
CVEReports

Automated vulnerability intelligence platform. Comprehensive reports for high-severity CVEs generated by AI.

Product

  • Home
  • Sitemap
  • RSS Feed

Company

  • About
  • Contact
  • Privacy Policy
  • Terms of Service

© 2026 CVEReports. All rights reserved.

Made with love by Amit Schendel & Alon Barad



CVE-2026-25480
6.5

The Kelvin Collision: Breaking Litestar's Cache with Basic Arithmetic

Alon Barad
Alon Barad
Software Engineer

Feb 9, 2026·5 min read·7 visits

PoC Available

Executive Summary (TL;DR)

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.

The Hook: When "Safe" Isn't Safe

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 Logic Flaw: Arithmetic Without Delimiters

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.

The Code: Anatomy of a Collision

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: user45

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

The Exploit: Cache Poisoning Made Easy

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:

  1. Reconnaissance: The attacker identifies that /api/news/45 is cached. They deduce the ID 45 corresponds to the ASCII character - (hyphen).
  2. Payload Delivery: The attacker sends a request to /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.
  3. The Collision: Litestar computes the cache key for /api/news/-. The _safe_file_name function converts - to 45. It writes the response for the invalid request to the file .../api_news_45.
  4. The Victim: A legitimate user requests /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: Stop Being Clever, Start Hashing

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.

Official Patches

LitestarGitHub Commit fixing the issue

Fix Analysis (1)

Technical Appendix

CVSS Score
6.5/ 10
CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:L/I:L/A:N

Affected Systems

Litestar Framework < 2.20.0 using FileStore

Affected Versions Detail

Product
Affected Versions
Fixed Version
Litestar
litestar-org
< 2.20.02.20.0
AttributeDetail
CWE IDCWE-178
Attack VectorNetwork
CVSS Score6.5
ImpactCache Poisoning
Exploit StatusProof of Concept Available
KEV StatusNot Listed

MITRE ATT&CK Mapping

T1557Adversary-in-the-Middle
Credential Access
T1499Endpoint Denial of Service
Impact
CWE-178
Improper Handling of Case Sensitivity

Improper Handling of Case Sensitivity

Known Exploits & Detection

ManualLocal reproduction using Python script to demonstrate string collision.

Vulnerability Timeline

Patch Merged
2026-02-08
CVE Published
2026-02-09

References & Sources

  • [1]GHSA Advisory
  • [2]Litestar 2.20.0 Changelog

Attack Flow Diagram

Press enter or space to select a node. You can then use the arrow keys to move the node around. Press delete to remove it and escape to cancel.
Press enter or space to select an edge. You can then press delete to remove it or escape to cancel.