Skip to content

Security: Replace pickle serialization with JSON (CWE-502)#1646

Open
jamestford wants to merge 1 commit into
jmfernandes:masterfrom
jamestford:fix/replace-pickle-with-json
Open

Security: Replace pickle serialization with JSON (CWE-502)#1646
jamestford wants to merge 1 commit into
jmfernandes:masterfrom
jamestford:fix/replace-pickle-with-json

Conversation

@jamestford

Copy link
Copy Markdown

Summary

This PR replaces all uses of Python pickle for session file storage with json, eliminating a class of arbitrary code execution vulnerabilities (CWE-502: Deserialization of Untrusted Data).

Why This Matters

Python's pickle module can execute arbitrary code during deserialization. When pickle.load() is called on a file, it can instantiate any Python object and run any code — including malware, reverse shells, or file deletion. This is not a theoretical risk; it is one of the most well-documented attack vectors in Python.

Current risk scenario: An attacker with write access to ~/.tokens/robinhood.pickle (or equivalent TDA file) can replace it with a crafted pickle payload. The next time the user's script calls login(), the malicious code executes with full user privileges — no password needed, no warning given.

This has been flagged by three independent security scanners in our audit:

  • Semgrep (WARNING: avoid-pickle)
  • Bandit (MEDIUM: B301 — "Pickle and modules that wrap it can be unsafe")
  • Bearer (CRITICAL: CWE-502 — "Usage of unsafe Pickle libraries")

What This PR Changes

Robinhood (robin_stocks/robinhood/authentication.py)

  • import pickleimport json
  • Session file format: .pickle (binary) → .json (plaintext)
  • pickle.load(f)json.load(f)
  • pickle.dump(data, f)json.dump(data, f, indent=2)
  • File open mode: rb/wbr/w

TDA (robin_stocks/tda/authentication.py)

  • import pickleimport json + import base64
  • Encrypted token bytes → base64-encoded strings (JSON-compatible)
  • datetime objects → ISO 8601 strings (datetime.isoformat() / datetime.fromisoformat())
  • All pickle.dump/pickle.load calls replaced with JSON equivalents

TDA Globals (robin_stocks/tda/globals.py)

  • PICKLE_NAME = "tda.pickle"SESSION_NAME = "tda.json" with backward-compatible alias

Version

  • Bumped to 3.5.0 (feature-level change due to file format change)

Do We Lose Any Functionality?

No. The session data consists entirely of:

  • Authentication tokens (strings)
  • Device tokens (strings)
  • Timestamps (datetime → ISO string)
  • Encrypted bytes (→ base64 strings)

All of these are natively representable in JSON. Pickle's ability to serialize arbitrary Python objects is the vulnerability, not a feature being used by this codebase.

Breaking Change

Existing .pickle session files will not be loaded by the updated code. Users will need to log in once after updating, which creates a new .json session file. This is the same experience as when a pickle file becomes corrupted (issue #451, #524).

The parameter names pickle_path and pickle_name are preserved for backward API compatibility.

Related Issues

This PR addresses the root cause behind several existing issues:

Testing

The session data structure is unchanged — only the serialization format differs. All existing tests that don't directly test pickle file internals should pass without modification. The login() and logout() function signatures are unchanged.

- Replace pickle with JSON for session file storage (CWE-502)
- Robinhood: session files now stored as .json instead of .pickle
- TDA: encrypted tokens stored as base64-encoded strings in JSON
- TDA: datetime objects stored as ISO format strings
- Bumped version to 3.5.0

BREAKING CHANGE: Existing .pickle session files will not be loaded.
Users will need to re-authenticate once after updating.
DhruvaBansal00 added a commit to DhruvaBansal00/robin_stocks that referenced this pull request May 23, 2026
Upstream PR jmfernandes#1646 (jmfernandes/robin_stocks). Replaces pickle with JSON
for the Robinhood and TDA session caches. Eliminates CWE-502 (arbitrary
code execution via crafted pickle on disk) and makes the cache files
human-readable.

For TDA, the encrypted credential bytes are base64-encoded before being
written, and datetime stamps are stored as ISO-8601 strings.

Also threads the format change through the MCP layer:
- robin_stocks_mcp/auth.py now reads ~/.tokens/robinhood.json via
  json.load() and reports session-reuse failures with the new wording.
- The existing auth-bootstrap tests now write JSON instead of pickle,
  plus a new test confirms a malformed session file does not abort
  the bootstrap.
- Help text and printed messages no longer reference "pickle".

Breaking change: existing ~/.tokens/robinhood.pickle and ~/.tokens/tda.pickle
files will not be loaded — users get prompted to log in once.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
DhruvaBansal00 added a commit to DhruvaBansal00/robin_stocks that referenced this pull request May 23, 2026
DhruvaBansal00 added a commit to DhruvaBansal00/robin_stocks that referenced this pull request May 23, 2026
Adds a credential-free, fully-mocked test suite covering the changes
landed across the 12 upstream-PR merges plus the surrounding SDK.

New tests/sdk/ tree (mock-based, no network/credentials):
- test_urls: every URL builder we touched (options, futures, recurring,
  tax-lots, marketdata-index)
- test_helper / test_helper_requests: index routing, rate limiter +
  thread safety, request_get/post/delete/document branches, filter_data
- test_authentication_robinhood / test_auth_verification: login JSON
  persistence (jmfernandes#1646), store_session=False fix (jmfernandes#1643), the device
  verification polling flow
- test_authentication_tda: JSON + base64 + Fernet roundtrip, refresh
  branches
- test_orders / test_order_wrappers / test_option_crypto_orders:
  market_hours coupling (jmfernandes#454), tax-lot payloads, every order wrapper
- test_options_index_routing / test_options_remaining: index chain
  symbols (jmfernandes#541), find_options_by_* variants, historicals validation
- test_stocks / test_stocks_index_routing: quote routing, historicals
- test_crypto: open-positions nonzero filter (jmfernandes#333)
- test_futures: full module coverage incl. pagination + P&L helpers
- test_recurring_investments: create/update/cancel payloads + branches
- test_account / test_markets / test_misc_modules: legacy sweeps

New tests/mcp tests:
- test_all_tools_dispatch: introspection-based generic dispatch test
  that verifies all 206 registered MCP tools forward to their SDK
  function (synthesizes args from each tool's signature)
- test_remaining_tools: download + TDA-auth tools with bespoke returns

Results: 801 passing, 1 skipped. Coverage: 100% on every MCP tool
module and on the modules we authored (futures, recurring_investments,
markets, tda/authentication); 92% overall across robinhood + MCP.
Remaining gaps are legacy SDK paths untouched by these PRs.

Also adds pytest-cov to the dev extra and ignores coverage artifacts.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@DhruvaBansal00

DhruvaBansal00 commented May 23, 2026

Copy link
Copy Markdown

Thanks for this contribution! jmfernandes/robin_stocks appears to be unmaintained, so I've started a community-maintained fork at https://github.com/DhruvaBansal00/robin_stocks_v2 and merged this PR there: DhruvaBansal00/robin_stocks_v2@4f13fce

It's included with test coverage in the new repo, alongside 11 other open PRs from here. If you'd like a maintained build with this change, that's where to find it.

Shipped as-is; note it's a breaking change for existing pickle session files (one-time re-login).

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants