Threat Intelligence

Live CVE feed

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

66threats · AI / LLM Apps · High· page 1/4
Get guardrails →

Transformers save_pretrained path traversal allows arbitrary file writes through chat template names

A vulnerability in huggingface/transformers versions < 5.10.0 allows an attacker to perform arbitrary file writes via path traversal. The issue resides in the save_pretrained() methods of PreTrainedTokenizerBase and ProcessorMixin, where keys from the chat_template dictionary are used directly as filenames without proper validation. An attacker can exploit this by publishing a malicious Hugging Face Hub repository with a crafted tokenizer_config.json file. When a victim downloads and saves the tokenizer or processor, the attacker-controlled keys can escape the intended save directory, enabling arbitrary file writes with attacker-controlled content. This vulnerability affects multiple processors inheriting from ProcessorMixin, including Idefics, Florence, Gemma, Phi, and Qwen-VL.

OWASP A01OWASP LLM
Get guardrail →

vLLM: ReDoS via structured_outputs.regex compiled without timeout in xgrammar and outlines backends

Summary The structured_outputs.regex API parameter passes a user-supplied regex string directly to grammar compiler backends with no compilation timeout. In the xgrammar backend, the string reaches compile_regex() with no guard. In the outlines backend, validate_regex_is_buildable() blocks structural issues (lookarounds, backreferences) but provides zero protection against exponential DFA state-space explosion. Patterns like (a+)+b pass all checks and hang the inference worker. Root Cause backend_xgrammar.py:91 — no timeout: ``python ctx = self.compiler.compile_regex(grammar_spec) ` backend_outlines.py:299–330 — structural checks only, no complexity analysis: `python def validate_regex_is_buildable(regex: str) -> None: sre_parse.parse(regex) # AST parse only — does not detect exponential patterns _check_unsupported(...) # blocks lookarounds/backrefs, not nested quantifiers ` backend_outlines.py:64 — no timeout: `python oc.Index(regex_string, vocabulary.inner) ` Impact Denial of service — one request with an adversarial regex pattern hangs an inference worker indefinitely. Remediation Wrap compile_regex() and oc.Index() calls in a thread with a deadline (e.g., 5 seconds). Add complexity analysis to validate_regex_is_buildable()` to detect nested quantifier patterns before compilation.

vLLM has Remote DoS via Invalid Recovered Token Reinjection

