77
88import yaml
99
10+ from langchain_core ._api import deprecated
1011from langchain_core .output_parsers .string import StrOutputParser
1112from langchain_core .prompts .base import BasePromptTemplate
1213from langchain_core .prompts .chat import ChatPromptTemplate
1718logger = logging .getLogger (__name__ )
1819
1920
20- def load_prompt_from_config (config : dict ) -> BasePromptTemplate :
21+ def _validate_path (path : Path ) -> None :
22+ """Reject absolute paths and ``..`` traversal components.
23+
24+ Args:
25+ path: The path to validate.
26+
27+ Raises:
28+ ValueError: If the path is absolute or contains ``..`` components.
29+ """
30+ if path .is_absolute ():
31+ msg = (
32+ f"Path '{ path } ' is absolute. Absolute paths are not allowed "
33+ f"when loading prompt configurations to prevent path traversal "
34+ f"attacks. Use relative paths instead, or pass "
35+ f"`allow_dangerous_paths=True` if you trust the input."
36+ )
37+ raise ValueError (msg )
38+ if ".." in path .parts :
39+ msg = (
40+ f"Path '{ path } ' contains '..' components. Directory traversal "
41+ f"sequences are not allowed when loading prompt configurations. "
42+ f"Use direct relative paths instead, or pass "
43+ f"`allow_dangerous_paths=True` if you trust the input."
44+ )
45+ raise ValueError (msg )
46+
47+
48+ @deprecated (
49+ since = "1.2.21" ,
50+ removal = "2.0.0" ,
51+ alternative = "Use `dumpd`/`dumps` from `langchain_core.load` to serialize "
52+ "prompts and `load`/`loads` to deserialize them." ,
53+ )
54+ def load_prompt_from_config (
55+ config : dict , * , allow_dangerous_paths : bool = False
56+ ) -> BasePromptTemplate :
2157 """Load prompt from config dict.
2258
2359 Args:
2460 config: Dict containing the prompt configuration.
61+ allow_dangerous_paths: If ``False`` (default), file paths in the
62+ config (such as ``template_path``, ``examples``, and
63+ ``example_prompt_path``) are validated to reject absolute paths
64+ and directory traversal (``..``) sequences. Set to ``True`` only
65+ if you trust the source of the config.
2566
2667 Returns:
2768 A `PromptTemplate` object.
@@ -38,10 +79,12 @@ def load_prompt_from_config(config: dict) -> BasePromptTemplate:
3879 raise ValueError (msg )
3980
4081 prompt_loader = type_to_loader_dict [config_type ]
41- return prompt_loader (config )
82+ return prompt_loader (config , allow_dangerous_paths = allow_dangerous_paths )
4283
4384
44- def _load_template (var_name : str , config : dict ) -> dict :
85+ def _load_template (
86+ var_name : str , config : dict , * , allow_dangerous_paths : bool = False
87+ ) -> dict :
4588 """Load template from the path if applicable."""
4689 # Check if template_path exists in config.
4790 if f"{ var_name } _path" in config :
@@ -51,6 +94,8 @@ def _load_template(var_name: str, config: dict) -> dict:
5194 raise ValueError (msg )
5295 # Pop the template path from the config.
5396 template_path = Path (config .pop (f"{ var_name } _path" ))
97+ if not allow_dangerous_paths :
98+ _validate_path (template_path )
5499 # Load the template.
55100 if template_path .suffix == ".txt" :
56101 template = template_path .read_text (encoding = "utf-8" )
@@ -61,12 +106,14 @@ def _load_template(var_name: str, config: dict) -> dict:
61106 return config
62107
63108
64- def _load_examples (config : dict ) -> dict :
109+ def _load_examples (config : dict , * , allow_dangerous_paths : bool = False ) -> dict :
65110 """Load examples if necessary."""
66111 if isinstance (config ["examples" ], list ):
67112 pass
68113 elif isinstance (config ["examples" ], str ):
69114 path = Path (config ["examples" ])
115+ if not allow_dangerous_paths :
116+ _validate_path (path )
70117 with path .open (encoding = "utf-8" ) as f :
71118 if path .suffix == ".json" :
72119 examples = json .load (f )
@@ -92,11 +139,17 @@ def _load_output_parser(config: dict) -> dict:
92139 return config
93140
94141
95- def _load_few_shot_prompt (config : dict ) -> FewShotPromptTemplate :
142+ def _load_few_shot_prompt (
143+ config : dict , * , allow_dangerous_paths : bool = False
144+ ) -> FewShotPromptTemplate :
96145 """Load the "few shot" prompt from the config."""
97146 # Load the suffix and prefix templates.
98- config = _load_template ("suffix" , config )
99- config = _load_template ("prefix" , config )
147+ config = _load_template (
148+ "suffix" , config , allow_dangerous_paths = allow_dangerous_paths
149+ )
150+ config = _load_template (
151+ "prefix" , config , allow_dangerous_paths = allow_dangerous_paths
152+ )
100153 # Load the example prompt.
101154 if "example_prompt_path" in config :
102155 if "example_prompt" in config :
@@ -105,19 +158,30 @@ def _load_few_shot_prompt(config: dict) -> FewShotPromptTemplate:
105158 "be specified."
106159 )
107160 raise ValueError (msg )
108- config ["example_prompt" ] = load_prompt (config .pop ("example_prompt_path" ))
161+ example_prompt_path = Path (config .pop ("example_prompt_path" ))
162+ if not allow_dangerous_paths :
163+ _validate_path (example_prompt_path )
164+ config ["example_prompt" ] = load_prompt (
165+ example_prompt_path , allow_dangerous_paths = allow_dangerous_paths
166+ )
109167 else :
110- config ["example_prompt" ] = load_prompt_from_config (config ["example_prompt" ])
168+ config ["example_prompt" ] = load_prompt_from_config (
169+ config ["example_prompt" ], allow_dangerous_paths = allow_dangerous_paths
170+ )
111171 # Load the examples.
112- config = _load_examples (config )
172+ config = _load_examples (config , allow_dangerous_paths = allow_dangerous_paths )
113173 config = _load_output_parser (config )
114174 return FewShotPromptTemplate (** config )
115175
116176
117- def _load_prompt (config : dict ) -> PromptTemplate :
177+ def _load_prompt (
178+ config : dict , * , allow_dangerous_paths : bool = False
179+ ) -> PromptTemplate :
118180 """Load the prompt template from config."""
119181 # Load the template from disk if necessary.
120- config = _load_template ("template" , config )
182+ config = _load_template (
183+ "template" , config , allow_dangerous_paths = allow_dangerous_paths
184+ )
121185 config = _load_output_parser (config )
122186
123187 template_format = config .get ("template_format" , "f-string" )
@@ -134,12 +198,28 @@ def _load_prompt(config: dict) -> PromptTemplate:
134198 return PromptTemplate (** config )
135199
136200
137- def load_prompt (path : str | Path , encoding : str | None = None ) -> BasePromptTemplate :
201+ @deprecated (
202+ since = "1.2.21" ,
203+ removal = "2.0.0" ,
204+ alternative = "Use `dumpd`/`dumps` from `langchain_core.load` to serialize "
205+ "prompts and `load`/`loads` to deserialize them." ,
206+ )
207+ def load_prompt (
208+ path : str | Path ,
209+ encoding : str | None = None ,
210+ * ,
211+ allow_dangerous_paths : bool = False ,
212+ ) -> BasePromptTemplate :
138213 """Unified method for loading a prompt from LangChainHub or local filesystem.
139214
140215 Args:
141216 path: Path to the prompt file.
142217 encoding: Encoding of the file.
218+ allow_dangerous_paths: If ``False`` (default), file paths referenced
219+ inside the loaded config (such as ``template_path``, ``examples``,
220+ and ``example_prompt_path``) are validated to reject absolute paths
221+ and directory traversal (``..``) sequences. Set to ``True`` only
222+ if you trust the source of the config.
143223
144224 Returns:
145225 A `PromptTemplate` object.
@@ -154,11 +234,16 @@ def load_prompt(path: str | Path, encoding: str | None = None) -> BasePromptTemp
154234 "instead."
155235 )
156236 raise RuntimeError (msg )
157- return _load_prompt_from_file (path , encoding )
237+ return _load_prompt_from_file (
238+ path , encoding , allow_dangerous_paths = allow_dangerous_paths
239+ )
158240
159241
160242def _load_prompt_from_file (
161- file : str | Path , encoding : str | None = None
243+ file : str | Path ,
244+ encoding : str | None = None ,
245+ * ,
246+ allow_dangerous_paths : bool = False ,
162247) -> BasePromptTemplate :
163248 """Load prompt from file."""
164249 # Convert file to a Path object.
@@ -174,10 +259,14 @@ def _load_prompt_from_file(
174259 msg = f"Got unsupported file type { file_path .suffix } "
175260 raise ValueError (msg )
176261 # Load the prompt from the config now.
177- return load_prompt_from_config (config )
262+ return load_prompt_from_config (config , allow_dangerous_paths = allow_dangerous_paths )
178263
179264
180- def _load_chat_prompt (config : dict ) -> ChatPromptTemplate :
265+ def _load_chat_prompt (
266+ config : dict ,
267+ * ,
268+ allow_dangerous_paths : bool = False , # noqa: ARG001
269+ ) -> ChatPromptTemplate :
181270 """Load chat prompt from config."""
182271 messages = config .pop ("messages" )
183272 template = messages [0 ]["prompt" ].pop ("template" ) if messages else None
@@ -190,7 +279,7 @@ def _load_chat_prompt(config: dict) -> ChatPromptTemplate:
190279 return ChatPromptTemplate .from_template (template = template , ** config )
191280
192281
193- type_to_loader_dict : dict [str , Callable [[ dict ] , BasePromptTemplate ]] = {
282+ type_to_loader_dict : dict [str , Callable [... , BasePromptTemplate ]] = {
194283 "prompt" : _load_prompt ,
195284 "few_shot" : _load_few_shot_prompt ,
196285 "chat" : _load_chat_prompt ,
0 commit comments