Python: block cloud metadata endpoints and IPv6-embedded IPv4 in server_url_validator - #14242
Python: block cloud metadata endpoints and IPv6-embedded IPv4 in server_url_validator#14242ErenAta16 wants to merge 1 commit into
Conversation
…er_url_validator server_url_validator is the default SSRF guard for OpenAPI plugin calls, and the URL it sees is shaped by the model through server variables and path parameters. Two categories get through it: 168.63.129.16 -- Azure WireServer, reachable from every Azure VM. It is publicly routable, so none of the _try_classify_ipv4 branches match it. NAT64, 6to4 and Teredo addresses. _try_classify_ipv6 unwraps ::ffff: via ipv4_mapped but does not decode the other encodings, so 64:ff9b::169.254.169.254 and 2002:a9fe:a9fe:: both pass. Add an explicit metadata denylist, checked before the range logic and also when allow_private_network_access is set: reaching a host on your own network and reaching the instance's credential endpoint are different requests, and only the first is what that option is for. RFC1918 and loopback still pass with the option on. Decode the embedded IPv4 for NAT64 (RFC 6052 / RFC 8215), 6to4 (RFC 3056) and Teredo (RFC 4380) and classify that. Only the /96 embedding is decoded: guessing the shorter lengths inside the local-use prefix reads bytes that are not the embedded address, which rejected legitimate NAT64 targets like 64:ff9b:1::1.1.1.1 -- there is a test pinning that those stay allowed. Two existing tests moved off 169.254.169.254 onto a plain link-local address. It is still blocked, now with the more specific metadata message, and the new tests assert on it directly. Closes microsoft#14240
There was a problem hiding this comment.
🟡 Not ready to approve
NAT64 decoding currently applies to all addresses in 64:ff9b:1::/48, which can cause false-positive blocking for non-/96 embeddings and should be restricted to the /96-embedding shape.
Once you've addressed the issues Copilot identified, you can request another Copilot review.
This review doesn't count toward merge requirements. Sign up for the private preview to control whether Copilot approvals count.
Pull request overview
This PR hardens the Python OpenAPI plugin SSRF guard (server_url_validator) by explicitly blocking known cloud metadata/credential endpoints (including Azure WireServer) and by detecting IPv6 encodings that embed an IPv4 target (NAT64/6to4/Teredo/IPv4-mapped) before categorization.
Changes:
- Add an explicit denylist for cloud metadata endpoints and enforce it even when
allow_private_network_access=True. - Decode IPv4 targets embedded in certain IPv6 forms and classify the decoded IPv4 for non-public ranges.
- Expand unit tests to cover cloud metadata endpoints,
allow_private_network_accessbehavior, and IPv6 embedded-IPv4 cases.
File summaries
| File | Description |
|---|---|
python/semantic_kernel/connectors/openapi_plugin/server_url_validator.py |
Adds cloud metadata endpoint blocking and IPv6 embedded-IPv4 decoding for SSRF protection. |
python/tests/unit/connectors/openapi_plugin/test_server_url_validator.py |
Adds/adjusts unit tests for metadata endpoint blocking and embedded IPv4-in-IPv6 rejection/allow cases. |
Review details
- Files reviewed: 2/2 changed files
- Comments generated: 1
- Review effort level: Low
We're testing this review assessment. Please use 👍 or 👎 to tell us if it's correct.
| if any(address in network for network in _NAT64_NETWORKS): | ||
| candidates.append(ipaddress.IPv4Address(bytes(packed[offset] for offset in _NAT64_IPV4_OFFSETS))) | ||
|
|
Motivation and Context
Fixes #14240.
server_url_validatoris the default SSRF guard for OpenAPI plugin calls (allow_private_network_accessdefaults toFalse), and the URL it validates is shaped by the model through server variables and path parameters. Two categories get past it.A publicly routable metadata endpoint.
_try_classify_ipv4works by octet ranges, so168.63.129.16— Azure's WireServer, reachable from every Azure VM — matches nothing:169.254.169.254(AWS/GCP/Azure/OCI/DO IMDS)169.254.170.2(AWS ECS task creds)169.254.170.23(AWS EKS Pod Identity)169.254.42.42(Scaleway)100.100.100.200(Alibaba)192.0.0.192(Oracle Cloud Classic)168.63.129.16(Azure WireServer)IPv6 forms carrying an IPv4 target.
_try_classify_ipv6unwraps::ffff:throughipv4_mappedbut doesn't decode the tunnel encodings:::ffff:169.254.169.254(IPv4-mapped)64:ff9b::169.254.169.254(NAT64, RFC 6052)2002:a9fe:a9fe::(6to4, RFC 3056)Description
CLOUD_METADATA_ADDRESSESis checked before the range logic, and also whenallow_private_network_accessis set. That option reads as "let me reach hosts on my own network"; reaching the instance's credential endpoint is a different request. RFC1918 and loopback still pass with it on:allow_private_network_access=True10.0.0.5127.0.0.11.1.1.1_embedded_ipv4sdecodes NAT64, 6to4, Teredo and IPv4-mapped, andtry_categorize_non_public_addressclassifies whatever comes out, reporting e.g.link-local (embedded in IPv6).Only the /96 NAT64 embedding is decoded. I started with all the RFC 6052 §2.2 lengths for the RFC 8215 local-use
64:ff9b:1::/48prefix and it produced false positives — the shorter-prefix offsets read bytes that aren't the embedded address, yielding0.0.0.0, so64:ff9b:1::1.1.1.1(a legitimate NAT64 address pointing at a public IPv4) came back blocked asunspecified. Reading only bytes 12-15 covers the well-known prefix exactly and the local-use prefix for the /96 embedding, with no false positives.test_ipv6_forms_carrying_a_public_ipv4_are_allowedpins that.How did you test it?
tests/unit/connectors/openapi_plugin/test_server_url_validator.py:allow_private_network_access=True)allow_private_network_access=True, so the option isn't accidentally neutered169.254.169.254, plus a Teredo address built by XOR-ing the low 32 bits64:ff9b::1.1.1.1,64:ff9b:1::1.1.1.1,2002:0101:0101::,2606:4700:4700::1111) that must stay allowedStashing only
server_url_validator.pygives 12 failures; with the change the file is 77 passed.ruff checkandruff format --checkclean.Notes for the reviewer
Two existing tests moved off
169.254.169.254onto169.254.10.10. The address is still blocked — it now reports the metadata message instead oflink-local, so theirmatch=no longer applied. The new tests assert on it directly with a stricter match.Behaviour change worth calling out: anyone currently using
allow_private_network_access=Trueto reach a metadata endpoint will now get an error. That felt right for a control whose stated purpose is SSRF prevention, but I'm happy to put it behind its own flag instead.Checklist