Threat Intelligence

Live CVE feed

299 threats tracked across 7 launch stacks — sourced from NVD, GHSA, CISA KEV, OSV, npm Audit, and EPSS.

152threats · All threats· page 1/8
Get guardrails →

Pydantic AI: SSRF cloud-metadata blocklist bypass via IPv4-mapped IPv6 (Incomplete fix of CVE-2026-25580)

Summary When an application using Pydantic AI opts a URL into force_download='allow-local' (which disables the default block on private/internal IPs), the cloud-metadata blocklist could be bypassed by encoding the metadata IP in an IPv6 transition form (IPv4-mapped IPv6, 6to4, or NAT64). Dual-stack and translated networks route the IPv6 wrapper to the underlying IPv4 endpoint, exposing cloud IAM short-term credentials. This is an incomplete fix of GHSA-2jrp-274c-jhv3 / CVE-2026-25580. The parent advisory's remediation guaranteed that "cloud metadata endpoints are always blocked, even with allow-local." That guarantee did not hold for IPv6-encoded forms of the metadata IPs. Severity Same impact metrics as the parent CVE, but materially narrower attack surface (AC:H instead of AC:L), because exploitation requires the application to have opted into allow-local on a URL influenced by untrusted input. Who Is Affected Applications are affected only if they explicitly opt for FileUrl (ImageUrl, AudioUrl, VideoUrl, DocumentUrl) into force_download='allow-local' on a URL that is, or could be, influenced by untrusted input. Applications are not affected if they use any of the bundled integrations to ingest user input, because they do not propagate force_download from external data: Agent.to_web / clai web VercelAIAdapter AGUIAdapter / Agent.to_ag_ui Applications that only download from developer-controlled URLs are not affected. Remediation Upgrade to 1.99.0 or later. The cloud-metadata and private-IP blocklists now apply to IPv6 transition forms that route to a blocked IPv4 endpoint (IPv4-mapped IPv6, 6to4, and NAT64 well-known prefix). The blocklists have also been extended to cover additional IANA-reserved IPv4 and IPv6 special-purpose ranges. Workaround for Unpatched Versions Avoid passing force_download='allow-local' on any URL that could be influenced by untrusted input. If developers must, resolve the hostname themselves and validate the result against their own metadata blocklist — including IPv6-encoded forms — before constructing the FileUrl. Credits Reported by j0hndo.

OWASP A10LLM02 · Insecure OutputOWASP LLM
Get guardrail →

LangSmith SDK: Public prompt pull deserializes untrusted manifests without trust boundary warning

Description The LangSmith SDK's prompt pull methods (pull_prompt / pull_prompt_commit in Python, pullPrompt / pullPromptCommit in JS/TS) fetch and deserialize prompt manifests from the LangSmith Hub. These manifests may contain serialized LangChain objects and model configuration that affect runtime behavior. When pulling a public prompt by owner/name identifier, the manifest content is controlled by an external party, but prior versions of the SDK did not distinguish this from pulling a prompt within the caller's own organization. Prompt manifests can intentionally configure a model with a custom base URL, default headers, model name, or other constructor arguments. These are supported features, but they also mean the prompt contents should be treated as executable configuration rather than plain text. A prompt can also include serialized LangChain Runnable or PromptTemplate objects with attacker-controlled constructor kwargs, or secret references that, if secrets_from_env is enabled, read environment variables at deserialization time. Applications are exposed when all of the following are true: The application calls pull_prompt or pull_prompt_commit (Python) or pullPrompt or pullPromptCommit (JS/TS) with a public owner/name prompt identifier. The prompt was published or modified by an untrusted or compromised account. The application uses the pulled prompt without independently validating its contents. Applications that only pull prompts from their own organization (referenced by name only, without an owner/ prefix) are not affected by the public prompt trust boundary issue described above. However, same-organization prompts carry their own risk. If an attacker gains write access to the organization (for example, through a leaked LANGSMITH_API_KEY or a compromised team member account), they can push a malicious prompt that is pulled and deserialized without any additional warning. Impact An attacker who publishes a malicious prompt to LangSmith Hub may be able to affect applications that pull that prompt by owner/name. If the prompt manifest reaches the SDK's deserialization path, the SDK will instantiate the referenced LangChain objects with the attacker-supplied constructor arguments rather than treating the manifest as inert data. Realistic impacts include: Server-side request forgery (SSRF), outbound request redirection, and interception of LLM traffic if a prompt manifest configures an LLM client with an attacker-controlled base_url, proxy, or equivalent endpoint-setting parameter. In typical deployments, redirected requests may include prompt contents, system prompts, retrieved context, model parameters, provider credentials, or other secrets and may disclose them to the attacker-controlled endpoint. Prompt injection or behavior manipulation if a manifest embeds attacker-controlled system messages, prompt templates, or model parameters that alter the application's behavior. Additional deserialization risk when include_model=True is passed, because this expands the allowlist to partner integration classes. This is not the default, but it materially increases risk when pulling prompts from outside the caller's organization. Remediation The LangSmith SDK now blocks pulling public prompts by owner/name by default. Callers must explicitly opt in by passing dangerously_pull_public_prompt=True (Python) or dangerouslyPullPublicPrompt: true (JS/TS) to acknowledge the trust boundary. This flag should only be set after reviewing and trusting the prompt contents, not merely the publishing account. Upgrade to LangSmith SDK Python >= 0.8.0 or JS/TS >= 0.6.0. Guidance for prompt pull methods The prompt pull methods (pull_prompt / pull_prompt_commit in Python, pullPrompt / pullPromptCommit in JS/TS) should be used only with trusted prompts. Do not pull public prompts by owner/name from untrusted or unreviewed sources without understanding that the manifest contents will be deserialized and may affect runtime behavior. When pulling prompts that include model configuration (include_model=True in Python, includeModel: true in JS/TS), the deserialization allowlist expands to include partner integration classes. Because this mode is not the default and is often unnecessary for third-party prompts, prefer the default (false) when pulling prompts from sources outside your organization. Avoid passing secrets_from_env=True (Python) when pulling untrusted prompts. This parameter allows prompt manifests to read environment variables during deserialization. Only use it with trusted prompts from your own organization. Same-organization prompts Prompts pulled from the caller's own organization (referenced by name only, without an owner/ prefix) are not gated by the new dangerously_pull_public_prompt flag, but they are not inherently safe. If an attacker gains write access to the organization (for example, through a leaked LANGSMITH_API_KEY or a compromised team member account), they can push a malicious prompt that redirects LLM traffic to attacker-controlled infrastructure and may disclose any credentials attached to those requests. The security of same-organization prompts follows a shared responsibility model. The LangSmith SDK enforces trust boundaries for public prompts pulled from external accounts, but it cannot protect against compromised credentials or accounts within the caller's own organization. Securing API keys, managing team member access, and reviewing prompt contents before production deployment are the responsibility of the organization. Organizations should treat prompts as executable configuration and apply the same review and audit practices they would apply to application code. Credits First reported by @Moaaz-0x.

