GHSA-39wr-7q6h-cf68: lmdeploy: URL parser mismatch enables SSRF bypass

GHSA-39wr-7q6h-cf68HIGH
Published September 18, 2026
CISO Take

lmdeploy's `_is_safe_url` check can be bypassed because Python's urlparse and the requests library disagree on which host a URL like `http://127.0.0.1:6666\@1.1.1.1` points to. The validator sees a public IP, but the HTTP client connects to the internal address. That lets an unauthenticated remote attacker reach loopback and private services (cloud metadata endpoints, internal admin APIs) from the inference server, with high confidentiality impact (CVSS 7.5). No KEV listing, public exploit, or Nuclei template is recorded and EPSS is not yet available, but the PoC is a single string in the advisory and the fix is trivial to reverse-engineer. Blast radius is modest (1 downstream dependent), though 9 other CVEs in the same package suggest a weak security track record. Upgrade lmdeploy to 0.15.0 (affected: >= 0.12.3, < 0.15.0) and restrict egress from inference hosts to internal networks and metadata services.

Sources:GitHub AdvisoryATLASNVD

What is the risk?

High, driven by network exploitability, no authentication, and low complexity (AV:N/AC:L/PR:N/UI:N). Impact is confidentiality only (C:H/I:N/A:N), but exposure of cloud credentials via metadata endpoints can escalate well beyond the inference service. Likelihood is raised by the one-line public PoC. No active exploitation (KEV: no), no public exploit module or scanner template, and EPSS is not available, so the immediate probability of mass exploitation is unquantified. Exposure is limited to deployments that accept user-supplied URLs (for example image or media URLs in multimodal requests) on internet-reachable or multi-tenant lmdeploy servers.

How does the attack unfold?

Initial Access
Attacker submits a crafted URL such as `http://127.0.0.1:6666\@1.1.1.1` to an exposed lmdeploy endpoint that fetches remote content.
AML.T0049
Validation Bypass
`_is_safe_url` parses the host as the public IP 1.1.1.1 and approves the URL, while requests interprets the real target as 127.0.0.1.
Internal Request
The inference server connects to loopback, private network, or cloud metadata services and retrieves their responses.
AML.T0036
Impact
Attacker harvests internal data or cloud credentials and pivots to other systems in the environment.
AML.T0040

What systems are affected?

PackageEcosystemVulnerable RangePatched
LMDeploypip>= 0.12.3, < 0.15.00.15.0
8.1K1 dependentsPushed 6d ago46% patched~18d to patchFull package profile →

Do you use LMDeploy? You're affected.

How severe is it?

CVSS 3.1
7.5 / 10
EPSS
N/A
Exploitation Status
No known exploitation
Sophistication
Trivial

What is the attack surface?

AVACPRUISCIA
AVNetwork
ACLow
PRNone
UINone
SUnchanged
CHigh
INone
ANone

What should I do?

1 step
  1. 1) Upgrade lmdeploy to 0.15.0 or later. 2) Until patched, do not expose the lmdeploy API server to untrusted users, and put it behind an authenticated gateway. 3) Enforce egress filtering on inference hosts: block access to 127.0.0.0/8, RFC1918 ranges, 169.254.169.254 and other metadata endpoints. 4) Enforce IMDSv2 (hop limit 1) or equivalent on cloud instances and use least-privilege instance roles. 5) Detection: search request logs and API payloads for URLs containing backslashes or \@, userinfo (@) components, or unexpected outbound connections from the inference process to internal addresses. 6) Pin to an allowlist of trusted media hosts if URL fetching is required.

How is it classified?

Which compliance frameworks are affected?

This CVE is relevant to:

EU AI Act
Art. 15-Accuracy, robustness and cybersecurity
ISO 42001
A.6.2.6-AI system operation and monitoring
NIST AI RMF
MANAGE 2.3-Response to previously unknown risks and vulnerabilities
OWASP LLM Top 10
LLM03-Supply ChainLLM06-Excessive Agency

Frequently Asked Questions

What is GHSA-39wr-7q6h-cf68?

lmdeploy's `_is_safe_url` check can be bypassed because Python's urlparse and the requests library disagree on which host a URL like `http://127.0.0.1:6666\@1.1.1.1` points to. The validator sees a public IP, but the HTTP client connects to the internal address. That lets an unauthenticated remote attacker reach loopback and private services (cloud metadata endpoints, internal admin APIs) from the inference server, with high confidentiality impact (CVSS 7.5). No KEV listing, public exploit, or Nuclei template is recorded and EPSS is not yet available, but the PoC is a single string in the advisory and the fix is trivial to reverse-engineer. Blast radius is modest (1 downstream dependent), though 9 other CVEs in the same package suggest a weak security track record. Upgrade lmdeploy to 0.15.0 (affected: >= 0.12.3, < 0.15.0) and restrict egress from inference hosts to internal networks and metadata services.

Is GHSA-39wr-7q6h-cf68 actively exploited?

No confirmed active exploitation of GHSA-39wr-7q6h-cf68 has been reported, but organizations should still patch proactively.

How to fix GHSA-39wr-7q6h-cf68?

1) Upgrade lmdeploy to 0.15.0 or later. 2) Until patched, do not expose the lmdeploy API server to untrusted users, and put it behind an authenticated gateway. 3) Enforce egress filtering on inference hosts: block access to 127.0.0.0/8, RFC1918 ranges, 169.254.169.254 and other metadata endpoints. 4) Enforce IMDSv2 (hop limit 1) or equivalent on cloud instances and use least-privilege instance roles. 5) Detection: search request logs and API payloads for URLs containing backslashes or `\@`, userinfo (`@`) components, or unexpected outbound connections from the inference process to internal addresses. 6) Pin to an allowlist of trusted media hosts if URL fetching is required.

