CVE-2026-40087
Description
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.
Affected packages
Versions sourced from the GitHub Security Advisory.
| Package | Affected versions | Patched versions |
|---|---|---|
langchain-corePyPI | < 0.3.84 | 0.3.84 |
langchain-corePyPI | >= 1.0.0a1, < 1.2.28 | 1.2.28 |
Affected products
1Patches
26bab0ba3c123fix(core): sanitize prompts more (#36613)
7 files changed · +228 −31
libs/core/langchain_core/prompts/dict.py+9 −0 modified@@ -1,9 +1,12 @@ """Dict prompt template.""" +from __future__ import annotations + import warnings from functools import cached_property from typing import Any, Literal, Optional +from pydantic import model_validator from typing_extensions import override from langchain_core.load import dumpd @@ -25,6 +28,12 @@ class DictPromptTemplate(RunnableSerializable[dict, dict]): template: dict[str, Any] template_format: Literal["f-string", "mustache"] + @model_validator(mode="after") + def validate_template(self) -> DictPromptTemplate: + """Validate that the template structure contains only safe variables.""" + _get_input_variables(self.template, self.template_format) + return self + @property def input_variables(self) -> list[str]: """Template input variables."""
libs/core/langchain_core/prompts/image.py+12 −1 modified@@ -2,13 +2,15 @@ from typing import Any -from pydantic import Field +from pydantic import Field, model_validator +from typing_extensions import Self from langchain_core.prompt_values import ImagePromptValue, ImageURL, PromptValue from langchain_core.prompts.base import BasePromptTemplate from langchain_core.prompts.string import ( DEFAULT_FORMATTER_MAPPING, PromptTemplateFormat, + get_template_variables, ) from langchain_core.runnables import run_in_executor @@ -40,8 +42,17 @@ def __init__(self, **kwargs: Any) -> None: f" Found: {overlap}" ) raise ValueError(msg) + super().__init__(**kwargs) + @model_validator(mode="after") + def validate_template(self) -> Self: + """Validate template string values after Pydantic parsing.""" + for value in self.template.values(): + if isinstance(value, str): + get_template_variables(value, self.template_format) + return self + @property def _prompt_type(self) -> str: """Return the prompt type key."""
libs/core/langchain_core/prompts/string.py+45 −28 modified@@ -263,6 +263,46 @@ def _create_model_recursive(name: str, defs: Defs) -> type: } +def _parse_f_string_fields(template: str) -> list[tuple[str, str | None]]: + fields: list[tuple[str, str | None]] = [] + for _, field_name, format_spec, _ in Formatter().parse(template): + if field_name is not None: + fields.append((field_name, format_spec)) + return fields + + +def validate_f_string_template(template: str) -> list[str]: + """Validate an f-string template and return its input variables.""" + input_variables = set() + for var, format_spec in _parse_f_string_fields(template): + if "." in var or "[" in var or "]" in var: + msg = ( + f"Invalid variable name {var!r} in f-string template. " + f"Variable names cannot contain attribute " + f"access (.) or indexing ([])." + ) + raise ValueError(msg) + + if var.isdigit(): + msg = ( + f"Invalid variable name {var!r} in f-string template. " + f"Variable names cannot be all digits as they are interpreted " + f"as positional arguments." + ) + raise ValueError(msg) + + if format_spec and ("{" in format_spec or "}" in format_spec): + msg = ( + "Invalid format specifier in f-string template. " + "Nested replacement fields are not allowed." + ) + raise ValueError(msg) + + input_variables.add(var) + + return sorted(input_variables) + + def check_valid_template( template: str, template_format: str, input_variables: list[str] ) -> None: @@ -285,6 +325,8 @@ def check_valid_template( f" {list(DEFAULT_FORMATTER_MAPPING)}." ) raise ValueError(msg) from exc + if template_format == "f-string": + validate_f_string_template(template) try: validator_func(template, input_variables) except (KeyError, IndexError) as exc: @@ -308,43 +350,18 @@ def get_template_variables(template: str, template_format: str) -> list[str]: Raises: ValueError: If the template format is not supported. """ + input_variables: list[str] | set[str] if template_format == "jinja2": # Get the variables for the template - input_variables = _get_jinja2_variables_from_template(template) + input_variables = sorted(_get_jinja2_variables_from_template(template)) elif template_format == "f-string": - input_variables = { - v for _, v, _, _ in Formatter().parse(template) if v is not None - } + input_variables = validate_f_string_template(template) elif template_format == "mustache": input_variables = mustache_template_vars(template) else: msg = f"Unsupported template format: {template_format}" raise ValueError(msg) - # For f-strings, block attribute access and indexing syntax - # This prevents template injection attacks via accessing dangerous attributes - if template_format == "f-string": - for var in input_variables: - # Formatter().parse() returns field names with dots/brackets if present - # e.g., "obj.attr" or "obj[0]" - we need to block these - if "." in var or "[" in var or "]" in var: - msg = ( - f"Invalid variable name {var!r} in f-string template. " - f"Variable names cannot contain attribute " - f"access (.) or indexing ([])." - ) - raise ValueError(msg) - - # Block variable names that are all digits (e.g., "0", "100") - # These are interpreted as positional arguments, not keyword arguments - if var.isdigit(): - msg = ( - f"Invalid variable name {var!r} in f-string template. " - f"Variable names cannot be all digits as they are interpreted " - f"as positional arguments." - ) - raise ValueError(msg) - return sorted(input_variables)
libs/core/tests/unit_tests/prompts/test_chat.py+18 −0 modified@@ -1300,6 +1300,24 @@ def test_fstring_rejects_invalid_identifier_variable_names() -> None: assert result.messages[0].content == expected # type: ignore[attr-defined] +def test_fstring_rejects_nested_replacement_field_in_image_url() -> None: + with pytest.raises(ValueError, match="Nested replacement fields are not allowed"): + ChatPromptTemplate.from_messages( + [ + ( + "human", + [ + { + "type": "image_url", + "image_url": {"url": "{img:{img.__class__.__name__}}"}, + } + ], + ) + ], + template_format="f-string", + ) + + def test_mustache_template_attribute_access_vulnerability() -> None: """Test that Mustache template injection is blocked.
libs/core/tests/unit_tests/prompts/test_dict.py+85 −1 modified@@ -1,4 +1,9 @@ -from langchain_core.load import load +import json + +import pytest + +from langchain_core.load import load, loads +from langchain_core.prompts import PromptTemplate from langchain_core.prompts.dict import DictPromptTemplate @@ -32,3 +37,82 @@ def test_deserialize_legacy() -> None: template={"type": "audio", "audio": "{audio_data}"}, template_format="f-string" ) assert load(ser, allowed_objects=[DictPromptTemplate]) == expected + + +def test_dict_prompt_template_rejects_attribute_access_to_rich_objects() -> None: + with pytest.raises(ValueError, match="Variable names cannot contain attribute"): + DictPromptTemplate( + template={"output": "{message.additional_kwargs[secret]}"}, + template_format="f-string", + ) + + +def test_dict_prompt_template_loads_payload_rejects_attribute_access() -> None: + payload = json.dumps( + { + "lc": 1, + "type": "constructor", + "id": ["langchain_core", "prompts", "dict", "DictPromptTemplate"], + "kwargs": { + "template": {"output": "{message.additional_kwargs[secret]}"}, + "template_format": "f-string", + }, + } + ) + + with pytest.raises(ValueError, match="Variable names cannot contain attribute"): + loads(payload) + + +def test_dict_prompt_template_dumpd_round_trip_rejects_attribute_access() -> None: + payload = { + "lc": 1, + "type": "constructor", + "id": ["langchain_core", "prompts", "dict", "DictPromptTemplate"], + "kwargs": { + "template": {"output": "{message.additional_kwargs[secret]}"}, + "template_format": "f-string", + }, + } + + with pytest.raises(ValueError, match="Variable names cannot contain attribute"): + load(payload, allowed_objects=[DictPromptTemplate]) + + +def test_dict_prompt_template_deserialization_rejects_attribute_access() -> None: + payload = json.dumps( + { + "lc": 1, + "type": "constructor", + "id": ["langchain_core", "prompts", "dict", "DictPromptTemplate"], + "kwargs": { + "template": {"output": "{name.__class__.__name__}"}, + "template_format": "f-string", + }, + } + ) + + with pytest.raises(ValueError, match="Variable names cannot contain attribute"): + loads(payload) + + +def test_dict_prompt_template_legacy_deserialization_rejects_attribute_access() -> None: + ser = { + "type": "constructor", + "lc": 1, + "id": ["langchain_core", "prompts", "message", "_DictMessagePromptTemplate"], + "kwargs": { + "template_format": "f-string", + "template": {"output": "{name.__class__.__name__}"}, + }, + } + + with pytest.raises(ValueError, match="Variable names cannot contain attribute"): + load(ser, allowed_objects=[DictPromptTemplate]) + + +def test_prompt_template_blocks_attribute_access() -> None: + with pytest.raises( + ValueError, match="Variable names cannot contain attribute access" + ): + PromptTemplate.from_template("{name.__class__}", template_format="f-string")
libs/core/tests/unit_tests/prompts/test_image.py+9 −0 modified@@ -1,7 +1,11 @@ import json +import pytest +from pydantic import ValidationError + from langchain_core.load import dump, loads from langchain_core.prompts import ChatPromptTemplate +from langchain_core.prompts.image import ImagePromptTemplate def test_image_prompt_template_deserializable() -> None: @@ -15,6 +19,11 @@ def test_image_prompt_template_deserializable() -> None: ) +def test_image_prompt_template_invalid_template_type() -> None: + with pytest.raises(ValidationError): + ImagePromptTemplate(template=None) + + def test_image_prompt_template_deserializable_old() -> None: """Test that the image prompt template is serializable.""" loads(
libs/core/tests/unit_tests/prompts/test_string.py+50 −1 modified@@ -1,7 +1,12 @@ import pytest from packaging import version -from langchain_core.prompts.string import mustache_schema +from langchain_core.prompts.string import ( + check_valid_template, + get_template_variables, + mustache_schema, +) +from langchain_core.utils.formatting import formatter from langchain_core.utils.pydantic import PYDANTIC_VERSION PYDANTIC_VERSION_AT_LEAST_29 = version.parse("2.9") <= PYDANTIC_VERSION @@ -30,3 +35,47 @@ def test_mustache_schema_parent_child() -> None: } actual = mustache_schema(template).model_json_schema() assert expected == actual + + +def test_get_template_variables_rejects_nested_replacement_field_in_format_spec() -> ( + None +): + template = "{name:{name.__class__.__name__}}" + + with pytest.raises(ValueError, match="Nested replacement fields are not allowed"): + get_template_variables(template, "f-string") + + +def test_formatter_rejects_nested_replacement_field_in_format_spec() -> None: + template = "{name:{name.__class__.__name__}}" + + with pytest.raises(ValueError, match="Invalid format specifier"): + formatter.format(template, name="hello") + + +def test_check_valid_template_rejects_nested_replacement_field_in_format_spec() -> None: + template = "{name:{name.__class__.__name__}}" + + with pytest.raises(ValueError, match="Nested replacement fields are not allowed"): + check_valid_template(template, "f-string", ["name"]) + + +@pytest.mark.parametrize( + ("template", "kwargs", "expected_variables", "expected_output"), + [ + ("{value:.2f}", {"value": 3.14159}, ["value"], "3.14"), + ("{value:>10}", {"value": "cat"}, ["value"], " cat"), + ("{value:*^10}", {"value": "cat"}, ["value"], "***cat****"), + ("{value:,}", {"value": 1234567}, ["value"], "1,234,567"), + ("{value:%}", {"value": 0.125}, ["value"], "12.500000%"), + ("{value!r}", {"value": "cat"}, ["value"], "'cat'"), + ], +) +def test_f_string_templates_allow_safe_format_specs( + template: str, + kwargs: dict[str, object], + expected_variables: list[str], + expected_output: str, +) -> None: + assert get_template_variables(template, "f-string") == expected_variables + assert formatter.format(template, **kwargs) == expected_output
af2ed47c6f00fix(core): add more sanitization to templates (#36612)
7 files changed · +275 −31
libs/core/langchain_core/prompts/dict.py+25 −0 modified@@ -4,6 +4,7 @@ from functools import cached_property from typing import Any, Literal, cast +from pydantic import model_validator from typing_extensions import override from langchain_core.load import dumpd @@ -21,11 +22,35 @@ class DictPromptTemplate(RunnableSerializable[dict, dict]): Recognizes variables in f-string or mustache formatted string dict values. Does NOT recognize variables in dict keys. Applies recursively. + + Example: + ```python + prompt = DictPromptTemplate( + template={ + "type": "text", + "text": "Hello {name}", + "metadata": {"source": "{source}"}, + }, + template_format="f-string", + ) + prompt.format(name="Alice", source="docs") + # { + # "type": "text", + # "text": "Hello Alice", + # "metadata": {"source": "docs"}, + # } + ``` """ template: dict[str, Any] template_format: Literal["f-string", "mustache"] + @model_validator(mode="after") + def validate_template(self) -> "DictPromptTemplate": + """Validate that the template structure contains only safe variables.""" + _get_input_variables(self.template, self.template_format) + return self + @property def input_variables(self) -> list[str]: """Template input variables."""
libs/core/langchain_core/prompts/image.py+21 −1 modified@@ -9,12 +9,25 @@ from langchain_core.prompts.string import ( DEFAULT_FORMATTER_MAPPING, PromptTemplateFormat, + get_template_variables, ) from langchain_core.runnables import run_in_executor class ImagePromptTemplate(BasePromptTemplate[ImageURL]): - """Image prompt template for a multimodal model.""" + """Image prompt template for a multimodal model. + + Example: + ```python + prompt = ImagePromptTemplate( + input_variables=["image_id"], + template={"url": "https://example.com/{image_id}.png", "detail": "high"}, + template_format="f-string", + ) + prompt.format(image_id="cat") + # {"url": "https://example.com/cat.png", "detail": "high"} + ``` + """ template: dict = Field(default_factory=dict) """Template for the prompt.""" @@ -43,6 +56,13 @@ def __init__(self, **kwargs: Any) -> None: f" Found: {overlap}" ) raise ValueError(msg) + + template = kwargs.get("template", {}) + template_format = kwargs.get("template_format", "f-string") + for value in template.values(): + if isinstance(value, str): + get_template_variables(value, template_format) + super().__init__(**kwargs) @property
libs/core/langchain_core/prompts/string.py+45 −28 modified@@ -219,6 +219,46 @@ def _create_model_recursive(name: str, defs: Defs) -> type[BaseModel]: } +def _parse_f_string_fields(template: str) -> list[tuple[str, str | None]]: + fields: list[tuple[str, str | None]] = [] + for _, field_name, format_spec, _ in Formatter().parse(template): + if field_name is not None: + fields.append((field_name, format_spec)) + return fields + + +def validate_f_string_template(template: str) -> list[str]: + """Validate an f-string template and return its input variables.""" + input_variables = set() + for var, format_spec in _parse_f_string_fields(template): + if "." in var or "[" in var or "]" in var: + msg = ( + f"Invalid variable name {var!r} in f-string template. " + f"Variable names cannot contain attribute " + f"access (.) or indexing ([])." + ) + raise ValueError(msg) + + if var.isdigit(): + msg = ( + f"Invalid variable name {var!r} in f-string template. " + f"Variable names cannot be all digits as they are interpreted " + f"as positional arguments." + ) + raise ValueError(msg) + + if format_spec and ("{" in format_spec or "}" in format_spec): + msg = ( + "Invalid format specifier in f-string template. " + "Nested replacement fields are not allowed." + ) + raise ValueError(msg) + + input_variables.add(var) + + return sorted(input_variables) + + def check_valid_template( template: str, template_format: str, input_variables: list[str] ) -> None: @@ -243,6 +283,8 @@ def check_valid_template( f" {list(DEFAULT_FORMATTER_MAPPING)}." ) raise ValueError(msg) from exc + if template_format == "f-string": + validate_f_string_template(template) try: validator_func(template, input_variables) except (KeyError, IndexError) as exc: @@ -268,43 +310,18 @@ def get_template_variables(template: str, template_format: str) -> list[str]: Raises: ValueError: If the template format is not supported. """ + input_variables: list[str] | set[str] if template_format == "jinja2": # Get the variables for the template - input_variables = _get_jinja2_variables_from_template(template) + input_variables = sorted(_get_jinja2_variables_from_template(template)) elif template_format == "f-string": - input_variables = { - v for _, v, _, _ in Formatter().parse(template) if v is not None - } + input_variables = validate_f_string_template(template) elif template_format == "mustache": input_variables = mustache_template_vars(template) else: msg = f"Unsupported template format: {template_format}" raise ValueError(msg) - # For f-strings, block attribute access and indexing syntax - # This prevents template injection attacks via accessing dangerous attributes - if template_format == "f-string": - for var in input_variables: - # Formatter().parse() returns field names with dots/brackets if present - # e.g., "obj.attr" or "obj[0]" - we need to block these - if "." in var or "[" in var or "]" in var: - msg = ( - f"Invalid variable name {var!r} in f-string template. " - f"Variable names cannot contain attribute " - f"access (.) or indexing ([])." - ) - raise ValueError(msg) - - # Block variable names that are all digits (e.g., "0", "100") - # These are interpreted as positional arguments, not keyword arguments - if var.isdigit(): - msg = ( - f"Invalid variable name {var!r} in f-string template. " - f"Variable names cannot be all digits as they are interpreted " - f"as positional arguments." - ) - raise ValueError(msg) - return sorted(input_variables)
libs/core/tests/unit_tests/prompts/test_chat.py+18 −0 modified@@ -1951,6 +1951,24 @@ def test_fstring_rejects_invalid_identifier_variable_names() -> None: assert result.messages[0].content == expected # type: ignore[attr-defined] +def test_fstring_rejects_nested_replacement_field_in_image_url() -> None: + with pytest.raises(ValueError, match="Nested replacement fields are not allowed"): + ChatPromptTemplate.from_messages( + [ + ( + "human", + [ + { + "type": "image_url", + "image_url": {"url": "{img:{img.__class__.__name__}}"}, + } + ], + ) + ], + template_format="f-string", + ) + + def test_mustache_template_attribute_access_vulnerability() -> None: """Test that Mustache template injection is blocked.
libs/core/tests/unit_tests/prompts/test_dict.py+85 −1 modified@@ -1,4 +1,9 @@ -from langchain_core.load import load +import json + +import pytest + +from langchain_core.load import load, loads +from langchain_core.prompts import PromptTemplate from langchain_core.prompts.dict import DictPromptTemplate @@ -32,3 +37,82 @@ def test_deserialize_legacy() -> None: template={"type": "audio", "audio": "{audio_data}"}, template_format="f-string" ) assert load(ser, allowed_objects=[DictPromptTemplate]) == expected + + +def test_dict_prompt_template_rejects_attribute_access_to_rich_objects() -> None: + with pytest.raises(ValueError, match="Variable names cannot contain attribute"): + DictPromptTemplate( + template={"output": "{message.additional_kwargs[secret]}"}, + template_format="f-string", + ) + + +def test_dict_prompt_template_loads_payload_rejects_attribute_access() -> None: + payload = json.dumps( + { + "lc": 1, + "type": "constructor", + "id": ["langchain_core", "prompts", "dict", "DictPromptTemplate"], + "kwargs": { + "template": {"output": "{message.additional_kwargs[secret]}"}, + "template_format": "f-string", + }, + } + ) + + with pytest.raises(ValueError, match="Variable names cannot contain attribute"): + loads(payload) + + +def test_dict_prompt_template_dumpd_round_trip_rejects_attribute_access() -> None: + payload = { + "lc": 1, + "type": "constructor", + "id": ["langchain_core", "prompts", "dict", "DictPromptTemplate"], + "kwargs": { + "template": {"output": "{message.additional_kwargs[secret]}"}, + "template_format": "f-string", + }, + } + + with pytest.raises(ValueError, match="Variable names cannot contain attribute"): + load(payload, allowed_objects=[DictPromptTemplate]) + + +def test_dict_prompt_template_deserialization_rejects_attribute_access() -> None: + payload = json.dumps( + { + "lc": 1, + "type": "constructor", + "id": ["langchain_core", "prompts", "dict", "DictPromptTemplate"], + "kwargs": { + "template": {"output": "{name.__class__.__name__}"}, + "template_format": "f-string", + }, + } + ) + + with pytest.raises(ValueError, match="Variable names cannot contain attribute"): + loads(payload) + + +def test_dict_prompt_template_legacy_deserialization_rejects_attribute_access() -> None: + ser = { + "type": "constructor", + "lc": 1, + "id": ["langchain_core", "prompts", "message", "_DictMessagePromptTemplate"], + "kwargs": { + "template_format": "f-string", + "template": {"output": "{name.__class__.__name__}"}, + }, + } + + with pytest.raises(ValueError, match="Variable names cannot contain attribute"): + load(ser, allowed_objects=[DictPromptTemplate]) + + +def test_prompt_template_blocks_attribute_access() -> None: + with pytest.raises( + ValueError, match="Variable names cannot contain attribute access" + ): + PromptTemplate.from_template("{name.__class__}", template_format="f-string")
libs/core/tests/unit_tests/prompts/test_image.py+31 −0 modified@@ -1,7 +1,10 @@ import json +import pytest + from langchain_core.load import dump, loads from langchain_core.prompts import ChatPromptTemplate +from langchain_core.prompts.image import ImagePromptTemplate def test_image_prompt_template_deserializable() -> None: @@ -107,3 +110,31 @@ def test_image_prompt_template_deserializable_old() -> None: } ), ) + + +def test_image_prompt_template_rejects_attribute_access_in_template_values() -> None: + with pytest.raises(ValueError, match="Variable names cannot contain attribute"): + ImagePromptTemplate( + input_variables=["image"], + template={"url": "https://example.com/{image.__class__.__name__}.png"}, + ) + + +def test_image_prompt_template_deserialization_rejects_attribute_access() -> None: + payload = json.dumps( + { + "lc": 1, + "type": "constructor", + "id": ["langchain", "prompts", "image", "ImagePromptTemplate"], + "kwargs": { + "template": { + "url": "https://example.com/{image.__class__.__name__}.png" + }, + "input_variables": ["image"], + "template_format": "f-string", + }, + } + ) + + with pytest.raises(ValueError, match="Variable names cannot contain attribute"): + loads(payload)
libs/core/tests/unit_tests/prompts/test_string.py+50 −1 modified@@ -1,7 +1,12 @@ import pytest from packaging import version -from langchain_core.prompts.string import get_template_variables, mustache_schema +from langchain_core.prompts.string import ( + check_valid_template, + get_template_variables, + mustache_schema, +) +from langchain_core.utils.formatting import formatter from langchain_core.utils.pydantic import PYDANTIC_VERSION PYDANTIC_VERSION_AT_LEAST_29 = version.parse("2.9") <= PYDANTIC_VERSION @@ -39,3 +44,47 @@ def test_get_template_variables_mustache_nested() -> None: expected = ["user"] actual = get_template_variables(template, template_format) assert actual == expected + + +def test_get_template_variables_rejects_nested_replacement_field_in_format_spec() -> ( + None +): + template = "{name:{name.__class__.__name__}}" + + with pytest.raises(ValueError, match="Nested replacement fields are not allowed"): + get_template_variables(template, "f-string") + + +def test_formatter_rejects_nested_replacement_field_in_format_spec() -> None: + template = "{name:{name.__class__.__name__}}" + + with pytest.raises(ValueError, match="Invalid format specifier"): + formatter.format(template, name="hello") + + +def test_check_valid_template_rejects_nested_replacement_field_in_format_spec() -> None: + template = "{name:{name.__class__.__name__}}" + + with pytest.raises(ValueError, match="Nested replacement fields are not allowed"): + check_valid_template(template, "f-string", ["name"]) + + +@pytest.mark.parametrize( + ("template", "kwargs", "expected_variables", "expected_output"), + [ + ("{value:.2f}", {"value": 3.14159}, ["value"], "3.14"), + ("{value:>10}", {"value": "cat"}, ["value"], " cat"), + ("{value:*^10}", {"value": "cat"}, ["value"], "***cat****"), + ("{value:,}", {"value": 1234567}, ["value"], "1,234,567"), + ("{value:%}", {"value": 0.125}, ["value"], "12.500000%"), + ("{value!r}", {"value": "cat"}, ["value"], "'cat'"), + ], +) +def test_f_string_templates_allow_safe_format_specs( + template: str, + kwargs: dict[str, object], + expected_variables: list[str], + expected_output: str, +) -> None: + assert get_template_variables(template, "f-string") == expected_variables + assert formatter.format(template, **kwargs) == expected_output
Vulnerability mechanics
Generated by null/stub on May 9, 2026. Inputs: CWE entries + fix-commit diffs from this CVE's patches. Citations validated against bundle.
References
9- github.com/langchain-ai/langchain/commit/6bab0ba3c12328008ddca3e0d54ff5a6151cd27bnvdPatchWEB
- github.com/langchain-ai/langchain/commit/af2ed47c6f008cdd551f3c0d87db3774c8dfe258nvdPatchWEB
- github.com/langchain-ai/langchain/pull/36612nvdIssue TrackingPatchWEB
- github.com/langchain-ai/langchain/pull/36613nvdIssue TrackingPatchWEB
- github.com/advisories/GHSA-926x-3r5x-gfhwghsaADVISORY
- github.com/langchain-ai/langchain/security/advisories/GHSA-926x-3r5x-gfhwnvdMitigationVendor AdvisoryWEB
- nvd.nist.gov/vuln/detail/CVE-2026-40087ghsaADVISORY
- github.com/langchain-ai/langchain/releases/tag/langchain-core%3D%3D0.3.84nvdProductRelease NotesWEB
- github.com/langchain-ai/langchain/releases/tag/langchain-core%3D%3D1.2.28nvdProductRelease NotesWEB
News mentions
0No linked articles in our index yet.