Team Ai
Datasetpublic

codekingpro/portable-devtools

sourceHugging Faceupdated 5mo agoView on Hugging Face
1likes14kdownloads
_validation.py192 linesDownload Raw Back to load
1"""Validation utilities for LangChain serialization.2 3Provides escape-based protection against injection attacks in serialized objects. The4approach uses an allowlist design: only dicts explicitly produced by5`Serializable.to_json()` are treated as LC objects during deserialization.6 7## How escaping works8 9During serialization, plain dicts (user data) that contain an `'lc'` key are wrapped:10 11```python12{"lc": 1, ...}  # user data that looks like LC object13# becomes:14{"__lc_escaped__": {"lc": 1, ...}}15```16 17During deserialization, escaped dicts are unwrapped and returned as plain dicts,18NOT instantiated as LC objects.19"""20 21from typing import Any, cast22 23from langchain_core.load.serializable import (24    Serializable,25    to_json_not_implemented,26)27 28_LC_ESCAPED_KEY = "__lc_escaped__"29"""Sentinel key used to mark escaped user dicts during serialization.30 31When a plain dict contains 'lc' key (which could be confused with LC objects),32we wrap it as {"__lc_escaped__": {...original...}}.33"""34 35 36def _needs_escaping(obj: dict[str, Any]) -> bool:37    """Check if a dict needs escaping to prevent confusion with LC objects.38 39    A dict needs escaping if:40 41    1. It has an `'lc'` key (could be confused with LC serialization format)42    2. It has only the escape key (would be mistaken for an escaped dict)43    """44    return "lc" in obj or (len(obj) == 1 and _LC_ESCAPED_KEY in obj)45 46 47def _escape_dict(obj: dict[str, Any]) -> dict[str, Any]:48    """Wrap a dict in the escape marker.49 50    Example:51        ```python52        {"key": "value"}  # becomes {"__lc_escaped__": {"key": "value"}}53        ```54    """55    return {_LC_ESCAPED_KEY: obj}56 57 58def _is_escaped_dict(obj: dict[str, Any]) -> bool:59    """Check if a dict is an escaped user dict.60 61    Example:62        ```python63        {"__lc_escaped__": {...}}  # is an escaped dict64        ```65    """66    return len(obj) == 1 and _LC_ESCAPED_KEY in obj67 68 69def _serialize_value(obj: Any) -> Any:70    """Serialize a value with escaping of user dicts.71 72    Called recursively on kwarg values to escape any plain dicts that could be confused73    with LC objects.74 75    Args:76        obj: The value to serialize.77 78    Returns:79        The serialized value with user dicts escaped as needed.80    """81    if isinstance(obj, Serializable):82        # This is an LC object - serialize it properly (not escaped)83        return _serialize_lc_object(obj)84    if isinstance(obj, dict):85        if not all(isinstance(k, (str, int, float, bool, type(None))) for k in obj):86            # if keys are not json serializable87            return to_json_not_implemented(obj)88        # Check if dict needs escaping BEFORE recursing into values.89        # If it needs escaping, wrap it as-is - the contents are user data that90        # will be returned as-is during deserialization (no instantiation).91        # This prevents re-escaping of already-escaped nested content.92        if _needs_escaping(obj):93            return _escape_dict(obj)94        # Safe dict (no 'lc' key) - recurse into values95        return {k: _serialize_value(v) for k, v in obj.items()}96    if isinstance(obj, (list, tuple)):97        return [_serialize_value(item) for item in obj]98    if isinstance(obj, (str, int, float, bool, type(None))):99        return obj100 101    # Non-JSON-serializable object (datetime, custom objects, etc.)102    return to_json_not_implemented(obj)103 104 105def _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 here109    match the keys whose values `_replace_secrets` rewrites into secret110    markers. Used by `_serialize_lc_object` to decide which kwargs to skip111    when escaping user data.112    """113    secrets: dict[str, str] = {}114    model_fields = type(obj).model_fields115    for cls in [None, *obj.__class__.mro()]:116        if cls is Serializable:117            break118        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)124 125 126def _serialize_lc_object(obj: Any) -> dict[str, Any]:127    """Serialize a `Serializable` object with escaping of user data in kwargs.128 129    Args:130        obj: The `Serializable` object to serialize.131 132    Returns:133        The serialized dict with user data in kwargs escaped as needed.134 135    Note:136        Kwargs values are processed with `_serialize_value` to escape user data137        (like metadata) that contains `'lc'` keys. Secret fields are identified138        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 check142        ("this dict looks like a secret marker") can be forged by user data,143        letting attacker-controlled free-form dicts bypass escaping and reach144        the Reviver.145    """146    if not isinstance(obj, Serializable):147        msg = f"Expected Serializable, got {type(obj)}"148        raise TypeError(msg)149 150    serialized: dict[str, Any] = dict(obj.to_json())151 152    # Process kwargs to escape user data that could be confused with LC objects.153    # Skip kwargs declared as secrets - `to_json()` already replaced their154    # values with secret markers via `_replace_secrets`.155    if serialized.get("type") == "constructor" and "kwargs" in serialized:156        secret_keys = _get_secret_keys(obj)157        serialized["kwargs"] = {158            k: v if k in secret_keys else _serialize_value(v)159            for k, v in serialized["kwargs"].items()160        }161 162    return serialized163 164 165def _unescape_value(obj: Any) -> Any:166    """Unescape a value, processing escape markers in dict values and lists.167 168    When an escaped dict is encountered (`{"__lc_escaped__": ...}`), it's169    unwrapped and the contents are returned AS-IS (no further processing).170    The contents represent user data that should not be modified.171 172    For regular dicts and lists, we recurse to find any nested escape markers.173 174    Args:175        obj: The value to unescape.176 177    Returns:178        The unescaped value.179    """180    if isinstance(obj, dict):181        if _is_escaped_dict(obj):182            # Unwrap and return the user data as-is (no further unescaping).183            # The contents are user data that may contain more escape keys,184            # but those are part of the user's actual data.185            return obj[_LC_ESCAPED_KEY]186 187        # Regular dict - recurse into values to find nested escape markers188        return {k: _unescape_value(v) for k, v in obj.items()}189    if isinstance(obj, list):190        return [_unescape_value(item) for item in obj]191    return obj192 
codekingpro/portable-devtools · Team Ai