#8085·hummingbot

Security: pickle.loads() on remote HTTP response in RemoteAPIOrderBookDataSource

Author: theeggorchickenCreated Feb 26, 2026Updated Aug 31, 2026

Summary

RemoteAPIOrderBookDataSource.get_tracking_pairs() fetches a binary blob from api.coinalpha.com and deserializes it with pickle.loads() directly, with no integrity check. Python's pickle protocol can instantiate arbitrary objects and execute code during deserialization, so a compromised server or DNS hijack is enough to achieve code execution — HTTPS and Basic Auth protect the transport, not the payload.

I sent a detailed report with reproduction steps and test artifacts to [email protected]. Happy to share the full writeup here or in a private channel.

Affected Code

File: hummingbot/core/data_type/remote_api_order_book_data_source.py#L64-L65

python
binary_data: bytes = await response.read()
order_book_tracker_data: Dict[str, Tuple[pd.DataFrame, pd.DataFrame]] = pickle.loads(binary_data)

Impact

If api.coinalpha.com is ever compromised or the DNS resolution is hijacked, the attacker can return a crafted pickle payload that runs arbitrary code on every machine that calls get_tracking_pairs(). The official Docker image runs as root.

For a bot that holds live exchange API credentials, the threat model really does include server compromise — the credentials themselves are the prize.

Suggested Fix

The cleanest long-term fix is switching the endpoint to JSON. If the binary format needs to stay, a restricted unpickler that allowlists only the expected types stops arbitrary code execution:

python
import io

class SafeOrderBookUnpickler(pickle.Unpickler):
    _ALLOWED = {
        ("pandas.core.frame", "DataFrame"),
        ("builtins", "dict"),
        ("builtins", "tuple"),
        ("builtins", "list"),
        ("builtins", "float"),
        ("builtins", "int"),
        ("builtins", "str"),
        ("numpy", "ndarray"),
        ("numpy.core.multiarray", "_reconstruct"),
        ("numpy", "dtype"),
    }

    def find_class(self, module, name):
        if (module, name) not in self._ALLOWED:
            raise pickle.UnpicklingError(f"Blocked: {module}.{name}")
        return super().find_class(module, name)

order_book_tracker_data = SafeOrderBookUnpickler(io.BytesIO(binary_data)).load()

I've submitted a pull request with this fix and tests.