@tduncan / Nvidia-Optimization-Stack-VLLM Mirror · read-only / commits / 3958b96bf5
Fork 0

A high-throughput and memory-efficient inference and serving engine for LLMs

Mirrored from https://github.com/tduncan-lum/Nvidia-Optimization-Stack---VLLM.git

Synced Nov 14, 05:30 UTC

Add option to restrict media domains (#25783)

Signed-off-by: Chenheli Hua <huachenheli@outlook.com> Signed-off-by: Russell Bryant <rbryant@redhat.com> Co-authored-by: Chenheli Hua <huachenheli@outlook.com>

Russell Bryant committed Sep 26, 2025 at 21:23 UTC 3958b96bf5f771560053b752424b1e7caba04a61
11 files changed +80 -1
docs/features/multimodal_inputs.md
+4
@@ -6,6 +6,10 @@ This page teaches you how to pass multi-modal inputs to [multi-modal models][sup
6 We are actively iterating on multi-modal support. See [this RFC](gh-issue:4194) for upcoming changes,
7 and [open an issue on GitHub](https://github.com/vllm-project/vllm/issues/new/choose) if you have any feedback or feature requests.
8
9 +!!! tip
10 + When serving multi-modal models, consider setting `--allowed-media-domains` to restrict domain that vLLM can access to prevent it from accessing arbitrary endpoints that can potentially be vulnerable to Server-Side Request Forgery (SSRF) attacks. You can provide a list of domains for this arg. For example: `--allowed-media-domains upload.wikimedia.org github.com www.bogotobogo.com`
11 + This restriction is especially important if you run vLLM in a containerized environment where the vLLM pods may have unrestricted access to internal networks.
12 +
13 ## Offline Inference
14
15 To input multi-modal data, follow this schema in [vllm.inputs.PromptType][]:
docs/usage/security.md
+6
@@ -60,6 +60,12 @@ Key points from the PyTorch security guide:
60 - Implement proper authentication and authorization for management interfaces
61 - Follow the principle of least privilege for all system components
62
63 +### 4. **Restrict Domains Access for Media URLs:**
64 +
65 +Restrict domains that vLLM can access for media URLs by setting
66 +`--allowed-media-domains` to prevent Server-Side Request Forgery (SSRF) attacks.
67 +(e.g. `--allowed-media-domains upload.wikimedia.org github.com www.bogotobogo.com`)
68 +
69 ## Security and Firewalls: Protecting Exposed vLLM Systems
70
71 While vLLM is designed to allow unsafe network services to be isolated to
tests/entrypoints/openai/test_lora_resolvers.py
+1
@@ -45,6 +45,7 @@ class MockModelConfig:
45 logits_processor_pattern: Optional[str] = None
46 diff_sampling_param: Optional[dict] = None
47 allowed_local_media_path: str = ""
48 + allowed_media_domains: Optional[list[str]] = None
49 encoder_config = None
50 generation_config: str = "auto"
51 skip_tokenizer_init: bool = False
tests/entrypoints/openai/test_serving_chat.py
+1
@@ -240,6 +240,7 @@ class MockModelConfig:
240 logits_processor_pattern = None
241 diff_sampling_param: Optional[dict] = None
242 allowed_local_media_path: str = ""
243 + allowed_media_domains: Optional[list[str]] = None
244 encoder_config = None
245 generation_config: str = "auto"
246 media_io_kwargs: dict[str, dict[str, Any]] = field(default_factory=dict)
tests/multimodal/test_utils.py
+32 -1
@@ -66,7 +66,12 @@ async def test_fetch_image_http(image_url: str):
66 @pytest.mark.parametrize("suffix", get_supported_suffixes())
67 async def test_fetch_image_base64(url_images: dict[str, Image.Image],
68 raw_image_url: str, suffix: str):
69 - connector = MediaConnector()
69 + connector = MediaConnector(
70 + # Domain restriction should not apply to data URLs.
71 + allowed_media_domains=[
72 + "www.bogotobogo.com",
73 + "github.com",
74 + ])
75 url_image = url_images[raw_image_url]
76
77 try:
@@ -387,3 +392,29 @@ def test_argsort_mm_positions(case):
392 modality_idxs = argsort_mm_positions(mm_positions)
393
394 assert modality_idxs == expected_modality_idxs
395 +
396 +
397 +@pytest.mark.asyncio
398 +@pytest.mark.parametrize("video_url", TEST_VIDEO_URLS)
399 +@pytest.mark.parametrize("num_frames", [-1, 32, 1800])
400 +async def test_allowed_media_domains(video_url: str, num_frames: int):
401 + connector = MediaConnector(
402 + media_io_kwargs={"video": {
403 + "num_frames": num_frames,
404 + }},
405 + allowed_media_domains=[
406 + "www.bogotobogo.com",
407 + "github.com",
408 + ])
409 +
410 + video_sync, metadata_sync = connector.fetch_video(video_url)
411 + video_async, metadata_async = await connector.fetch_video_async(video_url)
412 + assert np.array_equal(video_sync, video_async)
413 + assert metadata_sync == metadata_async
414 +
415 + disallowed_url = "https://upload.wikimedia.org/wikipedia/commons/4/47/PNG_transparency_demonstration_1.png"
416 + with pytest.raises(ValueError):
417 + _, _ = connector.fetch_video(disallowed_url)
418 +
419 + with pytest.raises(ValueError):
420 + _, _ = await connector.fetch_video_async(disallowed_url)
vllm/config/model.py
+3
@@ -137,6 +137,9 @@ class ModelConfig:
137 """Allowing API requests to read local images or videos from directories
138 specified by the server file system. This is a security risk. Should only
139 be enabled in trusted environments."""
140 + allowed_media_domains: Optional[list[str]] = None
141 + """If set, only media URLs that belong to this domain can be used for
142 + multi-modal inputs. """
143 revision: Optional[str] = None
144 """The specific model version to use. It can be a branch name, a tag name,
145 or a commit id. If unspecified, will use the default version."""
vllm/config/speculative.py
+2
@@ -288,6 +288,8 @@ class SpeculativeConfig:
288 trust_remote_code,
289 allowed_local_media_path=self.target_model_config.
290 allowed_local_media_path,
291 + allowed_media_domains=self.target_model_config.
292 + allowed_media_domains,
293 dtype=self.target_model_config.dtype,
294 seed=self.target_model_config.seed,
295 revision=self.revision,
vllm/engine/arg_utils.py
+5
@@ -297,6 +297,8 @@ class EngineArgs:
297 tokenizer_mode: TokenizerMode = ModelConfig.tokenizer_mode
298 trust_remote_code: bool = ModelConfig.trust_remote_code
299 allowed_local_media_path: str = ModelConfig.allowed_local_media_path
300 + allowed_media_domains: Optional[
301 + list[str]] = ModelConfig.allowed_media_domains
302 download_dir: Optional[str] = LoadConfig.download_dir
303 safetensors_load_strategy: str = LoadConfig.safetensors_load_strategy
304 load_format: Union[str, LoadFormats] = LoadConfig.load_format
@@ -531,6 +533,8 @@ class EngineArgs:
533 **model_kwargs["hf_config_path"])
534 model_group.add_argument("--allowed-local-media-path",
535 **model_kwargs["allowed_local_media_path"])
536 + model_group.add_argument("--allowed-media-domains",
537 + **model_kwargs["allowed_media_domains"])
538 model_group.add_argument("--revision", **model_kwargs["revision"])
539 model_group.add_argument("--code-revision",
540 **model_kwargs["code_revision"])
@@ -997,6 +1001,7 @@ class EngineArgs:
1001 tokenizer_mode=self.tokenizer_mode,
1002 trust_remote_code=self.trust_remote_code,
1003 allowed_local_media_path=self.allowed_local_media_path,
1004 + allowed_media_domains=self.allowed_media_domains,
1005 dtype=self.dtype,
1006 seed=self.seed,
1007 revision=self.revision,
vllm/entrypoints/chat_utils.py
+6
@@ -632,6 +632,10 @@ class BaseMultiModalItemTracker(ABC, Generic[_T]):
632 def allowed_local_media_path(self):
633 return self._model_config.allowed_local_media_path
634
635 + @property
636 + def allowed_media_domains(self):
637 + return self._model_config.allowed_media_domains
638 +
639 @property
640 def mm_registry(self):
641 return MULTIMODAL_REGISTRY
@@ -832,6 +836,7 @@ class MultiModalContentParser(BaseMultiModalContentParser):
836 self._connector = MediaConnector(
837 media_io_kwargs=media_io_kwargs,
838 allowed_local_media_path=tracker.allowed_local_media_path,
839 + allowed_media_domains=tracker.allowed_media_domains,
840 )
841
842 def parse_image(
@@ -916,6 +921,7 @@ class AsyncMultiModalContentParser(BaseMultiModalContentParser):
921 self._connector = MediaConnector(
922 media_io_kwargs=media_io_kwargs,
923 allowed_local_media_path=tracker.allowed_local_media_path,
924 + allowed_media_domains=tracker.allowed_media_domains,
925 )
926
927 def parse_image(
vllm/entrypoints/llm.py
+4
@@ -86,6 +86,8 @@ class LLM:
86 or videos from directories specified by the server file system.
87 This is a security risk. Should only be enabled in trusted
88 environments.
89 + allowed_media_domains: If set, only media URLs that belong to this
90 + domain can be used for multi-modal inputs.
91 tensor_parallel_size: The number of GPUs to use for distributed
92 execution with tensor parallelism.
93 dtype: The data type for the model weights and activations. Currently,
@@ -169,6 +171,7 @@ class LLM:
171 skip_tokenizer_init: bool = False,
172 trust_remote_code: bool = False,
173 allowed_local_media_path: str = "",
174 + allowed_media_domains: Optional[list[str]] = None,
175 tensor_parallel_size: int = 1,
176 dtype: ModelDType = "auto",
177 quantization: Optional[QuantizationMethods] = None,
@@ -264,6 +267,7 @@ class LLM:
267 skip_tokenizer_init=skip_tokenizer_init,
268 trust_remote_code=trust_remote_code,
269 allowed_local_media_path=allowed_local_media_path,
270 + allowed_media_domains=allowed_media_domains,
271 tensor_parallel_size=tensor_parallel_size,
272 dtype=dtype,
273 quantization=quantization,
vllm/multimodal/utils.py
+16
@@ -50,6 +50,7 @@ class MediaConnector:
50 connection: HTTPConnection = global_http_connection,
51 *,
52 allowed_local_media_path: str = "",
53 + allowed_media_domains: Optional[list[str]] = None,
54 ) -> None:
55 """
56 Args:
@@ -82,6 +83,9 @@ class MediaConnector:
83 allowed_local_media_path_ = None
84
85 self.allowed_local_media_path = allowed_local_media_path_
86 + if allowed_media_domains is None:
87 + allowed_media_domains = []
88 + self.allowed_media_domains = allowed_media_domains
89
90 def _load_data_url(
91 self,
@@ -115,6 +119,14 @@ class MediaConnector:
119
120 return media_io.load_file(filepath)
121
122 + def _assert_url_in_allowed_media_domains(self, url_spec) -> None:
123 + if self.allowed_media_domains and url_spec.hostname not in \
124 + self.allowed_media_domains:
125 + raise ValueError(
126 + f"The URL must be from one of the allowed domains: "
127 + f"{self.allowed_media_domains}. Input URL domain: "
128 + f"{url_spec.hostname}")
129 +
130 def load_from_url(
131 self,
132 url: str,
@@ -125,6 +137,8 @@ class MediaConnector:
137 url_spec = urlparse(url)
138
139 if url_spec.scheme.startswith("http"):
140 + self._assert_url_in_allowed_media_domains(url_spec)
141 +
142 connection = self.connection
143 data = connection.get_bytes(url, timeout=fetch_timeout)
144
@@ -150,6 +164,8 @@ class MediaConnector:
164 loop = asyncio.get_running_loop()
165
166 if url_spec.scheme.startswith("http"):
167 + self._assert_url_in_allowed_media_domains(url_spec)
168 +
169 connection = self.connection
170 data = await connection.async_get_bytes(url, timeout=fetch_timeout)
171 future = loop.run_in_executor(global_thread_pool,