Skip to content

Python: block cloud metadata endpoints and IPv6-embedded IPv4 in server_url_validator - #14242

Open
ErenAta16 wants to merge 1 commit into
microsoft:mainfrom
ErenAta16:block-metadata-and-embedded-ipv4
Open

Python: block cloud metadata endpoints and IPv6-embedded IPv4 in server_url_validator#14242
ErenAta16 wants to merge 1 commit into
microsoft:mainfrom
ErenAta16:block-metadata-and-embedded-ipv4

Conversation

@ErenAta16

Copy link
Copy Markdown

Motivation and Context

Fixes #14240.

server_url_validator is the default SSRF guard for OpenAPI plugin calls (allow_private_network_access defaults to False), 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_ipv4 works by octet ranges, so 168.63.129.16 — Azure's WireServer, reachable from every Azure VM — matches nothing:

address before after
169.254.169.254 (AWS/GCP/Azure/OCI/DO IMDS) blocked (link-local) blocked
169.254.170.2 (AWS ECS task creds) blocked (link-local) blocked
169.254.170.23 (AWS EKS Pod Identity) blocked (link-local) blocked
169.254.42.42 (Scaleway) blocked (link-local) blocked
100.100.100.200 (Alibaba) blocked (CGNAT) blocked
192.0.0.192 (Oracle Cloud Classic) blocked (reserved) blocked
168.63.129.16 (Azure WireServer) allowed blocked

IPv6 forms carrying an IPv4 target. _try_classify_ipv6 unwraps ::ffff: through ipv4_mapped but doesn't decode the tunnel encodings:

form before after
::ffff:169.254.169.254 (IPv4-mapped) blocked blocked
64:ff9b::169.254.169.254 (NAT64, RFC 6052) allowed blocked
2002:a9fe:a9fe:: (6to4, RFC 3056) allowed blocked
Teredo (RFC 4380, low 32 bits XOR'd) allowed blocked

Description

CLOUD_METADATA_ADDRESSES is checked before the range logic, and also when allow_private_network_access is 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:

default allow_private_network_access=True
10.0.0.5 blocked allowed
127.0.0.1 blocked allowed
1.1.1.1 allowed allowed
any metadata address blocked blocked

_embedded_ipv4s decodes NAT64, 6to4, Teredo and IPv4-mapped, and try_categorize_non_public_address classifies 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::/48 prefix and it produced false positives — the shorter-prefix offsets read bytes that aren't the embedded address, yielding 0.0.0.0, so 64:ff9b:1::1.1.1.1 (a legitimate NAT64 address pointing at a public IPv4) came back blocked as unspecified. 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_allowed pins that.

How did you test it?

tests/unit/connectors/openapi_plugin/test_server_url_validator.py:

  • the seven metadata addresses, parametrised twice (default and allow_private_network_access=True)
  • RFC1918 and loopback under allow_private_network_access=True, so the option isn't accidentally neutered
  • the four IPv6 encodings carrying 169.254.169.254, plus a Teredo address built by XOR-ing the low 32 bits
  • four public targets (64:ff9b::1.1.1.1, 64:ff9b:1::1.1.1.1, 2002:0101:0101::, 2606:4700:4700::1111) that must stay allowed

Stashing only server_url_validator.py gives 12 failures; with the change the file is 77 passed. ruff check and ruff format --check clean.

Notes for the reviewer

Two existing tests moved off 169.254.169.254 onto 169.254.10.10. The address is still blocked — it now reports the metadata message instead of link-local, so their match= 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=True to 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

…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
Copilot AI review requested due to automatic review settings July 29, 2026 19:26
@ErenAta16
ErenAta16 requested a review from a team as a code owner July 29, 2026 19:26

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 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_access behavior, 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.

Comment on lines +144 to +146
if any(address in network for network in _NAT64_NETWORKS):
candidates.append(ipaddress.IPv4Address(bytes(packed[offset] for offset in _NAT64_IPV4_OFFSETS)))

@github-actions github-actions Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Automated Code Review

Reviewers: 5 | Confidence: 93% | Result: All clear

Reviewed: Correctness, Security Reliability, Test Coverage, Failure Modes, Design Approach


Automated review by ErenAta16's agents

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Python: server_url_validator allows Azure WireServer (168.63.129.16) and IPv6-embedded IPv4 forms (NAT64/6to4/Teredo)

2 participants