* [meta-python][scarthgap][PATCH] python3-web3: Fix CVE-2026-40072
@ 2026-08-20 5:45 Hetvi Thakar -X (hthakar - E INFOCHIPS PRIVATE LIMITED at Cisco)
0 siblings, 0 replies; only message in thread
From: Hetvi Thakar -X (hthakar - E INFOCHIPS PRIVATE LIMITED at Cisco) @ 2026-08-20 5:45 UTC (permalink / raw)
To: openembedded-devel; +Cc: xe-linux-external, Hetvi Thakar
From: Hetvi Thakar <hthakar@cisco.com>
This patch applies the v7 upstream fix for CVE-2026-40072 shown in [1].
The security advisory [2] references the patch-equivalent v8/main fix [3].
The v7 commit was selected because it is the nearest source baseline to
Scarthgap's web3.py 6.17.0 recipe.
[1] https://github.com/ApeWorX/web3.py/commit/d62e67d3b636bd4c5a929696c0f5c4167c31625b
[2] https://github.com/ApeWorX/web3.py/security/advisories/GHSA-5hr4-253g-cpx2
[3] https://github.com/ApeWorX/web3.py/commit/b1c57bb0a124359c9902daaefab4d8af7c3c4c1e
Signed-off-by: Hetvi Thakar <hthakar@cisco.com>
---
.../python/python3-web3/CVE-2026-40072.patch | 434 ++++++++++++++++++
.../python/python3-web3_6.17.0.bb | 1 +
2 files changed, 435 insertions(+)
create mode 100644 meta-python/recipes-devtools/python/python3-web3/CVE-2026-40072.patch
diff --git a/meta-python/recipes-devtools/python/python3-web3/CVE-2026-40072.patch b/meta-python/recipes-devtools/python/python3-web3/CVE-2026-40072.patch
new file mode 100644
index 0000000000..35e94cde60
--- /dev/null
+++ b/meta-python/recipes-devtools/python/python3-web3/CVE-2026-40072.patch
@@ -0,0 +1,434 @@
+From 21ee858ea75287d781eb0a878d9463346da648b3 Mon Sep 17 00:00:00 2001
+From: fselmo <fselmo2@gmail.com>
+Date: Fri, 13 Mar 2026 15:38:09 -0600
+Subject: [PATCH] feat: added restrictions on CCIP read durin calls
+MIME-Version: 1.0
+Content-Type: text/plain; charset=UTF-8
+Content-Transfer-Encoding: 8bit
+
+SSRF Mitigation for CCIP Read:
+
+- validate_ccip_url_scheme() — HTTPS-only by default; HTTP allowed via opt-in
+- validate_ccip_url_host() / async_validate_ccip_url_host() — resolves hostname and blocks private/reserved IP ranges (127.0.0.0/8, 10.0.0.0/8, 172.16.0.0/12, 192.168.0.0/16, etc.)
+- Type aliases: CcipUrlValidator, AsyncCcipUrlValidator
+- Provider config (base.py, async_base.py):
+- ccip_read_allow_http: bool = False
+- ccip_read_url_validator — optional user-supplied hook to reject/allow URLs
+- Handler changes (exception_handling.py, async_exception_handling.py):
+- Scheme + host validation before each HTTP request
+- allow_redirects=False on all requests
+- Validation failures continue to next URL (consistent with existing error handling)
+
+- Wiring (eth.py, async_eth.py):
+- _durin_call passes provider config to handlers
+
+Tests:
+- tests/core/utilities/test_ccip_url_validation.py — 23 unit tests for scheme/host validation
+- tests/core/contracts/test_offchain_lookup.py — 6 new integration tests (HTTP rejection, allow_http, custom validator, private IP blocking, redirect prevention)
+- Updated test mocks to patch socket.getaddrinfo and assert allow_redirects=False
+
+CVE: CVE-2026-40072
+Upstream-Status: Backport [https://github.com/ApeWorX/web3.py/commit/d62e67d3b636bd4c5a929696c0f5c4167c31625b]
+
+Backport Changes:
+- Retained the v6.17 request helper APIs and passed
+ `allow_redirects=False` through them instead of using the newer direct
+ session APIs.
+- Retained the v6.17 POST `data` payload and malformed-URL checks; the
+ newer source uses a `json` payload and a generic POST fallback.
+- Exported the validator aliases through the v6.17 utility imports; this
+ version has no utility `__all__` list to update.
+- Kept the v6.17 timeout and POST-payload assertions in the request mocks
+ while adding the upstream redirect assertions.
+- Reformatted the synchronous `_durin_call` arguments so the new provider
+ options can be passed on the v6.17 call site.
+- Omitted changes under `tests/core` and `tests/ens` because the PyPI
+ source archive used by this recipe does not ship those directories.
+ The omitted tests were validated separately against the v6.17 Git tag.
+
+(cherry picked from commit d62e67d3b636bd4c5a929696c0f5c4167c31625b)
+Signed-off-by: Hetvi Thakar <hthakar@cisco.com>
+---
+ .../module_testing/module_testing_utils.py | 26 ++++-
+ web3/eth/async_eth.py | 2 +
+ web3/eth/eth.py | 5 +-
+ web3/providers/async_base.py | 5 +
+ web3/providers/base.py | 5 +
+ web3/utils/__init__.py | 4 +
+ web3/utils/async_exception_handling.py | 21 +++-
+ web3/utils/ccip_url_validation.py | 105 ++++++++++++++++++
+ web3/utils/exception_handling.py | 21 +++-
+ 9 files changed, 190 insertions(+), 4 deletions(-)
+ create mode 100644 web3/utils/ccip_url_validation.py
+
+diff --git a/web3/_utils/module_testing/module_testing_utils.py b/web3/_utils/module_testing/module_testing_utils.py
+index 46c82c22..7b05702b 100644
+--- a/web3/_utils/module_testing/module_testing_utils.py
++++ b/web3/_utils/module_testing/module_testing_utils.py
+@@ -89,6 +89,24 @@ def assert_contains_log(
+ assert log_entry["transactionHash"] == HexBytes(txn_hash_with_log)
+
+
++def _mock_getaddrinfo_public(
++ monkeypatch: "MonkeyPatch",
++) -> None:
++ # Patch socket.getaddrinfo to return a public IP for CCIP test domains
++ # so that CCIP URL host validation passes during tests. Pass through
++ # to the real getaddrinfo for all other hosts (e.g. 127.0.0.1 for geth).
++ import socket as _socket
++
++ _original_getaddrinfo = _socket.getaddrinfo
++
++ def _patched_getaddrinfo(host: Any, port: Any, *args: Any, **kwargs: Any) -> Any:
++ if host == "web3.py":
++ return [(_socket.AF_INET, _socket.SOCK_STREAM, 0, "", ("1.2.3.4", 0))]
++ return _original_getaddrinfo(host, port, *args, **kwargs)
++
++ monkeypatch.setattr("socket.getaddrinfo", _patched_getaddrinfo)
++
++
+ def mock_offchain_lookup_request_response(
+ monkeypatch: "MonkeyPatch",
+ http_method: Literal["GET", "POST"] = "GET",
+@@ -100,6 +118,8 @@ def mock_offchain_lookup_request_response(
+ sender: str = None,
+ calldata: str = None,
+ ) -> None:
++ _mock_getaddrinfo_public(monkeypatch)
++
+ class MockedResponse:
+ status_code = mocked_status_code
+
+@@ -119,6 +139,7 @@ def mock_offchain_lookup_request_response(
+ # mock response only to specified url while validating appropriate fields
+ if url_from_args == mocked_request_url:
+ assert kwargs["timeout"] == 10
++ assert kwargs.get("allow_redirects") is False
+ if http_method.upper() == "POST":
+ assert kwargs["data"] == {"data": calldata, "sender": sender}
+ return MockedResponse()
+@@ -146,6 +167,8 @@ def async_mock_offchain_lookup_request_response(
+ sender: str = None,
+ calldata: str = None,
+ ) -> None:
++ _mock_getaddrinfo_public(monkeypatch)
++
+ class AsyncMockedResponse:
+ status = mocked_status_code
+
+@@ -169,7 +192,8 @@ def async_mock_offchain_lookup_request_response(
+ # mock response only to specified url while validating appropriate fields
+ if url_from_args == mocked_request_url:
+ assert kwargs["timeout"] == ClientTimeout(10)
+- if http_method.upper() == "post":
++ assert kwargs.get("allow_redirects") is False
++ if http_method.upper() == "POST":
+ assert kwargs["data"] == {"data": calldata, "sender": sender}
+ return AsyncMockedResponse()
+
+diff --git a/web3/eth/async_eth.py b/web3/eth/async_eth.py
+index b6412a59..14d5679a 100644
+--- a/web3/eth/async_eth.py
++++ b/web3/eth/async_eth.py
+@@ -293,6 +293,8 @@ class AsyncEth(BaseEth):
+ durin_calldata = await async_handle_offchain_lookup(
+ offchain_lookup.payload,
+ transaction,
++ allow_http=self.w3.provider.ccip_read_allow_http,
++ url_validator=self.w3.provider.ccip_read_url_validator,
+ )
+ transaction["data"] = durin_calldata
+
+diff --git a/web3/eth/eth.py b/web3/eth/eth.py
+index 6e1700ca..e11623a7 100644
+--- a/web3/eth/eth.py
++++ b/web3/eth/eth.py
+@@ -279,7 +279,10 @@ class Eth(BaseEth):
+ return self._call(transaction, block_identifier, state_override)
+ except OffchainLookup as offchain_lookup:
+ durin_calldata = handle_offchain_lookup(
+- offchain_lookup.payload, transaction
++ offchain_lookup.payload,
++ transaction,
++ allow_http=self.w3.provider.ccip_read_allow_http,
++ url_validator=self.w3.provider.ccip_read_url_validator,
+ )
+ transaction["data"] = durin_calldata
+
+diff --git a/web3/providers/async_base.py b/web3/providers/async_base.py
+index 30404b6b..64b9f7f8 100644
+--- a/web3/providers/async_base.py
++++ b/web3/providers/async_base.py
+@@ -38,6 +38,9 @@ if TYPE_CHECKING:
+ AsyncWeb3,
+ WebsocketProviderV2,
+ )
++ from web3.utils.ccip_url_validation import (
++ AsyncCcipUrlValidator,
++ )
+
+
+ class AsyncBaseProvider:
+@@ -54,6 +57,8 @@ class AsyncBaseProvider:
+ has_persistent_connection = False
+ global_ccip_read_enabled: bool = True
+ ccip_read_max_redirects: int = 4
++ ccip_read_allow_http: bool = False
++ ccip_read_url_validator: "AsyncCcipUrlValidator | None" = None
+
+ @property
+ def middlewares(self) -> Tuple[AsyncMiddleware, ...]:
+diff --git a/web3/providers/base.py b/web3/providers/base.py
+index d7877546..5d91f635 100644
+--- a/web3/providers/base.py
++++ b/web3/providers/base.py
+@@ -32,6 +32,9 @@ from web3.types import (
+
+ if TYPE_CHECKING:
+ from web3 import Web3 # noqa: F401
++ from web3.utils.ccip_url_validation import (
++ CcipUrlValidator,
++ )
+
+
+ class BaseProvider:
+@@ -46,6 +49,8 @@ class BaseProvider:
+ has_persistent_connection = False
+ global_ccip_read_enabled: bool = True
+ ccip_read_max_redirects: int = 4
++ ccip_read_allow_http: bool = False
++ ccip_read_url_validator: "CcipUrlValidator | None" = None
+
+ @property
+ def middlewares(self) -> Tuple[Middleware, ...]:
+diff --git a/web3/utils/__init__.py b/web3/utils/__init__.py
+index 13c24de6..2c4f1d94 100644
+--- a/web3/utils/__init__.py
++++ b/web3/utils/__init__.py
+@@ -14,6 +14,10 @@ from .async_exception_handling import ( # NOQA
+ from .caching import ( # NOQA
+ SimpleCache,
+ )
++from .ccip_url_validation import ( # NOQA
++ AsyncCcipUrlValidator,
++ CcipUrlValidator,
++)
+ from .exception_handling import ( # NOQA
+ handle_offchain_lookup,
+ )
+diff --git a/web3/utils/async_exception_handling.py b/web3/utils/async_exception_handling.py
+index 0619bd5b..e4fa933d 100644
+--- a/web3/utils/async_exception_handling.py
++++ b/web3/utils/async_exception_handling.py
+@@ -26,11 +26,18 @@ from web3.exceptions import (
+ from web3.types import (
+ TxParams,
+ )
++from web3.utils.ccip_url_validation import (
++ AsyncCcipUrlValidator,
++ async_validate_ccip_url_host,
++ validate_ccip_url_scheme,
++)
+
+
+ async def async_handle_offchain_lookup(
+ offchain_lookup_payload: Dict[str, Any],
+ transaction: TxParams,
++ allow_http: bool = False,
++ url_validator: AsyncCcipUrlValidator | None = None,
+ ) -> bytes:
+ formatted_sender = to_hex_if_bytes(offchain_lookup_payload["sender"]).lower()
+ formatted_data = to_hex_if_bytes(offchain_lookup_payload["callData"]).lower()
+@@ -48,13 +55,25 @@ async def async_handle_offchain_lookup(
+ .replace("{data}", str(formatted_data))
+ )
+
++ try:
++ validate_ccip_url_scheme(formatted_url, allow_http=allow_http)
++ await async_validate_ccip_url_host(formatted_url)
++ if url_validator is not None:
++ await url_validator(formatted_url)
++ except Web3ValidationError:
++ continue
++
+ try:
+ if "{data}" in url and "{sender}" in url:
+- response = await async_get_response_from_get_request(formatted_url)
++ response = await async_get_response_from_get_request(
++ formatted_url,
++ allow_redirects=False,
++ )
+ elif "{sender}" in url:
+ response = await async_get_response_from_post_request(
+ formatted_url,
+ data={"data": formatted_data, "sender": formatted_sender},
++ allow_redirects=False,
+ )
+ else:
+ raise Web3ValidationError("url not formatted properly.")
+diff --git a/web3/utils/ccip_url_validation.py b/web3/utils/ccip_url_validation.py
+new file mode 100644
+index 00000000..a86618d8
+--- /dev/null
++++ b/web3/utils/ccip_url_validation.py
+@@ -0,0 +1,105 @@
++import asyncio
++import ipaddress
++import socket
++from typing import (
++ Awaitable,
++ Callable,
++)
++from urllib.parse import (
++ urlparse,
++)
++
++from web3.exceptions import (
++ Web3ValidationError,
++)
++
++CcipUrlValidator = Callable[[str], None]
++AsyncCcipUrlValidator = Callable[[str], Awaitable[None]]
++
++BLOCKED_IP_NETWORKS = [
++ ipaddress.ip_network("127.0.0.0/8"),
++ ipaddress.ip_network("10.0.0.0/8"),
++ ipaddress.ip_network("172.16.0.0/12"),
++ ipaddress.ip_network("192.168.0.0/16"),
++ ipaddress.ip_network("169.254.0.0/16"),
++ ipaddress.ip_network("0.0.0.0/8"),
++ ipaddress.ip_network("::1/128"),
++ ipaddress.ip_network("fe80::/10"),
++ ipaddress.ip_network("fc00::/7"),
++ ipaddress.ip_network("::/128"),
++]
++
++
++def validate_ccip_url_scheme(url: str, allow_http: bool = False) -> None:
++ parsed = urlparse(url)
++ scheme = parsed.scheme.lower()
++
++ if scheme == "https":
++ return
++
++ if scheme == "http" and allow_http:
++ return
++
++ if scheme == "http":
++ raise Web3ValidationError(
++ f"CCIP Read request to non-HTTPS URL '{url}' is not allowed. "
++ "Set ``ccip_read_allow_http=True`` on the provider to allow HTTP URLs."
++ )
++
++ raise Web3ValidationError(
++ f"CCIP Read request with scheme '{scheme}' is not allowed. "
++ "Only HTTPS URLs are permitted."
++ )
++
++
++def _check_ip_blocked(ip_str: str) -> bool:
++ try:
++ addr = ipaddress.ip_address(ip_str)
++ except ValueError:
++ return False
++ return any(addr in network for network in BLOCKED_IP_NETWORKS)
++
++
++def validate_ccip_url_host(url: str) -> None:
++ parsed = urlparse(url)
++ hostname = parsed.hostname
++ if not hostname:
++ raise Web3ValidationError(f"CCIP Read URL '{url}' has no hostname.")
++
++ try:
++ addrinfos = socket.getaddrinfo(hostname, None)
++ except socket.gaierror:
++ raise Web3ValidationError(
++ f"CCIP Read URL hostname '{hostname}' could not be resolved."
++ )
++
++ for addrinfo in addrinfos:
++ ip_str = str(addrinfo[4][0])
++ if _check_ip_blocked(ip_str):
++ raise Web3ValidationError(
++ f"CCIP Read request to '{url}' is not allowed: "
++ f"resolved IP '{ip_str}' is in a blocked private/reserved range."
++ )
++
++
++async def async_validate_ccip_url_host(url: str) -> None:
++ parsed = urlparse(url)
++ hostname = parsed.hostname
++ if not hostname:
++ raise Web3ValidationError(f"CCIP Read URL '{url}' has no hostname.")
++
++ loop = asyncio.get_running_loop()
++ try:
++ addrinfos = await loop.run_in_executor(None, socket.getaddrinfo, hostname, None)
++ except socket.gaierror:
++ raise Web3ValidationError(
++ f"CCIP Read URL hostname '{hostname}' could not be resolved."
++ )
++
++ for addrinfo in addrinfos:
++ ip_str = str(addrinfo[4][0])
++ if _check_ip_blocked(ip_str):
++ raise Web3ValidationError(
++ f"CCIP Read request to '{url}' is not allowed: "
++ f"resolved IP '{ip_str}' is in a blocked private/reserved range."
++ )
+diff --git a/web3/utils/exception_handling.py b/web3/utils/exception_handling.py
+index 77a46fc6..1d5ee0bd 100644
+--- a/web3/utils/exception_handling.py
++++ b/web3/utils/exception_handling.py
+@@ -25,11 +25,18 @@ from web3.exceptions import (
+ from web3.types import (
+ TxParams,
+ )
++from web3.utils.ccip_url_validation import (
++ CcipUrlValidator,
++ validate_ccip_url_host,
++ validate_ccip_url_scheme,
++)
+
+
+ def handle_offchain_lookup(
+ offchain_lookup_payload: Dict[str, Any],
+ transaction: TxParams,
++ allow_http: bool = False,
++ url_validator: CcipUrlValidator | None = None,
+ ) -> bytes:
+ formatted_sender = to_hex_if_bytes(offchain_lookup_payload["sender"]).lower()
+ formatted_data = to_hex_if_bytes(offchain_lookup_payload["callData"]).lower()
+@@ -47,9 +54,20 @@ def handle_offchain_lookup(
+ .replace("{data}", str(formatted_data))
+ )
+
++ try:
++ validate_ccip_url_scheme(formatted_url, allow_http=allow_http)
++ validate_ccip_url_host(formatted_url)
++ if url_validator is not None:
++ url_validator(formatted_url)
++ except Web3ValidationError:
++ continue
++
+ try:
+ if "{data}" in url and "{sender}" in url:
+- response = get_response_from_get_request(formatted_url)
++ response = get_response_from_get_request(
++ formatted_url,
++ allow_redirects=False,
++ )
+ elif "{sender}" in url:
+ response = get_response_from_post_request(
+ formatted_url,
+@@ -57,6 +75,7 @@ def handle_offchain_lookup(
+ "data": formatted_data,
+ "sender": formatted_sender,
+ },
++ allow_redirects=False,
+ )
+ else:
+ raise Web3ValidationError("url not formatted properly.")
+--
+2.35.6
diff --git a/meta-python/recipes-devtools/python/python3-web3_6.17.0.bb b/meta-python/recipes-devtools/python/python3-web3_6.17.0.bb
index f1be4dcf4d..6c093c794d 100644
--- a/meta-python/recipes-devtools/python/python3-web3_6.17.0.bb
+++ b/meta-python/recipes-devtools/python/python3-web3_6.17.0.bb
@@ -4,6 +4,7 @@ SECTION = "devel/python"
LICENSE = "MIT"
LIC_FILES_CHKSUM = "file://LICENSE;md5=373fede350846fdffd23648fba504635"
+SRC_URI += "file://CVE-2026-40072.patch"
SRC_URI[sha256sum] = "1b535272a40da3d8d2b120856edb53b84b0c08bcc8fe1a5bbd5f816fd72f4ec6"
inherit pypi setuptools3
--
2.35.6
^ permalink raw reply related [flat|nested] only message in thread
only message in thread, other threads:[~2026-08-20 5:47 UTC | newest]
Thread overview: (only message) (download: mbox.gz follow: Atom feed
-- links below jump to the message on this page --
2026-08-20 5:45 [meta-python][scarthgap][PATCH] python3-web3: Fix CVE-2026-40072 Hetvi Thakar -X (hthakar - E INFOCHIPS PRIVATE LIMITED at Cisco)
This is an external index of several public inboxes,
see mirroring instructions on how to clone and mirror
all data and code used by this external index.