Get unstuck

Hint 1: Judge the address, not the name or the text

A name like metadata.internal is harmless until you see the address resolve returns. Build ipaddress objects and test ip.is_global; an IP-literal host is its own address.

Hint 2: Let ipaddress do the ranges

ipaddress knows loopback, private, link-local, reserved and documentation ranges. is_global is false for all of them; handle an IPv4-mapped IPv6 address by its .ipv4_mapped form.

Reveal the reference patch and explanation
Fixedurl_policy.pyPython
import ipaddress
from urllib.parse import urlsplit


def check_url(raw_url, resolve):
    try:
        parts = urlsplit(raw_url)
    except ValueError:
        return "block"
    if parts.scheme not in ("http", "https") or parts.username or parts.password:
        return "block"
    host = parts.hostname
    if not host:
        return "block"
    try:
        addresses = [ipaddress.ip_address(host.strip("[]"))]
    except ValueError:
        addresses = [ipaddress.ip_address(a) for a in resolve(host)]
    if not addresses or not all(_public(ip) for ip in addresses):
        return "block"
    return "allow"


def _public(ip):
    mapped = getattr(ip, "ipv4_mapped", None)
    if mapped is not None:
        return _public(mapped)
    return ip.is_global and not ip.is_multicast

A name resolving to 93.184.216.34 is allowed, and that address as a literal is allowed. metadata.internal resolves to 169.254.169.254, whose is_global is false, so it is blocked, as are db.internal (10.0.0.5) and a name that resolves to ::ffff:127.0.0.1 (loopback by its mapped form). ipaddress.ip_address rejects 2130706433, 0x7f000001 and 0177.0.0.1 (it accepts only dotted-decimal IPv4 without leading zeros), so they are resolved as names, and the fixture, like glibc’s resolver on Linux, returns 127.0.0.1 for each: all blocked. [::1] is the IPv6 loopback. A name that returns one public and one private address is blocked, because every address must be global. 224.0.1.1 is blocked as multicast, although is_global is true for it, and http:///latest/meta-data/ has no host to judge. gopher and file URLs, a URL with credentials, an unparseable URL and a name with no address are blocked before or during classification.

Compare it with your own fix, then rerun the tests. The editor is not changed when you open this explanation.

Inspect the actual test harness

The browser and the lab download run the same cases, including cases that catch common wrong fixes, such as comparing with one fixture value instead of the real rule. The download includes the complete test adapter and cryptographic fixtures.

Download the grader, python/cases.mjs
How the browser runs your Python

Your code runs in CPython 3.14.2 compiled to WebAssembly (Pyodide 314.0.7), in a Web Worker of its own. The test cases and the pass or fail verdict run in a second worker, which only sends your functions their inputs and reads back the values they return, so changing built-ins or the harness inside Python cannot change the result. All the cases together have a 8-second limit.

The first run downloads about 6.3 MB from this site, which your browser then keeps; later runs start in a second or two. Other pages never load it.

Available: the Python standard library, including sqlite3, ipaddress, pathlib, os.path and urllib.parse, and print(), whose output appears with your results.

Not available: network access, subprocesses and threads, and other packages from PyPI. The lab download runs the same cases with your local python3 (3.12 or newer).

Use the lesson in your own system

resolve is a deterministic fixture, not real DNS, and the lab validates one URL rather than following redirects (the JavaScript SSRF lab adds redirects). Even a correct is_global check cannot stop DNS rebinding on its own: the address the policy checks must be the address the socket connects to, so a real client pins the resolved address or re-checks after connecting. Prefer an allowlist of destinations, and also bound the response size and time and deny unexpected ports.

Your next task: In your own fetchers, is the host resolved and its address range checked before the request, and is that the same address the connection then uses?

Python ipaddress: is_global and address properties

Related guide: Constrain server-side fetches to their intended destinations