diff --git a/prowler/providers/image/lib/registry/base.py b/prowler/providers/image/lib/registry/base.py index ee10944275..f33504edc3 100644 --- a/prowler/providers/image/lib/registry/base.py +++ b/prowler/providers/image/lib/registry/base.py @@ -246,37 +246,38 @@ class RegistryAdapter(ABC): message=f"URL has no host: {canonical_url}", ) - if _outbound_check_skipped(): - return canonical_url - - try: - ipaddress.ip_address(host) - except ValueError: + # Only the address classification is skipped. The origin rule below still + # applies: a registry-supplied URL pointing at an unrelated host is a + # different problem from deliberately reaching a private network. + if not _outbound_check_skipped(): try: - infos = socket.getaddrinfo(host, None) - except socket.gaierror: - infos = [] - for *_, sockaddr in infos: - resolved_ip = sockaddr[0] - if _ip_is_non_public(resolved_ip) and not self._ip_is_allowed( - resolved_ip - ): + ipaddress.ip_address(host) + except ValueError: + try: + infos = socket.getaddrinfo(host, None) + except socket.gaierror: + infos = [] + for *_, sockaddr in infos: + resolved_ip = sockaddr[0] + if _ip_is_non_public(resolved_ip) and not self._ip_is_allowed( + resolved_ip + ): + raise ImageRegistryAuthError( + file=__file__, + message=( + f"Host {host!r} resolves to non-public address " + f"{resolved_ip}. {_ALLOWLIST_HINT}" + ), + ) + else: + if _ip_is_non_public(host) and not self._ip_is_allowed(host): raise ImageRegistryAuthError( file=__file__, message=( - f"Host {host!r} resolves to non-public address " - f"{resolved_ip}. {_ALLOWLIST_HINT}" + f"URL targets a non-public address: {host}. " + f"{_ALLOWLIST_HINT}" ), ) - else: - if _ip_is_non_public(host) and not self._ip_is_allowed(host): - raise ImageRegistryAuthError( - file=__file__, - message=( - f"URL targets a non-public address: {host}. " - f"{_ALLOWLIST_HINT}" - ), - ) if enforce_origin: registry_host = urlparse(origin_url or self._origin_url()).hostname or "" diff --git a/tests/providers/image/lib/registry/test_oci_adapter.py b/tests/providers/image/lib/registry/test_oci_adapter.py index 14416186a9..b0fcdd5285 100644 --- a/tests/providers/image/lib/registry/test_oci_adapter.py +++ b/tests/providers/image/lib/registry/test_oci_adapter.py @@ -610,6 +610,28 @@ class TestOutboundCheckOptOut: mock_request.assert_not_called() + @patch("prowler.providers.image.lib.registry.base.requests.request") + def test_the_origin_rule_still_applies_when_skipped( + self, mock_request, monkeypatch + ): + """Skipping the address check must not let a registry redirect us elsewhere.""" + monkeypatch.setenv("PROWLER_SKIP_OUTBOUND_HOST_CHECK", "true") + adapter = OciRegistryAdapter(registry_url="https://registry.example.com") + + with pytest.raises(ImageRegistryAuthError, match="unrelated to registry host"): + adapter._validate_outbound_url("https://attacker.example.net/token") + + mock_request.assert_not_called() + + @patch("prowler.providers.image.lib.registry.base.requests.request") + def test_a_same_origin_url_is_allowed_when_skipped(self, mock_request, monkeypatch): + monkeypatch.setenv("PROWLER_SKIP_OUTBOUND_HOST_CHECK", "true") + adapter = OciRegistryAdapter(registry_url="https://registry.example.com") + + assert adapter._validate_outbound_url( + "https://registry.example.com/v2/token" + ).startswith("https://registry.example.com/") + @patch("prowler.providers.image.lib.registry.base.requests.request") def test_the_scheme_check_still_applies_when_skipped( self, mock_request, monkeypatch