1818NOT instantiated as LC objects.
1919"""
2020
21- from typing import Any
21+ from typing import Any , cast
2222
2323from langchain_core .load .serializable import (
2424 Serializable ,
@@ -102,16 +102,25 @@ def _serialize_value(obj: Any) -> Any:
102102 return to_json_not_implemented (obj )
103103
104104
105- def _is_lc_secret (obj : Any ) -> bool :
106- """Check if an object is a LangChain secret marker."""
107- expected_num_keys = 3
108- return (
109- isinstance (obj , dict )
110- and obj .get ("lc" ) == 1
111- and obj .get ("type" ) == "secret"
112- and "id" in obj
113- and len (obj ) == expected_num_keys
114- )
105+ def _get_secret_keys (obj : Serializable ) -> set [str ]:
106+ """Return the merged set of constructor kwarg names declared as secrets.
107+
108+ Mirrors the MRO walk in `Serializable.to_json` so the keys returned here
109+ match the keys whose values `_replace_secrets` rewrites into secret
110+ markers. Used by `_serialize_lc_object` to decide which kwargs to skip
111+ when escaping user data.
112+ """
113+ secrets : dict [str , str ] = {}
114+ model_fields = type (obj ).model_fields
115+ for cls in [None , * obj .__class__ .mro ()]:
116+ if cls is Serializable :
117+ break
118+ this = cast ("Serializable" , obj if cls is None else super (cls , obj ))
119+ secrets .update (this .lc_secrets )
120+ for key in list (secrets ):
121+ if (key in model_fields ) and (alias := model_fields [key ].alias ) is not None :
122+ secrets [alias ] = secrets [key ]
123+ return set (secrets )
115124
116125
117126def _serialize_lc_object (obj : Any ) -> dict [str , Any ]:
@@ -124,21 +133,29 @@ def _serialize_lc_object(obj: Any) -> dict[str, Any]:
124133 The serialized dict with user data in kwargs escaped as needed.
125134
126135 Note:
127- Kwargs values are processed with `_serialize_value` to escape user data (like
128- metadata) that contains `'lc'` keys. Secret fields (from `lc_secrets`) are
129- skipped because `to_json()` replaces their values with secret markers.
136+ Kwargs values are processed with `_serialize_value` to escape user data
137+ (like metadata) that contains `'lc'` keys. Secret fields are identified
138+ by the class's declared `lc_secrets` and skipped because `to_json()`
139+ already converted their values to secret markers.
140+
141+ The check is key-based rather than shape-based. A shape-based check
142+ ("this dict looks like a secret marker") can be forged by user data,
143+ letting attacker-controlled free-form dicts bypass escaping and reach
144+ the Reviver.
130145 """
131146 if not isinstance (obj , Serializable ):
132147 msg = f"Expected Serializable, got { type (obj )} "
133148 raise TypeError (msg )
134149
135150 serialized : dict [str , Any ] = dict (obj .to_json ())
136151
137- # Process kwargs to escape user data that could be confused with LC objects
138- # Skip secret fields - to_json() already converted them to secret markers
152+ # Process kwargs to escape user data that could be confused with LC objects.
153+ # Skip kwargs declared as secrets - `to_json()` already replaced their
154+ # values with secret markers via `_replace_secrets`.
139155 if serialized .get ("type" ) == "constructor" and "kwargs" in serialized :
156+ secret_keys = _get_secret_keys (obj )
140157 serialized ["kwargs" ] = {
141- k : v if _is_lc_secret ( v ) else _serialize_value (v )
158+ k : v if k in secret_keys else _serialize_value (v )
142159 for k , v in serialized ["kwargs" ].items ()
143160 }
144161
0 commit comments