OWASP A08LLM05 · Supply ChainOWASP LLM
Get guardrail →

LangChain vulnerable to unsafe deserialization of attacker-controlled objects through overly broad `load()` allowlists

LangChain contains older runtime code paths that deserialize run inputs, run outputs, or other application-controlled payloads using overly broad object allowlists. These paths may call load() with allowed_objects="all". This does not enable arbitrary Python object deserialization, but it does allow any trusted LangChain-serializable object to be revived, which is broader than these runtime paths require. As a result, attacker-supplied LangChain serialized constructor dictionaries may cause trusted runtime paths to instantiate classes with untrusted constructor arguments. Applications are exposed only when all of the following are true: 1. The application accepts untrusted structured input, such as JSON, from a user or network request. 2. The application does not validate or canonicalize that input into an inert schema before invoking LangChain. 3. Attacker-controlled nested dictionaries or lists are preserved in LangChain run inputs or outputs. 4. The application uses an affected API path that later deserializes that run data. Known affected runtime surfaces include: RunnableWithMessageHistory astream_log() astream_events(version="v1") Related unsafe deserialization patterns may also affect applications that explicitly load serialized LangChain prompt or runnable objects from untrusted sources, including shared prompt stores, Hub artifacts with model configuration, or other application-controlled serialization stores. Applications that validate incoming requests against a fixed schema, such as coercing user input to a plain string or message-content field before invoking LangChain, are unlikely to expose this deserialization primitive. This release also fixes a related secret-marker validation bypass in the serialization and deserialization layer (_is_lc_secret). That issue creates an additional path by which attacker-controlled constructor dictionaries can avoid escaping during dumps() -> loads() round-trips and reach LangChain object revival logic. Impact An attacker who can submit untrusted structured input to an affected application, and have that structure preserved in LangChain run data, may be able to inject LangChain serialized constructor payloads such as: ``json { "lc": 1, "type": "constructor", "id": ["langchain_core", "messages", "ai", "AIMessage"], "kwargs": {"content": "attacker-controlled content"} } ` If this payload reaches a broad load() call, LangChain may instantiate the referenced class instead of treating the payload as inert user data. Realistic impacts include: Persistent chat-history poisoning when revived AIMessage, HumanMessage, or SystemMessage objects are stored by RunnableWithMessageHistory. Prompt injection or behavior manipulation if attacker-controlled messages are later included in model context. Instantiation of unexpected trusted LangChain objects with attacker-controlled constructor arguments. Possible credential disclosure or server-side requests if a reachable object reads environment credentials, creates clients, or contacts attacker-controlled endpoints during initialization. Additional prompt-template or runnable-configuration impacts in applications that separately load and execute untrusted serialized LangChain objects. Remediation LangChain will deprecate the affected APIs as part of this fix: RunnableWithMessageHistory astream_log() astream_events(version="v1") These are older code paths that are no longer recommended for new applications. They were not previously marked as deprecated, but recent LangChain documentation has primarily directed users toward newer streaming and memory patterns, including the stream API. Applications should migrate to the currently recommended APIs rather than continue depending on these older surfaces. Separately, LangChain will update load() and loads() to tighten deserialization behavior so broad object revival is not applied implicitly to untrusted or application-controlled payloads. The older runtime surfaces listed above are being deprecated rather than preserved as supported paths for broad runtime deserialization. This release also fixes a related secret-marker validation bypass in the serialization and deserialization layer (_is_lc_secret). That issue creates an additional path by which attacker-controlled constructor dictionaries can avoid escaping during dumps() -> loads() round-trips and reach LangChain object revival logic. Guidance for load() and loads() load() and loads() should be used only with trusted LangChain manifests or serialized objects from trusted storage. Do not pass user-controlled data to load() or loads(), and do not use them as general parsers for request bodies, tool inputs, chat messages, or other attacker-controlled data. load() and loads() are beta APIs, and their behavior may change as LangChain narrows unsafe defaults. Future LangChain versions will require callers to be explicit about which objects may be revived. Users should pass a narrow allowed_objects value appropriate for the specific trusted manifest they are loading, rather than relying on broad defaults or allowed_objects="all", which permits the full trusted LangChain serialization allowlist. Credits The original issue was first reported by @u-ktdi. Similar findings were reported by @dewankpant, @shrutilohani, @Moaaz-0x, @pucagit. A related _is_lc_secret marker bypass affecting dumps() -> loads()` round-trips was reported by @yardenporat353 (and a similar report by @localhost-detect)

OWASP A08LLM05 · Supply ChainOWASP LLM
Get guardrail →

vLLM: extract_hidden_states speculative decoding crashes server on any request with penalty parameters

