web3.py, the dominant Python library for Ethereum interactions with over 10 million downloads on PyPI, ships with a server-side request forgery (SSRF) vulnerability in its CCIP Read implementation. Any backend, indexer, or API using its .call() method on untrusted contracts risks attackers forcing outbound HTTP requests to internal services, cloud metadata endpoints, or anywhere else.
The root cause lies in EIP-3668, CCIP Read, which lets smart contracts return an OffchainLookup error containing URLs for off-chain data fetches. web3.py automatically follows these contract-supplied URLs—after minimal template substitution for {sender} and {data}—without validation. It accepts HTTP and HTTPS equally, ignores private IP ranges like 127.0.0.1 or 10.0.0.0/8 (RFC 1918), permits any hostname, and follows redirects blindly via the underlying requests or aiohttp libraries.
CCIP Read activates by default. Set global_ccip_read_enabled = True across all providers. Developers calling contract.functions.someMethod().call() on a user-supplied address trigger it silently on the specific revert pattern, exposing services without opt-in.
Real-World Impact
SSRF here means a malicious contract turns your web3.py process into an attack vector. Attackers craft contracts that revert with payloads pointing to localhost services (e.g., http://127.0.0.1:8080/admin), internal databases, or cloud instance metadata like AWS IMDS at http://169.254.169.254/latest/meta-data/iam/security-credentials/. In indexers like those powering Dune Analytics or The Graph, or DeFi APIs querying untrusted contracts, this leaks sensitive data or pivots to lateral movement.
Scale matters: web3.py underpins thousands of projects. A quick scan of GitHub shows it in 50,000+ repos. DeFi protocols, NFT marketplaces, and RPC nodes all use .call() for static calls. One compromised indexer could scan entire internal networks. Past SSRF exploits, like the 2021 Capital One breach via metadata access, cost millions; blockchain backends hold wallet keys and API secrets, amplifying damage.
EIP-3668 itself flags these risks, urging libraries to offer hooks for URL rewriting, proxying, allow/block lists, or outright denial. web3.py ignores this: no such controls exist. Enabling a feature that dials attacker-chosen phones by default defies security basics. Libraries issuing outbound requests—think requests itself blocks some redirects—owe safe defaults, not “user responsibility.”
Technical Details and Proof
Affected code sits in web3/utils/exception_handling.py, lines 42-58 for sync handling. When a contract reverts with OffchainLookup(urls=..., ...), web3.py extracts offchain_lookup_payload["urls"] and fires GET requests:
# Simplified from web3.py source
def handle_offchain_lookup(provider, offchain_lookup_payload):
urls = offchain_lookup_payload["urls"]
# Template sub: replace {sender}, {data}
for url in urls:
substituted = substitute_template(url, ...)
response = requests.get(substituted) # No validation!
# Aggregate responses, retry call
Test it: Deploy a contract reverting to OffchainLookup(urls=["http://169.254.169.254/latest/meta-data/"]). Call it via web3.py in an AWS Lambda or EC2. Capture logs or metadata responses. Same for http://localhost:9200/_cat/indices against an Elasticsearch cluster.
Async handling in aiohttp mirrors this, following redirects up to 30 by default.
Fixes and Why They Matter Now
Disable it: from web3 import Web3; Web3.global_ccip_read_enabled = False. But that’s reactive. Demand library fixes: Add ccip_read_url_validator hook, default-block private IPs (use ipaddress module), enforce HTTPS unless opted-in, validate redirect chains.
web3.py maintainers should align with EIP-3668 guidance. Until patched—check GitHub issues—audit your stack. If you index user contracts or expose eth_call RPCs, you’re live fire. In crypto, where $600B+ TVL hangs on code, silent SSRF primitives erode trust faster than hacks. Act: Test your endpoints, isolate networks, monitor outbound traffic. This isn’t hype—it’s a loaded gun in your backend.