What systems are affected by GHSA-39wr-7q6h-cf68?

This vulnerability affects the following AI/ML architecture patterns: model serving, LLM inference servers, multimodal inference pipelines, cloud-hosted GPU inference.

What is the CVSS score for GHSA-39wr-7q6h-cf68?

GHSA-39wr-7q6h-cf68 has a CVSS v3.1 base score of 7.5 (HIGH).

What is the AI security impact?

Affected AI Architectures

model servingLLM inference serversmultimodal inference pipelinescloud-hosted GPU inference

MITRE ATLAS Techniques

AML.T0036Data from Information Repositories
AML.T0040AI Model Inference API Access
AML.T0049Exploit Public-Facing Application

Compliance Controls Affected

EU AI Act: Art. 15
ISO 42001: A.6.2.6
NIST AI RMF: MANAGE 2.3
OWASP LLM Top 10: LLM03, LLM06

What are the technical details?

Original Advisory

### Summary The URL checking logic in lmdeploy has a logical flaw that could be bypassed by attackers, leading to SSRF attacks. ### Details The current lmdeploy project uses `_is_safe_url` to validate the input URL. The main logic is to perform security checks on the host portion of the URL extracted by urlparse to prevent SSRF attacks. <img width="943" height="836" alt="QQ20260416-203956-16-1" src="https://github.com/user-attachments/assets/042faad1-7458-444a-bbc9-525c772b0a4d" /> However, there are indeed differences in parsing between urlparse and the library that actually sends the request. Currently, almost all application scenarios in this project involve first using `_is_safe_url` for URL validation, and then using requests.Session().get to send the request. <img width="1086" height="576" alt="QQ20260416-204053-16-2" src="https://github.com/user-attachments/assets/7ffb8a69-b155-483a-90be-016c53e6387a" /> The core issue: `urlparse()` and `requests` disagree on which host a URL like `http://127.0.0.1:6666\@1.1.1.1` points to: - `urlparse()` treats `\` as a regular character and `@` as the userinfo-host delimiter, so it extracts hostname as 1.1.1.1 (public) - `requests` treats `\` as a path character, connecting to `127.0.0.1` (internal) Below is a test code I wrote following the code. ``` from urllib.parse import urlparse import ipaddress import socket import requests def _is_safe_url(url: str) -> tuple[bool, str]: """Check if the URL is safe to fetch (not internal/private).""" try: parsed = urlparse(url) if parsed.scheme not in ("http", "https"): return False, f"Unsupported scheme: {parsed.scheme}" hostname = parsed.hostname if not hostname: return False, "Could not parse hostname from URL" # check all IPs (IPv4 + IPv6) using getaddrinfo try: infos = socket.getaddrinfo(hostname, None) except socket.gaierror: return False, "Hostname resolution failed" for info in infos: ip = ipaddress.ip_address(info[4][0]) # block any IP that is not globally routable (covers private, loopback, # link-local, multicast, reserved, unspecified, etc.) if not ip.is_global: return False, f"Blocked non-global IP detected: {ip}" return True, "URL is safe" except Exception as e: return False, f"URL validation failed: {str(e)}" # url = "http://127.0.0.1:6666" url = "http://127.0.0.1:6666\@1.1.1.1" is_safe, reason = _is_safe_url(url) if not is_safe: raise ValueError(f"URL is blocked for security reasons: {reason}") fetch_timeout = 10 client = requests.Session() client.max_redirects = 3 response = client.get(url, timeout=fetch_timeout, allow_redirects=True) ``` When an attacker uses http://127.0.0.1:6666/, the existing detection logic can detect that this is an internal network address and block it. <img width="1286" height="195" alt="QQ20260416-204234-16-3" src="https://github.com/user-attachments/assets/b921ff01-3b9f-49a5-a410-bd21fe42f9c9" /> However, when an attacker uses `http://127.0.0.1:6666\@1.1.1.1`, the detection logic resolves the host to `1.1.1.1`, which is a public IP address, thus passing the verification. But in the actual request process, this URL is forwarded by requests.get to `http://127.0.0.1:6666/`, bypassing the detection and achieving an SSRF attack. <img width="2064" height="154" alt="QQ20260416-204319-16-4" src="https://github.com/user-attachments/assets/5da18f35-f400-46e6-9bf3-1330ba424b02" /> ### PoC ``` http://127.0.0.1:6666\@1.1.1.1 ``` ### Impact SSRF

Exploitation Scenario

An attacker sends a multimodal inference request to an exposed lmdeploy server with an image URL such as `http://169.254.169.254:80\@1.1.1.1`. The `_is_safe_url` check parses the host as 1.1.1.1, resolves it to a global IP, and approves the URL. The requests library then connects to the cloud metadata service, and the response is processed by the server. The attacker retrieves temporary IAM credentials or probes internal services, then uses those credentials to pivot into model storage buckets, data stores, and the wider cloud environment.

Weaknesses (CWE)

CWE-436 — Interpretation Conflict: Product A handles inputs or steps differently than Product B, which causes A to perform incorrect actions based on its perception of B's state.

Source: MITRE CWE corpus.

CVSS Vector

CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:N/A:N

Timeline

Published
September 18, 2026
Last Modified
September 18, 2026
First Seen
September 18, 2026

Related Vulnerabilities