Summary A frontend-legal multi-request speculative workload can make vLLM produce an out-of-vocabulary recovered token equal to vocab_size, convert that value to -1 when choosing the next live token for a request, and then feed that -1 back into the next drafter input ids. On Qwen3 GPTQ this reaches the worker-side drafting / attention path and crashes the engine with a GPU device-side assert. The same issue is reachable through the public gRPC request surface by sending a specific overlapping Generate / Abort sequence. Impact A remote client that can send public gRPC generation requests can crash the shared vLLM engine worker The triggering request sequence aborts concurrent requests and prevents later requests from completing until the worker is restarted In shared deployments, this is a service-wide denial of service for other clients, not just a failure isolated to the attacking requests The failure is reproducible, so repeated request sequences can sustain the outage Affected version Confirmed on vLLM 0.17.1 Earlier and later versions have not been checked yet in this report Repro model Official Hugging Face repo: Qwen/Qwen3-0.6B-GPTQ-Int8 Anyone wants to reproduce the bug with my PoC scripts should download Qwen3-0.6B-GPTQ-Int8 first Trigger chain 1. A legal multi-request speculative workload keeps structured-output state, speculative decoding, overlap, and request cancellation active in the same live engine. 2. During rejection sampling, vLLM produces a recovered token equal to the model vocab_size boundary value. 3. That recovered token appears in position 0 of the sampled speculative row for a live request. The same row also contains trailing padding entries equal to -1, but those padding entries are not the key fault by themselves. 4. The next-token preparation step treats the position-0 recovered token as the real next token for that request and converts that out-of-vocabulary value to -1. 5. The drafter writes that converted -1 back into the live next-step input-id row for the request. 6. The drafting / embedding / attention path later consumes that live invalid token and the worker crashes on GPU. Details Simple example The important distinction is: trailing -1 values in a speculative row can be ordinary padding the bug appears when the first live token for a request becomes 151936 == vocab_size, and that live token is then converted into -1 In simplified form, the bad transition looks like this: ``text sampled speculative row: [151936, -1, -1, -1, ...] ` At this point, the trailing -1 values are only padding. The critical problem is that the first position holds 151936, which is out of vocabulary and is being treated as the request's real next token. Then vLLM prepares the next-token buffer: `text next_token_ids: [-1, ...] ` Finally, that converted -1 is written back into the live model input ids: `text input_ids_after: [-1, 0, 0, 0, ...] ` The crash happens because the live next token became -1 and was later consumed by the drafting / embedding / attention path, not merely because the speculative row contained padded -1 entries. Trigger path in code 1. The workload is frontend-legal. The requests use normal SamplingParams features such as structured outputs, stop, bad_words, min_tokens, and streaming overlap. No malformed token-id list is required at the request boundary. 2. In speculative decoding, the rejection sampler can generate recovered tokens when drafted tokens are rejected. `python # vllm/v1/sample/rejection_sampler.py def sample_recovered_tokens(...): recovered_token_ids = torch.empty_like(draft_token_ids) sample_recovered_tokens_kernel(batch_size, max_spec_len) return recovered_token_ids ` On the verified Qwen3 run, the recovered-token trace shows recovered_token_ids[0] = 151936, which is exactly vocab_size for this checkpoint. 3. The speculative proposer then prepares the next-token row from the sampled speculative row. `python # vllm/v1/spec_decode/eagle.py def prepare_next_token_ids_padded(...): ... eagle_prepare_next_token_padded_kernelgrid return next_token_ids, valid_sampled_tokens_count ` In the verified trace, this step receives a sampled row beginning with 151936, followed by -1 padding. The important point is that 151936 occupies the first live token position for the request. This step then produces next_token_ids[0] = -1, meaning the live next token for the request has been converted to -1. 4. The drafter then rotates the draft input ids and inserts those next_token_ids back into the live input-id buffer. `python # vllm/v1/spec_decode/eagle.py def set_inputs_first_pass(...): ... self.input_ids[token_indices_to_sample] = next_token_ids ` In the verified trace, this produces input_ids_after[0] = -1. 5. The model-side embed path later consumes those input ids. `python # vllm/model_executor/models/qwen2.py def embed_input_ids(self, input_ids: torch.Tensor) -> torch.Tensor: return self.embed_tokens(input_ids) ` In the verified trace, this is the first point where the converted -1 becomes visible as a real model input. The bug is not merely that the sampled speculative row contained padding -1; the bug is that the live next token for the request became -1 and was written back into input ids. 6. After that point, the visible sink depends on timing and backend state. On the attached Qwen3 reproducer, the engine commonly dies later in the drafting / attention path with CUDA error: device-side assert triggered, for example under flash_attn_varlen_func(...). Local script breakdown repro_g4_recovered_minus1_local.py is a standalone local reproducer. It reads the Qwen3 checkpoint path from VLLM_POC_G4_MODEL or the built-in /path/to/qwen3 placeholder It creates EngineCore directly without any external helper dependency It submits one fixed multi-request workload that preserves the same overlap and speculative-decoding state needed for the bug It writes: request_payloads.json repro_config.json timeline.json responses.json error.txt recovered_chain_trace.jsonl recovered_chain_trace.jsonl is the key attribution artifact. It records the recovered-token chain directly from the standalone reproducer gRPC script breakdown repro_g4_recovered_minus1_grpc.py is a standalone public gRPC reproducer. It reads the Qwen3 checkpoint path from VLLM_POC_G4_MODEL or the built-in /path/to/qwen3 placeholder It starts a temporary vllm.entrypoints.grpc_server process It sends only public Generate and Abort RPCs It submits one fixed overlapping request sequence that preserves the same speculative-decoding state needed for the bug After the crash window, it sends one more public Generate probe request to confirm that later gRPC requests also fail after the worker dies It writes: request_payloads.json timeline.json server_command.json responses.json post_crash_probe.json server.stdout.log server.stderr.log Observed result Local repro typically ends with: a recovered-token trace showing: sample_recovered_tokens_return -> recovered_token_ids[0] = 151936 prepare_next_token_ids_padded -> next_token_ids[0] = -1 set_inputs_first_pass -> input_ids_after[0] = -1 embed_input_ids_out_of_range -> input_ids[0] = -1 CUDA error: device-side assert triggered a fatal engine-side failure gRPC repro typically ends with: the triggering gRPC requests failing with INTERNAL: EngineCore encountered an issue. See stack trace (above) for the root cause. server logs showing the worker dies with CUDA error: device-side assert triggered a later public probe request also failing after the worker is dead This demonstrates that the issue is reachable through the public gRPC request surface, not only through a local reproducer. Log snippets Local recovered-chain trace `text sample_recovered_tokens_return: recovered_token_ids = [151936, ...] vocab_size = 151936 prepare_next_token_ids_padded: sampled_token_ids_head = [[151936, -1, -1, ...], ...] next_token_ids = [-1, ...] set_inputs_first_pass: input_ids_after = [-1, 0, 0, 0, ...] embed_input_ids_out_of_range: input_ids = [-1, 0, 0, 0, ...] ` gRPC server log `text torch.AcceleratorError: CUDA error: device-side assert triggered ... vllm.v1.engine.exceptions.EngineDeadError: EngineCore encountered an issue. See stack trace (above) for the root cause. ... Error in Generate for request post_crash_probe vllm.v1.engine.exceptions.EngineDeadError: EngineCore encountered an issue. See stack trace (above) for the root cause. ` Root cause This is a speculative-decoding state-handling bug, not an invalid frontend token-id input bug. The root cause is that a recovered speculative token can become equal to vocab_size, then be selected as the live next token for a request, then be converted to -1, and that converted -1 is still written back into live drafter input ids and later consumed by the drafting / embedding / attention path. For the Qwen3 checkpoint used here: 151936 == vocab_size This value should be described as the model vocab_size boundary value, not as a legal token id. Attachments The attached bundle for this report should contain: repro_g4_recovered_minus1_local.py repro_g4_recovered_minus1_grpc.py` These two standalone scripts are sufficient to reproduce the issue and its public gRPC reachability. Fix A fix for this vulnerability has been merged in: https://github.com/vllm-project/vllm/pull/44744