Summary The extract_hidden_states speculative decoding proposer in vLLM returns a tensor with an incorrect shape after the first decode step, causing a RuntimeError that crashes the EngineCore process. The crash is triggered when any request in the batch uses sampling penalty parameters (repetition_penalty, frequency_penalty, or presence_penalty). A single request with a penalty parameter (e.g., "repetition_penalty": 1.1) is sufficient to crash the server. The crash is deterministic and immediate — no concurrency, race condition, or special workload is required. Details In vLLM v0.17.0, the extract_hidden_states proposer's propose() method returned sampled_token_ids.unsqueeze(-1), producing a tensor of shape (batch_size, 1). In PR #37013 (first released in v0.18.0), the KV connector interface was refactored out of propose(). The return type changed from tuple[Tensor, KVConnectorOutput | None] to Tensor, and the .unsqueeze(-1) call was removed along with the KV connector output: ``python Before (v0.17.0): return sampled_token_ids.unsqueeze(-1), kv_connector_output # shape (batch_size, 1) After (v0.18.0+): return sampled_token_ids # shape (batch_size, 2) after first decode step ` The refactor missed that sampled_token_ids changed semantics between the first and subsequent decode steps. After the first decode step, the rejection sampler allocates its output as (batch_size, max_spec_len + 1). With num_speculative_tokens=1, this produces shape (batch_size, 2) instead of the expected (batch_size, 1), causing a broadcast shape mismatch during penalty application. Impact Any vLLM deployment between v0.18.0 and v0.19.1 (inclusive) configured with extract_hidden_states speculative decoding is affected. A single API request containing any penalty parameter immediately and permanently crashes the EngineCore process, resulting in complete loss of service availability. Patches Fixed in PR #38610, first included in vLLM v0.20.0. The fix slices the return value to sampled_token_ids[:, :1], ensuring the correct (batch_size, 1) shape regardless of the rejection sampler's output dimensions. Workarounds Upgrade to vLLM v0.20.0 or later. If upgrading is not possible, avoid using extract_hidden_states as the speculative decoding method on affected versions. Alternatively, reject or strip penalty parameters (repetition_penalty, frequency_penalty, presence_penalty`) from incoming requests at an API gateway before they reach vLLM.

vLLM Vulnerable to Remote DoS via Special-Token Placeholders

Summary This report explains a Token Injection vulnerability in vLLM’s multimodal processing. Unauthenticated, text-only prompts that spell special tokens are interpreted as control. Image and video placeholder sequences supplied without matching data cause vLLM to index into empty grids during input-position computation, raising an unhandled IndexError and terminating the worker or degrading availability. Multimodal paths that rely on image_grid_thw/video_grid_thw are affected. Severity: High (remote DoS). Reproduced on vLLM 0.10.0 with Qwen2.5-VL. Details Affected component: multimodal input position computation. File/functions (paths are indicative): vllm/model_executor/layers/rotary_embedding.py get_input_positions_tensor(...) _vl_get_input_positions_tensor(...) Failure mechanism: The code counts detected vision tokens and then indexes video_grid_thw/image_grid_thw accordingly. When user input carries placeholder tokens but no actual multimodal payload, these grids are empty. The code does not bounds-check before indexing. Representative snippet (context): ``python vllm/model_executor/layers/rotary_embedding.py @classmethod def _vl_get_input_positions_tensor( cls, input_tokens, hf_config, image_grid_thw, video_grid_thw, ..., ): # detect video tokens video_nums = (vision_tokens == video_token_id).sum() # later in processing t, h, w = ( video_grid_thw[video_index][0], # IndexError if no video data video_grid_thw[video_index][1], video_grid_thw[video_index][2], ) ` Abbreviated call path: ` OpenAI API request → vllm.v1.engine.core: step/execute_model → vllm.v1.worker.gpu_model_runner: _update_states/execute_model → vllm.model_executor.layers.rotary_embedding: get_input_positions_tensor → _vl_get_input_positions_tensor → IndexError: list index out of range ` PoC Environment vLLM: 0.10.0 Model: Qwen/Qwen2.5-VL-3B-Instruct Launch server: `bash python -m vllm.entrypoints.openai.api_server \ --model Qwen/Qwen2.5-VL-3B-Instruct \ --port 8000 ` Request (text-only, no image/video data) `bash cat > request.json <<'JSON' { "model": "Qwen/Qwen2.5-VL-3B-Instruct", "messages": [ { "role": "user", "content": [ { "type": "text", "text": "what's in picture <|vision_start|><|image_pad|><|vision_end|>" } ] } ] } JSON curl -s http://127.0.0.1:8000/v1/chat/completions \ -H 'Content-Type: application/json' \ --data @request.json `` Observed result HTTP 500; logs show IndexError: list index out of range from _vl_get_input_positions_tensor(...). In some deployments, the worker exits and capacity remains reduced until manual restart. Impact Type: Token Injection leading to Remote Denial of Service (unauthenticated). A single request can trigger the fault. Scope: Any vLLM deployment that serves VLMs and accepts raw user text via OpenAI-compatible endpoints (self-hosted or proxied/managed fronts). Effect: Request → unhandled exception in position computation → worker termination / service unavailability. Fixes Changes associated with https://github.com/vllm-project/vllm/issues/32656 Credits Pengyu Ding (Infra Security, Ant Group) Ziteng Xu (Infra Security, Ant Group)

LangChain has incomplete f-string validation in prompt templates

LangChain's f-string prompt-template validation was incomplete in two respects. First, some prompt template classes accepted f-string templates and formatted them without enforcing the same attribute-access validation as PromptTemplate. In particular, DictPromptTemplate and ImagePromptTemplate could accept templates containing attribute access or indexing expressions and subsequently evaluate those expressions during formatting. Examples of the affected shape include: ``python "{message.additional_kwargs[secret]}" "https://example.com/{image.__class__.__name__}.png" ` Second, f-string validation based on parsed top-level field names did not reject nested replacement fields inside format specifiers. For example: `python "{name:{name.__class__.__name__}}" ` In this pattern, the nested replacement field appears in the format specifier rather than in the top-level field name. As a result, earlier validation based on parsed field names did not reject the template even though Python formatting would still attempt to resolve the nested expression at runtime. Affected usage This issue is only relevant for applications that accept untrusted template strings, rather than only untrusted template variable values. In addition, practical impact depends on what objects are passed into template formatting: If applications only format simple values such as strings and numbers, impact is limited and may only result in formatting errors. If applications format richer Python objects, attribute access and indexing may interact with internal object state during formatting. In many deployments, these conditions are not commonly present together. Applications that allow end users to author arbitrary templates often expose only a narrow set of simple template variables, while applications that work with richer internal Python objects often keep template structure under developer control. As a result, the highest-impact scenario is plausible but is not representative of all LangChain applications. Applications that use hardcoded templates or that only allow users to provide variable values are not affected by this issue. Impact The direct issue in DictPromptTemplate and ImagePromptTemplate allowed attribute access and indexing expressions to survive template construction and then be evaluated during formatting. When richer Python objects were passed into formatting, this could expose internal fields or nested data to prompt output, model context, or logs. The nested format-spec issue is narrower in scope. It bypassed the intended validation rules for f-string templates, but in simple cases it results in an invalid format specifier error rather than direct disclosure. Accordingly, its practical impact is lower than that of direct top-level attribute traversal. Overall, the practical severity depends on deployment. Meaningful confidentiality impact requires attacker control over the template structure itself, and higher impact further depends on the surrounding application passing richer internal Python objects into formatting. Fix The fix consists of two changes. First, LangChain now applies f-string safety validation consistently to DictPromptTemplate and ImagePromptTemplate, so templates containing attribute access or indexing expressions are rejected during construction and deserialization. Second, LangChain now rejects nested replacement fields inside f-string format specifiers. Concretely, LangChain validates parsed f-string fields and raises an error for: variable names containing attribute access or indexing syntax such as . or [] format specifiers containing { or } This blocks templates such as: `python "{message.additional_kwargs[secret]}" "https://example.com/{image.__class__.__name__}.png" "{name:{name.__class__.__name__}}" ` The fix preserves ordinary f-string formatting features such as standard format specifiers and conversions, including examples like: `python "{value:.2f}" "{value:>10}" "{value!r}" `` In addition, the explicit template-validation path now applies the same structural f-string checks before performing placeholder validation, ensuring that the security checks and validation checks remain aligned.

OWASP A03LLM01 · Prompt InjectionOWASP LLM
Get guardrail →
2 rules

HuggingFace Transformers allows for arbitrary code execution in the `Trainer` class

A vulnerability in the HuggingFace Transformers library, specifically in the Trainer class, allows for arbitrary code execution. The _load_rng_state() method in src/transformers/trainer.py at line 3059 calls torch.load() without the weights_only=True parameter. This issue affects all versions of the library supporting torch>=2.2 when used with PyTorch versions below 2.6, as the safe_globals() context manager provides no protection in these versions. An attacker can exploit this vulnerability by supplying a malicious checkpoint file, such as rng_state.pth, which can execute arbitrary code when loaded. The issue is resolved in version v5.0.0rc3.

OWASP A08LLM05 · Supply ChainOWASP LLM
Get guardrail →

vLLM: Denial of Service via Unbounded Frame Count in video/jpeg Base64 Processing

Summary The VideoMediaIO.load_base64() method at vllm/multimodal/media/video.py:51-62 splits video/jpeg data URLs by comma to extract individual JPEG frames, but does not enforce a frame count limit. The num_frames parameter (default: 32), which is enforced by the load_bytes() code path at line 47-48, is completely bypassed in the video/jpeg base64 path. An attacker can send a single API request containing thousands of comma-separated base64-encoded JPEG frames, causing the server to decode all frames into memory and crash with OOM. Details Vulnerable code ``python video.py:51-62 def load_base64(self, media_type: str, data: str) -> tuple[npt.NDArray, dict[str, Any]]: if media_type.lower() == "video/jpeg": load_frame = partial(self.image_io.load_base64, "image/jpeg") return np.stack( [np.asarray(load_frame(frame_data)) for frame_data in data.split(",")] # ^^^^^^^^^^ # Unbounded split — no frame count limit ), {} return self.load_bytes(base64.b64decode(data)) ` The load_bytes() path (line 47-48) properly delegates to a video loader that respects self.num_frames (default 32). The load_base64("video/jpeg", ...) path bypasses this limit entirely — data.split(",") produces an unbounded list and every frame is decoded into a numpy array. video/jpeg is part of vLLM's public API video/jpeg is a vLLM-specific MIME type, not IANA-registered. However it is part of the public API surface: encode_video_url() at vllm/multimodal/utils.py:96-108 generates data:video/jpeg;base64,... URLs Official test suites at tests/entrypoints/openai/test_video.py:62 and tests/entrypoints/test_chat_utils.py:153 both use this format Memory amplification Each JPEG frame decodes to a full numpy array. For 640x480 RGB images, each frame is ~921 KB decoded. 5000 frames = ~4.6 GB. np.stack() then creates an additional copy. The compressed JPEG payload is small (~100 KB for 5000 frames) but decompresses to gigabytes. Data flow ` POST /v1/chat/completions → chat_utils.py:1434 video_url type → mm_parser.parse_video() → chat_utils.py:872 parse_video() → self._connector.fetch_video() → connector.py:295 fetch_video() → load_from_url(url, self.video_io) → connector.py:91 _load_data_url(): url_spec.path.split(",", 1) → media_type = "video/jpeg" → data = "<frame1>,<frame2>,...,<frame10000>" → connector.py:100 media_io.load_base64("video/jpeg", data) → video.py:54 data.split(",") ← UNBOUNDED → video.py:55-57 all frames decoded into numpy arrays → video.py:56 np.stack([...]) ← massive combined array → OOM ` connector.py:91 uses split(",", 1) which splits on only the first comma. All remaining commas stay in data and are later split by video.py:54. Comparison with existing protections | Code Path | Frame Limit | File | |-----------|-------------|------| | load_bytes() (binary video) | Yes — num_frames (default 32) | video.py:46-49 | | load_base64("video/jpeg", ...) | No — unlimited data.split(",")` | video.py:51-62 |

vLLM: Server-Side Request Forgery (SSRF) in `download_bytes_from_url `

Summary A Server Side Request Forgery (SSRF) vulnerability in download_bytes_from_url allows any actor who can control batch input JSON to make the vLLM batch runner issue arbitrary HTTP/HTTPS requests from the server, without any URL validation or domain restrictions. This can be used to target internal services (e.g. cloud metadata endpoints or internal HTTP APIs) reachable from the vLLM host. ------ Details Vulnerable component The vulnerable logic is in the batch runner entrypoint vllm/entrypoints/openai/run_batch.py, function download_bytes_from_url: `` run_batch.py Lines 442-482 async def download_bytes_from_url(url: str) -> bytes: """ Download data from a URL or decode from a data URL. Args: url: Either an HTTP/HTTPS URL or a data URL (data:...;base64,...) Returns: Data as bytes """ parsed = urlparse(url) # Handle data URLs (base64 encoded) if parsed.scheme == "data": # Format: data:...;base64,<base64_data> if "," in url: header, data = url.split(",", 1) if "base64" in header: return base64.b64decode(data) else: raise ValueError(f"Unsupported data URL encoding: {header}") else: raise ValueError(f"Invalid data URL format: {url}") # Handle HTTP/HTTPS URLs elif parsed.scheme in ("http", "https"): async with ( aiohttp.ClientSession() as session, session.get(url) as resp, ): if resp.status != 200: raise Exception( f"Failed to download data from URL: {url}. Status: {resp.status}" ) return await resp.read() else: raise ValueError( f"Unsupported URL scheme: {parsed.scheme}. " "Supported schemes: http, https, data" ) ` Key properties: The function only parses the URL to dispatch on the scheme (data, http, https). For http / https, it directly calls session.get(url) on the provided string. There is no validation of: hostname or IP address, whether the target is internal or external, port number, path, query, or redirect target. This is in contrast to the multimodal media path (MediaConnector), which implements an explicit domain allowlist. download_bytes_from_url does not reuse that protection. URL controllability The url argument is fully controlled by batch input JSON via the file_url field of BatchTranscriptionRequest / BatchTranslationRequest. 1. Batch request body type: ` run_batch.py Line 67-80 class BatchTranscriptionRequest(TranscriptionRequest): """ Batch transcription request that uses file_url instead of file. This class extends TranscriptionRequest but replaces the file field with file_url to support batch processing from audio files written in JSON format. """ file_url: str = Field( ..., description=( "Either a URL of the audio or a data URL with base64 encoded audio data. " ), ) ` ` run_batch.py Line 98-111 class BatchTranslationRequest(TranslationRequest): """ Batch translation request that uses file_url instead of file. This class extends TranslationRequest but replaces the file field with file_url to support batch processing from audio files written in JSON format. """ file_url: str = Field( ..., description=( "Either a URL of the audio or a data URL with base64 encoded audio data. " ), ) ` There is no restriction on the domain, IP, or port of file_url in these models. 1. Batch input is parsed directly from the batch file: ` run_batch.py Line 139-179 class BatchRequestInput(OpenAIBaseModel): ... url: str body: BatchRequestInputBody @field_validator("body", mode="plain") @classmethod def check_type_for_url(cls, value: Any, info: ValidationInfo): url: str = info.data["url"] ... if url == "/v1/audio/transcriptions": return BatchTranscriptionRequest.model_validate(value) if url == "/v1/audio/translations": return BatchTranslationRequest.model_validate(value) ` ` run_batch.py Line 770-781 logger.info("Reading batch from %s...", args.input_file) # Submit all requests in the file to the engine "concurrently". response_futures: list[Awaitable[BatchRequestOutput]] = [] for request_json in (await read_file(args.input_file)).strip().split("\n"): # Skip empty lines. request_json = request_json.strip() if not request_json: continue request = BatchRequestInput.model_validate_json(request_json) ` The batch runner reads each line of the input file (args.input_file), parses it as JSON, and constructs a BatchTranscriptionRequest / BatchTranslationRequest. Whatever file_url appears in that JSON line becomes batch_request_body.file_url. 1. file_url is passed directly into download_bytes_from_url: ` run_batch.py Line 610-623 def wrapper(handler_fn: Callable): async def transcription_wrapper( batch_request_body: (BatchTranscriptionRequest | BatchTranslationRequest), ) -> ( TranscriptionResponse | TranscriptionResponseVerbose | TranslationResponse | TranslationResponseVerbose | ErrorResponse ): try: # Download data from URL audio_data = await download_bytes_from_url(batch_request_body.file_url) ` So the data flow is: 1. Attacker supplies JSON line in the batch input file with arbitrary body.file_url. 2. BatchRequestInput / BatchTranscriptionRequest / BatchTranslationRequest parse that JSON and store file_url verbatim. 3. make_transcription_wrapper calls download_bytes_from_url(batch_request_body.file_url). 4. download_bytes_from_url’s HTTP/HTTPS branch issues aiohttp.ClientSession().get(url) to that attacker-controlled URL with no further validation. This is a classic SSRF pattern: a server-side component makes arbitrary HTTP requests to a URL string taken from untrusted input. Comparison with safer code The project already contains a safer URL-handling path for multimodal media in vllm/multimodal/media/connector.py, which demonstrates the intent to mitigate SSRF via domain allowlists and URL normalization: ` connector.py Lines 169-189 def load_from_url( self, url: str, media_io: MediaIO[_M], *, fetch_timeout: int | None = None, ) -> _M: # type: ignore[type-var] url_spec = parse_url(url) if url_spec.scheme and url_spec.scheme.startswith("http"): self._assert_url_in_allowed_media_domains(url_spec) connection = self.connection data = connection.get_bytes( url_spec.url, timeout=fetch_timeout, allow_redirects=envs.VLLM_MEDIA_URL_ALLOW_REDIRECTS, ) return media_io.load_bytes(data) ` and: ` connector.py Lines 158-167 def _assert_url_in_allowed_media_domains(self, url_spec: Url) -> None: if ( self.allowed_media_domains and url_spec.hostname not in self.allowed_media_domains ): raise ValueError( f"The URL must be from one of the allowed domains: " f"{self.allowed_media_domains}. Input URL domain: " f"{url_spec.hostname}" ) ` download_bytes_from_url` does not reuse this allowlist or any equivalent validation, even though it also fetches user-provided URLs.

OWASP A10LLM02 · Insecure OutputOWASP LLM
Get guardrail →

vLLM: Unauthenticated OOM Denial of Service via Unbounded `n` Parameter in OpenAI API Server

Summary A Denial of Service vulnerability exists in the vLLM OpenAI-compatible API server. Due to the lack of an upper bound validation on the n parameter in the ChatCompletionRequest and CompletionRequest Pydantic models, an unauthenticated attacker can send a single HTTP request with an astronomically large n value. This completely blocks the Python asyncio event loop and causes immediate Out-Of-Memory crashes by allocating millions of request object copies in the heap before the request even reaches the scheduling queue. Details The root cause of this vulnerability lies in the missing upper bound checks across the request parsing and asynchronous scheduling layers: 1. Protocol Layer: In vllm/entrypoints/openai/chat_completion/protocol.py, the n parameter is defined simply as an integer without any pydantic.Field constraints for an upper bound. ``python class ChatCompletionRequest(OpenAIBaseModel): # Ordered by official OpenAI API documentation # https://platform.openai.com/docs/api/reference/chat/create messages: list[ChatCompletionMessageParam] model: str | None = None frequency_penalty: float | None = 0.0 logit_bias: dict[str, float] | None = None logprobs: bool | None = False top_logprobs: int | None = 0 max_tokens: int | None = Field( default=None, deprecated="max_tokens is deprecated in favor of " "the max_completion_tokens field", ) max_completion_tokens: int | None = None n: int | None = 1 presence_penalty: float | None = 0.0 ` 1. SamplingParams Layer (Incomplete Validation): When the API request is converted to internal SamplingParams in vllm/sampling_params.py, the _verify_args method only checks the lower bound (self.n < 1), entirely omitting an upper bounds check. `python def _verify_args(self) -> None: if not isinstance(self.n, int): raise ValueError(f"n must be an int, but is of type {type(self.n)}") if self.n < 1: raise ValueError(f"n must be at least 1, got {self.n}.") ` 1. Engine Layer (The OOM Trigger): When the malicious request reaches the core engine (vllm/v1/engine/async_llm.py), the engine attempts to fan out the request n times to generate identical independent sequences within a synchronous loop. `python # Fan out child requests (for n>1). parent_request = ParentRequest(request) for idx in range(parent_params.n): request_id, child_params = parent_request.get_child_info(idx) child_request = request if idx == parent_params.n - 1 else copy(request) child_request.request_id = request_id child_request.sampling_params = child_params await self._add_request( child_request, prompt_text, parent_request, idx, queue ) return queue ` Because Python's asyncio runs on a single thread and event loop, this monolithic for-loop monopolizes the CPU thread. The server stops responding to all other connections (including liveness probes). Simultaneously, the memory allocator is overwhelmed by cloning millions of request object instances via copy(request), driving the host's Resident Set Size (RSS) up by gigabytes per second until the OS OOM-killer terminates the vLLM process. Impact Vulnerability Type: Resource Exhaustion / Denial of Service Impacted Parties: Any individual or organization hosting a public-facing vLLM API server (vllm.entrypoints.openai.api_server`), which happens to be the primary entrypoint for OpenAI-compatible setups. SaaS / AI-as-a-Service platforms acting as reverse proxies sitting in front of vLLM without strict HTTP body payload validation or rate limitations. Because this vulnerability exploits the control plane rather than the data plane, an unauthenticated remote attacker can achieve a high success rate in taking down production inference hosts with a single HTTP request. This effectively circumvents any hardware-level capacity planning and conventional bandwidth stress limitations.

Claude SDK for Python: Memory Tool Path Validation Race Condition Allows Sandbox Escape

The async local filesystem memory tool in the Anthropic Python SDK validated that model-supplied paths resolved inside the sandboxed memory directory, but then returned the unresolved path for subsequent file operations. A local attacker able to write to the memory directory could retarget a symlink between validation and use, causing reads or writes to escape the sandbox. The synchronous memory tool implementation was not affected. Users on the affected versions are advised to update to the latest version. Claude SDK for Python thanks hackerone.com/kasthelord for reporting this issue!

OWASP A01OWASP LLM
Get guardrail →

Claude SDK for Python has Insecure Default File Permissions in Local Filesystem Memory Tool

The local filesystem memory tool in the Anthropic Python SDK created memory files with mode 0o666, leaving them world-readable on systems with a standard umask and world-writable in environments with a permissive umask such as many Docker base images. A local attacker on a shared host could read persisted agent state, and in containerized deployments could modify memory files to influence subsequent model behavior. Both the synchronous and asynchronous memory tool implementations were affected. Users on the affected versions are advised to update to the latest version. Claude SDK for Python thanks lucasfutures on HackerOne for the report.

OWASP A05OWASP LLM
Get guardrail →

LangChain Core has Path Traversal vulnerabilites in legacy `load_prompt` functions

Summary Multiple functions in langchain_core.prompts.loading read files from paths embedded in deserialized config dicts without validating against directory traversal or absolute path injection. When an application passes user-influenced prompt configurations to load_prompt() or load_prompt_from_config(), an attacker can read arbitrary files on the host filesystem, constrained only by file-extension checks (.txt for templates, .json/.yaml for examples). Note: The affected functions (load_prompt, load_prompt_from_config, and the .save() method on prompt classes) are undocumented legacy APIs. They are superseded by the dumpd/dumps/load/loads serialization APIs in langchain_core.load, which do not perform filesystem reads and use an allowlist-based security model. As part of this fix, the legacy APIs have been formally deprecated and will be removed in 2.0.0. Affected component Package: langchain-core File: langchain_core/prompts/loading.py Affected functions: _load_template(), _load_examples(), _load_few_shot_prompt() Severity High The score reflects the file-extension constraints that limit which files can be read. Vulnerable code paths | Config key | Loaded by | Readable extensions | |---|---|---| | template_path, suffix_path, prefix_path | _load_template() | .txt | | examples (when string) | _load_examples() | .json, .yaml, .yml | | example_prompt_path | _load_few_shot_prompt() | .json, .yaml, .yml | None of these code paths validated the supplied path against absolute path injection or .. traversal sequences before reading from disk. Impact An attacker who controls or influences the prompt configuration dict can read files outside the intended directory: .txt files: cloud-mounted secrets (/mnt/secrets/api_key.txt), requirements.txt, internal system prompts .json/.yaml files: cloud credentials (~/.docker/config.json, ~/.azure/accessTokens.json), Kubernetes manifests, CI/CD configs, application settings This is exploitable in applications that accept prompt configs from untrusted sources, including low-code AI builders and API wrappers that expose load_prompt_from_config(). Proof of concept ``python from langchain_core.prompts.loading import load_prompt_from_config Reads /tmp/secret.txt via absolute path injection config = { "_type": "prompt", "template_path": "/tmp/secret.txt", "input_variables": [], } prompt = load_prompt_from_config(config) print(prompt.template) # file contents disclosed Reads ../../etc/secret.txt via directory traversal config = { "_type": "prompt", "template_path": "../../etc/secret.txt", "input_variables": [], } prompt = load_prompt_from_config(config) Reads arbitrary .json via few-shot examples config = { "_type": "few_shot", "examples": "../../../../.docker/config.json", "example_prompt": { "_type": "prompt", "input_variables": ["input", "output"], "template": "{input}: {output}", }, "prefix": "", "suffix": "{query}", "input_variables": ["query"], } prompt = load_prompt_from_config(config) ` Mitigation Update langchain-core to >= 1.2.22. The fix adds path validation that rejects absolute paths and .. traversal sequences by default. An allow_dangerous_paths=True keyword argument is available on load_prompt() and load_prompt_from_config() for trusted inputs. As described above, these legacy APIs have been formally deprecated. Users should migrate to dumpd/dumps/load/loads from langchain_core.load`. Credit jiayuqi7813 reporter VladimirEliTokarev reporter Rickidevs reporter Kenneth Cox (cczine@gmail.com) reporter

OWASP A01OWASP LLM
Get guardrail →

vLLM has Hardcoded Trust Override in Model Files Enables RCE Despite Explicit User Opt-Out

Summary Two model implementation files hardcode trust_remote_code=True when loading sub-components, bypassing the user's explicit --trust-remote-code=False security opt-out. This enables remote code execution via malicious model repositories even when the user has explicitly disabled remote code trust. ### Details Affected files (latest main branch): 1. vllm/model_executor/models/nemotron_vl.py:430 ``python vision_model = AutoModel.from_config(config.vision_config, trust_remote_code=True) ` 2. vllm/model_executor/models/kimi_k25.py:177 `python cached_get_image_processor(self.ctx.model_config.model, trust_remote_code=True) `` Both pass a hardcoded trust_remote_code=True to HuggingFace API calls, overriding the user's global --trust-remote-code=False setting. Relation to prior CVEs: CVE-2025-66448 fixed auto_map resolution in vllm/transformers_utils/config.py (config loading path) CVE-2026-22807 fixed broader auto_map at startup Both fixes are present in the current code. These hardcoded instances in model files survived both patches — different code paths. Impact Remote code execution. An attacker can craft a malicious model repository that executes arbitrary Python code when loaded by vLLM, even when the user has explicitly set --trust-remote-code=False. This undermines the security guarantee that trust_remote_code=False is intended to provide. Remediation: Replace hardcoded trust_remote_code=True with self.config.model_config.trust_remote_code in both files. Raise a clear error if the model component requires remote code but the user hasn't opted in.

vLLM has SSRF Protection Bypass

Summary The SSRF protection fix for https://github.com/vllm-project/vllm/security/advisories/GHSA-qh4c-xf7m-gxfc can be bypassed in the load_from_url_async method due to inconsistent URL parsing behavior between the validation layer and the actual HTTP client. Affected Component File: vllm/connections.py Function: load_from_url_async Vulnerability Details Root Cause The SSRF fix uses urllib3.util.parse_url() to validate and extract the hostname from user-provided URLs. However, load_from_url_async uses aiohttp for making the actual HTTP requests, and aiohttp internally uses the yarl library for URL parsing. These two URL parsers handle backslash characters (\) differently: | Parser | Input URL | Parsed Host | Parsed Path | Behavior | |--------|-----------|-------------|-------------|----------| | urllib3.parse_url() | https://httpbin.org\@evil.com/ | httpbin.org | /%5C@evil.com/ | URL-encodes \ as %5C, treats \@evil.com/ as part of the path | | yarl (via aiohttp) | https://httpbin.org\@evil.com/ | evil.com | / | Treats \ as part of userinfo (user: httpbin.org\), the @ acts as the userinfo/host separator | Attack Scenario ``python Attacker provides this URL malicious_url = "https://httpbin.org\\@evil.com/" 1. Validation layer (urllib3.parse_url) parsed = urllib3.util.parse_url(malicious_url) parsed.host == "httpbin.org" ✅ Passes validation 2. Actual request (aiohttp with yarl) async with aiohttp.ClientSession() as session: async with session.get(malicious_url) as response: # Request actually goes to evil.com! ❌ Bypass! ` Why This Happens 1. yarl: Interprets httpbin.org\ as the userinfo component, and @ as the userinfo/host separator, so the URL is parsed as user=httpbin.org\, host=evil.com, path=/ 2. urllib3: URL-encodes the backslash as %5C, so \@evil.com/ becomes /%5C@evil.com/ which is treated as part of the path, leaving host=httpbin.org` This inconsistency allows an attacker to: Bypass the hostname allowlist check Access arbitrary internal/external services Perform full SSRF attacks Fixes https://github.com/vllm-project/vllm/pull/34743

OWASP A10LLM02 · Insecure OutputOWASP LLM
Get guardrail →

Gradio has SSRF via Malicious `proxy_url` Injection in `gr.load()` Config Processing

Summary A Server-Side Request Forgery (SSRF) vulnerability in Gradio allows an attacker to make arbitrary HTTP requests from a victim's server by hosting a malicious Gradio Space. When a victim application uses gr.load() to load an attacker-controlled Space, the malicious proxy_url from the config is trusted and added to the allowlist, enabling the attacker to access internal services, cloud metadata endpoints, and private networks through the victim's infrastructure. Details The vulnerability exists in Gradio's config processing flow when loading external Spaces: 1. Config Fetching (gradio/external.py:630): gr.load() calls Blocks.from_config() which fetches and processes the remote Space's configuration. 2. Proxy URL Trust (gradio/blocks.py:1231-1233): The proxy_url from the untrusted config is added directly to self.proxy_urls: ``python if config.get("proxy_url"): self.proxy_urls.add(config["proxy_url"]) ` 3. Built-in Proxy Route (gradio/routes.py:1029-1031): Every Gradio app automatically exposes a /proxy={url_path} endpoint: `python @router.get("/proxy={url_path:path}", dependencies=[Depends(login_check)]) async def reverse_proxy(url_path: str): ` 4. Host-based Validation (gradio/routes.py:365-368): The validation only checks if the URL's host matches any trusted proxy_url host: `python is_safe_url = any( url.host == httpx.URL(root).host for root in self.blocks.proxy_urls ) ` An attacker can set proxy_url to http://169.254.169.254/ (AWS metadata) or any internal service, and the victim's server will proxy requests to those endpoints. PoC Full PoC: https://gist.github.com/logicx24/8d4c1aaa4e70f85d0d0fba06a463f2d6 1. Attacker creates a malicious Gradio Space that returns this config: `python { "mode": "blocks", "components": [...], "proxy_url": "http://169.254.169.254/" # AWS metadata endpoint } ` 2. Victim loads the malicious Space: `python import gradio as gr demo = gr.load("attacker/malicious-space") demo.launch(server_name="0.0.0.0", server_port=7860) ` 3. Attacker exploits the proxy: `bash Fetch AWS credentials through victim's server curl "http://victim:7860/gradio_api/proxy=http://169.254.169.254/latest/meta-data/iam/security-credentials/role-name" ` Impact Who is impacted: Any Gradio application that uses gr.load()` to load external/untrusted Spaces HuggingFace Spaces that compose or embed other Spaces Enterprise deployments where Gradio apps have access to internal networks Attack scenarios: Cloud credential theft: Access AWS/GCP/Azure metadata endpoints to steal IAM credentials Internal service access: Reach databases, admin panels, and APIs on private networks Network reconnaissance: Map internal infrastructure through the victim Data exfiltration: Access sensitive internal APIs and services

OWASP A10LLM02 · Insecure OutputOWASP LLM
Get guardrail →

Gradio has an Open Redirect in its OAuth Flow

Summary The _redirect_to_target() function in Gradio's OAuth flow accepts an unvalidated _target_url query parameter, allowing redirection to arbitrary external URLs. This affects the /logout and /login/callback endpoints on Gradio apps with OAuth enabled (i.e. apps running on Hugging Face Spaces with gr.LoginButton). Details ``python def _redirect_to_target(request, default_target="/"): target = request.query_params.get("_target_url", default_target) return RedirectResponse(target) # No validation `` An attacker can craft a URL like https://my-space.hf.space/logout?_target_url=https://evil.com/phishing that redirects the user to an external site after logout. Because the URL originates from a trusted hf.space domain, users are more likely to trust the link. Impact Phishing — an attacker can use the trusted domain to redirect users to a malicious site. No direct data exposure or server-side impact. ## Fix The _target_url parameter is now sanitized to only use the path, query, and fragment, stripping any scheme or host.

OWASP A01OWASP A02LLM02 · Insecure OutputLLM06 · Sensitive Info Disclosure
Get guardrail →

Gradio is Vulnerable to Absolute Path Traversal on Windows with Python 3.13+

Summary Gradio apps running on Window with Python 3.13+ are vulnerable to an absolute path traversal issue that enables unauthenticated attackers to read arbitrary files from the file system. Details Python 3.13+ changed the definition of os.path.isabs so that root-relative paths like /windows/win.ini on Windows are no longer considered absolute paths, resulting in a vulnerability in Gradio's logic for joining paths safely. This can be exploited by unauthenticated attackers to read arbitrary files from the Gradio server, even when Gradio is set up with authentication. PoC `` % curl http://10.10.10.10:7860/static//windows/win.ini ; for 16-bit app support [fonts] [extensions] [mci extensions] [files] [Mail] MAPI=1 `` Impact Arbitrary file read in the context of the Windows user running Gradio.

OWASP A01OWASP LLM
Get guardrail →

Pydantic AI has Stored XSS via Path Traversal in Web UI CDN URL

Summary A Path Traversal vulnerability in the Pydantic AI web UI allows an attacker to serve arbitrary JavaScript in the context of the application by crafting a malicious URL. If a victim clicks the link or visits it via an iframe, attacker-controlled code executes in their browser, enabling theft of chat history and other client-side data. This vulnerability only affects applications that use: Agent.to_web to serve a chat interface clai web to serve a chat interface from the CLI These are typically run locally (on localhost), but may also be deployed on a remote server. Description The web UI serves its frontend HTML by fetching it from a CDN. In affected versions, the CDN URL is constructed using a version query parameter from the request URL. This parameter is not validated, allowing path traversal sequences that cause the server to fetch and serve attacker-controlled HTML/JavaScript from an arbitrary source on the same CDN, instead of the legitimate chat UI package. Who Is Affected Projects are affected if your application uses Agent.to_web or clai web to serve the Pydantic AI chat interface. Attack Scenario 1. An attacker crafts a URL pointing to the victim's Pydantic AI web UI instance (either localhost with the known port, or a remote server endpoint) with a malicious version query parameter containing path traversal sequences. 2. The attacker gets the victim to visit this URL — directly via a link, through a redirect, or by embedding it in an iframe. 3. When the victim's browser loads the page, the server fetches and serves attacker-controlled HTML/JavaScript instead of the legitimate chat UI. 4. The attacker's JavaScript executes in the victim's browser in the context of the Pydantic AI web application, with access to: Chat history stored in localStorage (all user messages and AI responses) Session cookies that are not set as HttpOnly, if authentication middleware is configured Remediation Upgrade to Patched Version Upgrade to the patched version or later. The fix removes the user-controllable version parameter entirely. The CDN URL is now hardcoded at startup and cannot be influenced by request parameters. A new html_source parameter is available on Agent.to_web and create_web_app for applications that need to customize the UI source (e.g., for enterprise environments, offline usage, or custom UI builds). This parameter is only settable in application code, not via query parameters.

OWASP A01OWASP A03OWASP LLM
Get guardrail →

Pydantic AI has Server-Side Request Forgery (SSRF) in URL Download Handling

Summary A Server-Side Request Forgery (SSRF) vulnerability exists in Pydantic AI's URL download functionality. When applications accept message history from untrusted sources, attackers can include malicious URLs that cause the server to make HTTP requests to internal network resources, potentially accessing internal services or cloud credentials. This vulnerability only affects applications that accept message history from external users, such as those using: Agent.to_web or clai web to serve a chat interface VercelAIAdapter for Vercel AI SDK integration AGUIAdapter or Agent.to_ag_ui for AG-UI protocol integration Custom APIs that accept message history from user input Applications that only use hardcoded or developer-controlled URLs are not affected. Description The download_item() helper function downloads content from URLs without validating that the target is a public internet address. When user-supplied message history contains URLs, attackers can: 1. Access internal services: Request http://127.0.0.1, localhost, or private IP ranges (10.x.x.x, 172.16.x.x, 192.168.x.x) 2. Steal cloud credentials: Access cloud metadata endpoints (AWS IMDSv1 at 169.254.169.254, GCP, Azure, Alibaba Cloud) 3. Scan internal networks: Enumerate internal hosts and ports Who Is Affected You are affected if your application: 1. Uses Agent.to_web or clai web - The web interface accepts file attachments via the Vercel AI Data Stream Protocol, where users can provide arbitrary URLs through chat messages. 2. Uses VercelAIAdapter - Chat interfaces built with Vercel AI SDK allow users to submit messages containing URLs that are processed server-side. 3. Uses AGUIAdapter or Agent.to_ag_ui - The AG-UI protocol allows users to provide file references with URLs as part of agent interactions. 4. Exposes a custom API accepting message history - Any endpoint that accepts message history or ImageUrl, AudioUrl, VideoUrl, DocumentUrl objects from user input. Attack Scenario Via chat interface, an attacker submits a message with a file attachment pointing to an internal resource: ``json { "role": "user", "parts": [ {"type": "file", "mediaType": "image/png", "url": "http://169.254.169.254/latest/meta-data/iam/security-credentials/"} ] } ` Affected Model Integrations Multiple model integrations download URL content in certain conditions: | Provider | Downloaded Types | |----------|------------------| | OpenAIChatModel | AudioUrl, DocumentUrl | | AnthropicModel | DocumentUrl (text/plain) | | GoogleModel (GLA) | All URL types (except YouTube and Files API URLs) | | XaiModel | DocumentUrl | | BedrockConverseModel | ImageUrl, DocumentUrl, VideoUrl (non-S3 URLs) | | OpenRouterModel | AudioUrl | Remediation Upgrade to Patched Version Upgrade to the patched version or later. The fix adds comprehensive SSRF protection: Blocks private/internal IP addresses by default Always blocks cloud metadata endpoints (even with allow-local) Only allows http:// and https:// protocols Resolves hostnames before requests to prevent DNS rebinding Validates each redirect target New force_download='allow-local' Option If an application legitimately needs to access local/private network resources (e.g., in a fully trusted internal environment), it can explicitly opt in: `python from pydantic_ai import ImageUrl Default behavior: private IPs are blocked ImageUrl(url="http://internal-service/image.png") # Raises ValueError Opt-in to allow local access (use with caution) ImageUrl(url="http://internal-service/image.png", force_download='allow-local') ` Important: Cloud metadata endpoints (169.254.169.254, fd00:ec2::254, 100.100.100.200) are always blocked, even with allow-local. Workaround for Older Versions If a project cannot upgrade immediately, use a history processor to filter out URLs targeting local/private addresses: `python import ipaddress import socket from urllib.parse import urlparse from pydantic_ai import Agent, ModelMessage, ModelRequest from pydantic_ai.messages import AudioUrl, DocumentUrl, ImageUrl, VideoUrl def is_private_url(url: str) -> bool: """Check if a URL targets a private/internal IP address.""" try: parsed = urlparse(url) hostname = parsed.hostname if not hostname: return True # Invalid URL, block it # Resolve hostname to IP ip_str = socket.gethostbyname(hostname) ip = ipaddress.ip_address(ip_str) # Block private, loopback, and link-local addresses return ip.is_private or ip.is_loopback or ip.is_link_local except (socket.gaierror, ValueError): return True # DNS resolution failed, block it def filter_private_urls(messages: list[ModelMessage]) -> list[ModelMessage]: """Remove URL parts that target private/internal addresses.""" url_types = (ImageUrl, AudioUrl, VideoUrl, DocumentUrl) filtered = [] for msg in messages: if isinstance(msg, ModelRequest): safe_parts = [ part for part in msg.parts if not (isinstance(part, url_types) and is_private_url(part.url)) ] if safe_parts: filtered.append(ModelRequest(parts=safe_parts)) else: filtered.append(msg) return filtered Apply the filter to your agent agent = Agent('openai:gpt-5', history_processors=[filter_private_urls]) ` Technical Details of the Fix The fix introduces a new _ssrf.py module with comprehensive protection: 1. Protocol validation: Only http:// and https:// allowed 2. DNS resolution before request: Prevents DNS rebinding attacks 3. Private IP blocking (by default): 127.0.0.0/8, ::1/128 (loopback) 10.0.0.0/8, 172.16.0.0/12, 192.168.0.0/16 (private) 169.254.0.0/16, fe80::/10 (link-local) 100.64.0.0/10 (CGNAT) fc00::/7 (unique local) 2002::/16 (6to4, can embed private IPv4) 4. Cloud metadata always blocked: 169.254.169.254, fd00:ec2::254, 100.100.100.200` 5. Safe redirect handling: Each redirect validated before following (max 10)

OWASP A10LLM02 · Insecure OutputOWASP LLM
Get guardrail →

Showing 120 of 152 threats