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