OWASP A03LLM01 · Prompt InjectionOWASP LLM
Get guardrail →

vLLM: Security Check Bypass via assert Statement in Activation Function Loading Allows Arbitrary Code Execution

Summary An assert-based security check in vLLM's activation function loading allows any unauthenticated attacker to achieve arbitrary code execution on the server by publishing a malicious HuggingFace model, when vLLM runs in Python optimized mode (python -O or PYTHONOPTIMIZE=1). Details vLLM uses an assert statement at vllm/model_executor/layers/pooler/activations.py:48 as its sole security control to restrict which activation functions can be loaded from a HuggingFace model's config.json: ``python vllm/model_executor/layers/pooler/activations.py:35-53 function_name: str | None = None if ( hasattr(config, "sentence_transformers") and "activation_fn" in config.sentence_transformers ): function_name = config.sentence_transformers["activation_fn"] elif ( hasattr(config, "sbert_ce_default_activation_function") and config.sbert_ce_default_activation_function is not None ): function_name = config.sbert_ce_default_activation_function if function_name is not None: assert function_name.startswith("torch.nn.modules."), ( "Loading of activation functions is restricted to " "torch.nn.modules for security reasons" ) fn = resolve_obj_by_qualname(function_name)() ` Python's assert statements are stripped at compile time when running in optimized mode (python -O or PYTHONOPTIMIZE=1). When the assert is absent, the attacker-controlled function_name from the model's config.json is passed directly to resolve_obj_by_qualname() — an unrestricted import gadget: `python def resolve_obj_by_qualname(qualname: str) -> Any: module_name, obj_name = qualname.rsplit(".", 1) module = importlib.import_module(module_name) return getattr(module, obj_name) ` This is the same vulnerability class as CVE-2017-1000433 (pysaml2 assert-based auth bypass), flagged by Bandit B101 and Ruff S101, and the reason Django proactively replaced all assert-based security checks (ticket #32508). Attacker-controlled input sources: config.sentence_transformers["activation_fn"] (line 40) config.sbert_ce_default_activation_function (line 45) Affected call sites — get_act_fn() is called via resolve_classifier_act_fn() from: vllm/model_executor/layers/pooler/seqwise/poolers.py:122 — SequencePooler vllm/model_executor/layers/pooler/tokwise/poolers.py:130 — TokenPooler Broader systemic risk: resolve_obj_by_qualname is called from ~20 locations across the codebase with no validation of its own. Any future caller feeding user-controlled input to it without validation creates the same vulnerability class. Suggested fix: Replace the assert with an explicit conditional raise: `python if not function_name.startswith("torch.nn.modules."): raise ValueError( "Loading of activation functions is restricted to " "torch.nn.modules for security reasons" ) ` Impact Arbitrary code execution. A malicious model author publishes a HuggingFace model with a crafted config.json. When a victim loads this model with vLLM running under python -O or PYTHONOPTIMIZE=1, arbitrary code executes during model initialization with the privileges of the vLLM process. The attack requires: 1. Victim loads a malicious model from HuggingFace (user interaction) 2. vLLM runs under python -O or PYTHONOPTIMIZE=1` (documented in production use) 3. Model uses a cross-encoder architecture (e.g. BERT or RoBERTa with sequence classification) Coordinated disclosure note: This vulnerability was also reported via huntr.com on April 2, 2026 (https://huntr.com/bounties/dcb05b04-e625-41e7-adbc-bbae0cc2d64c). A GitHub Security Advisory was also filed because it is vLLM's stated preferred disclosure channel per SECURITY.md. Fix A fix for this was introduced in this commit: https://github.com/vllm-project/vllm/commit/b3c7ffcab82c2439726f8cb213800f6f38c023d3

OWASP A03LLM01 · Prompt InjectionOWASP LLM
Get guardrail →

vLLM is vulnerable to an Out-of-Memory (OOM) Denial of Service (DoS) attack due to unbounded frame count processing in the `VideoMediaIO.load_base64()` method

vLLM versions 0.8.0 and later are vulnerable to an Out-of-Memory (OOM) Denial of Service (DoS) attack due to unbounded frame count processing in the VideoMediaIO.load_base64() method. When processing video/jpeg data URLs, the method splits the base64 data string on commas to extract individual JPEG frames without enforcing a frame count limit. An attacker can exploit this by crafting a single API request containing thousands of comma-separated base64-encoded JPEG frames in a data URL, causing the server to decode all frames into memory and crash due to excessive memory consumption. This vulnerability is reachable via the OpenAI-compatible chat completions API and does not require authentication.

OWASP A06LLM04 · Model DoSOWASP LLM
Get guardrail →

HuggingFace transformers vulnerable to remote code execution

A critical remote code execution vulnerability exists in all versions of the HuggingFace transformers library prior to version 5.3.0. The vulnerability allows an attacker to craft a malicious config.json file containing the _attn_implementation_internal field set to an attacker-controlled HuggingFace Hub repository ID. When a victim loads this model using the standard AutoModelForCausalLM.from_pretrained() API, the library downloads and executes arbitrary Python code from the attacker's repository with the victim's full OS privileges. This issue arises due to unfiltered deserialization of configuration attributes, insufficient sanitization of internal fields, and unsandboxed execution of downloaded kernels. The vulnerability bypasses the trust_remote_code security mechanism, is invisible to the victim, and exploits the standard documented usage pattern, making it particularly severe. Users are advised to upgrade to version 5.3.0 or later to mitigate this issue.

OWASP A08LLM05 · Supply ChainOWASP 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 →

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.

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 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 →

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

OWASP A10LLM02 · Insecure OutputOWASP LLM
Get guardrail →

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

OWASP A03LLM01 · Prompt InjectionOWASP LLM
Get guardrail →

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 →

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

OWASP A03LLM01 · Prompt InjectionOWASP LLM
Get guardrail →

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.

Showing 1–20 of 66 threats