High severity7.5NVD Advisory· Published Apr 20, 2026· Updated Apr 23, 2026
CVE-2026-33626
CVE-2026-33626
Description
LMDeploy is a toolkit for compressing, deploying, and serving large language models. Versions prior to 0.12.3 have a Server-Side Request Forgery (SSRF) vulnerability 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. Version 0.12.3 patches the issue.
Affected packages
Versions sourced from the GitHub Security Advisory.
| Package | Affected versions | Patched versions |
|---|---|---|
lmdeployPyPI | <= 0.12.2 | — |
Affected products
1Patches
14 files changed · +97 −7
docs/en/conf.py+1 −1 modified@@ -164,7 +164,7 @@ def metrics(): # { # "name": "切换至简体中文", # "url": "https://lmdeploy.readthedocs.io/en/latest", - # "icon": "https://img.shields.io/badge/Doc-%E7%AE%80%E4%BD%93%E4%B8%AD%E6%96%87-blue", # noqa: #501 + # "icon": "https://img.shields.io/badge/Doc-%E7%AE%80%E4%BD%93%E4%B8%AD%E6%96%87-blue", # noqa: E501 # "type": "url", # }, # ],
lmdeploy/pytorch/config.py+12 −3 modified@@ -57,7 +57,13 @@ def _update_torch_dtype(config: 'ModelConfig', dtype: str, device_type: str = 'a torch_dtype = torch_dtype if torch_dtype in ['float16', 'bfloat16'] else 'float16' else: torch_dtype = dtype - config.dtype = eval(f'torch.{torch_dtype}') + + resolved_dtype = getattr(torch, torch_dtype, None) + if not isinstance(resolved_dtype, torch.dtype): + raise ValueError(f'Invalid torch dtype "{torch_dtype}" resolved from model config; ' + 'expected a torch.dtype attribute on torch.') + config.dtype = resolved_dtype + return config @@ -629,8 +635,11 @@ def from_config(cls, hf_config: Any): else: raise TypeError(f'Unsupported quant method: {quant_method}') - if quant_dtype is not None: - quant_dtype = eval(f'torch.{quant_dtype}') + resolved_quant_dtype = getattr(torch, quant_dtype, None) + if not isinstance(resolved_quant_dtype, torch.dtype): + raise ValueError(f'Invalid quant dtype "{quant_dtype}" resolved from model config; ' + 'expected a torch.dtype attribute on torch.') + quant_dtype = resolved_quant_dtype ignored_layers = quant_config.get('ignored_layers', []) if not ignored_layers:
lmdeploy/vl/media/connection.py+37 −3 modified@@ -1,5 +1,7 @@ # Copyright (c) OpenMMLab. All rights reserved. +import ipaddress import os +import socket from pathlib import Path from typing import TypeVar from urllib.parse import ParseResult, urlparse @@ -20,9 +22,40 @@ } +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)}' + + def _load_http_url(url_spec: ParseResult, media_io: MediaIO[_M]) -> _M: - if url_spec.scheme not in ('http', 'https'): - raise ValueError(f'Unsupported URL scheme: {url_spec.scheme}') + url = url_spec.geturl() + is_safe, reason = _is_safe_url(url) + if not is_safe: + raise ValueError(f'URL is blocked for security reasons: {reason}') fetch_timeout = 10 if isinstance(media_io, ImageMediaIO): @@ -31,7 +64,8 @@ def _load_http_url(url_spec: ParseResult, media_io: MediaIO[_M]) -> _M: fetch_timeout = int(os.environ.get('LMDEPLOY_VIDEO_FETCH_TIMEOUT', 30)) client = requests.Session() - response = client.get(url_spec.geturl(), headers=headers, timeout=fetch_timeout) + client.max_redirects = 3 + response = client.get(url_spec.geturl(), headers=headers, timeout=fetch_timeout, allow_redirects=True) response.raise_for_status() return media_io.load_bytes(response.content)
tests/test_lmdeploy/test_vl/test_safe_url.py+47 −0 added@@ -0,0 +1,47 @@ +import socket +from unittest.mock import MagicMock, patch +from urllib.parse import urlparse + +import pytest + +from lmdeploy.vl.media.connection import _is_safe_url, _load_http_url + + +@pytest.mark.parametrize( + 'url,expected_safe,mock_ips', + [ + ('https://github.com', True, ['140.82.112.3']), # Public domain + ('http://8.8.8.8', True, ['8.8.8.8']), # Public IPv4 + ('ftp://example.com', False, []), # Forbidden scheme + ('http://127.0.0.1', False, ['127.0.0.1']), # IPv4 loopback + ('http://localhost', False, ['127.0.0.1']), # Resolves to loopback + ('http://169.254.169.254', False, ['169.254.169.254']), # Cloud metadata service + ('http://[::1]', False, ['::1']), # IPv6 loopback + ('http://[fc00::1]', False, ['fc00::1']), # IPv6 unique local address + ('http://mixed-dns.com', False, ['1.1.1.1', '10.0.0.1']), # DNS Rebinding + ('http://', False, []), # Empty host + ('http://invalid-host-name', False, None), # Invalid hostname (simulate DNS failure) + ]) +def test_is_safe_url(url, expected_safe, mock_ips): + with patch('socket.getaddrinfo') as mock_gai: + if mock_ips is None: + # simulate DNS resolution failure + mock_gai.side_effect = socket.gaierror('Hostname resolution failed') + else: + mock_gai.return_value = [(socket.AF_INET if '.' in ip else socket.AF_INET6, None, None, None, (ip, 80)) + for ip in mock_ips] + is_safe, _ = _is_safe_url(url) + assert is_safe == expected_safe + + +@patch('requests.Session.get') +@patch('lmdeploy.vl.media.connection._is_safe_url', return_value=(True, '')) +def test_load_http_url_logic(mock_safe, mock_get): + media_io = MagicMock() + url_spec = urlparse('https://example.com/img.jpg') + + # test success with allow_redirects=True + mock_get.return_value = MagicMock(content=b'data', status_code=200) + media_io.load_bytes.return_value = 'loaded' + assert _load_http_url(url_spec, media_io) == 'loaded' + assert mock_get.call_args.kwargs['allow_redirects'] is True
Vulnerability mechanics
Generated by null/stub on May 9, 2026. Inputs: CWE entries + fix-commit diffs from this CVE's patches. Citations validated against bundle.
References
6- github.com/InternLM/lmdeploy/commit/71d64a339edb901e9005358e0633fbbab367d626nvdPatchWEB
- github.com/InternLM/lmdeploy/pull/4447nvdIssue TrackingPatchWEB
- github.com/InternLM/lmdeploy/security/advisories/GHSA-6w67-hwm5-92mqnvdExploitMitigationVendor AdvisoryWEB
- github.com/advisories/GHSA-6w67-hwm5-92mqghsaADVISORY
- nvd.nist.gov/vuln/detail/CVE-2026-33626ghsaADVISORY
- github.com/InternLM/lmdeploy/releases/tag/v0.12.3nvdRelease NotesWEB
News mentions
3- ⚡ Weekly Recap: Fast16 Malware, XChat Launch, Federal Backdoor, AI Employee Tracking & MoreThe Hacker News · Apr 27, 2026
- 27th April – Threat Intelligence ReportCheck Point Research · Apr 27, 2026
- LMDeploy CVE-2026-33626 Flaw Exploited Within 13 Hours of DisclosureThe Hacker News · Apr 24, 2026