microsoft / microsoft/semantic-kernel

OpenAPI plugin SSRF validator: resolved IP is not pinned for the connection (DNS check-time vs use-time gap)

Open
#14,312 0 comments 0 reactions 0 assignees View on GitHub

Nobody has claimed this yet.

Dominant language
C#
Stars
28.6k
Forks
4.8k
Avg merge
14h 13m
Merged PRs (30d)
18

Description

Summary

validate_server_url (connectors/openapi_plugin/server_url_validator.py) protects the
OpenAPI plugin against SSRF by resolving the target host and blocking
private/loopback/link-local/metadata addresses. However, the validated IP is not
reused for the actual request: openapi_runner.run_operation calls
validate_server_url(url, ...) (openapi_runner.py ~L146) with no dns_resolver, then
issues the request via httpx.AsyncClient(...).request(url=<hostname>) (~L172-186),
which re-resolves the hostname independently at connect time. A host that resolves
to a public address during validation and to a private address at connect time
(classic DNS rebinding) passes the check and is then contacted. Because
run_operation also attaches auth_callback credentials to the request, the request
that reaches the rebound address is credential-bearing.

Severity (stated honestly — this is hardening, not a high-severity SSRF)

Impact in the default configuration is low, because other layers already constrain it:

  • The validator forces https by default, and httpx verifies TLS certificates
    (verify=True), so a rebind to e.g. 169.254.169.254 fails the TLS handshake — the
    request is not sent and no credential is disclosed over the default https path. The
    residual over https is a blind connection attempt (TCP connect + ClientHello) to the
    internal IP, not data/credential exfiltration.
  • Full SSRF + credential disclosure via rebinding requires an operator-configured
    http allowed_base_urls entry, or a caller-supplied http_client with
    verify=False, or a host platform that ingests untrusted OpenAPI specs/overrides.
  • The feature is @experimental.

I'm filing this as defense-in-depth: the validator is a deliberate anti-SSRF control,
and pinning the resolved IP closes the one check-time/use-time gap in it.

Reproduction (mechanism; offline)

semantic-kernel 1.44.1. Making getaddrinfo return a public IP on the 1st lookup
(validation) and a link-local IP on the 2nd (connect) shows the validator passes while
the connection target is an address it would have blocked:

import asyncio, socket
from semantic_kernel.connectors.openapi_plugin.server_url_validator import (
    validate_server_url, try_categorize_non_public_address)

HOST, PUBLIC, META = "rebind.example", "93.184.216.34", "169.254.169.254"
_real, n = socket.getaddrinfo, {"i": 0}
def rebinding(host, *a, **k):
    if host == HOST:
        n["i"] += 1
        ip = PUBLIC if n["i"] == 1 else META
        return [(socket.AF_INET, socket.SOCK_STREAM, 6, "", (ip, 0))]
    return _real(host, *a, **k)
socket.getaddrinfo = rebinding

async def main():
    await validate_server_url(f"https://{HOST}/api/op")     # 1st resolution -> public -> PASSES
    connect_ip = socket.getaddrinfo(HOST, 443)[0][4][0]     # 2nd -> 169.254.169.254 (what httpx uses)
    print("validated public; connect IP:", connect_ip, try_categorize_non_public_address(connect_ip))
asyncio.run(main())

I did not stand up a live authoritative rebinding DNS server + real httpx connection;
this demonstrates the resolve-then-connect gap the runner relies on.

Suggested remediation

Resolve once and pin: connect to the validated IP (e.g. a custom httpx transport /
resolver that reuses the vetted address while preserving SNI/Host), or re-validate the
peer IP at connect time. Consider applying the IP check on the allowed_base_urls path
too (it currently matches on hostname strings without resolving), and re-validating
after redirects if a caller-supplied client enables follow_redirects.

Contributor guide

Open the contributing guide

First steps

  1. Read the whole issue, then the project's contributing guide.
  2. Comment on the issue to say you are picking it up — it saves two people doing the same work.
  3. Fork the repository and make your change on a branch.
  4. Open a pull request that references the issue number.

Research direction

Start with connectors/openapi_plugin/server_url_validator.py and openapi_runner.py, especially validate_server_url and run_operation around the cited lines. Reproduce the offline DNS lookup sequence first, then determine how the vetted address can be reused for the request while preserving hostname handling; done means the connection cannot independently resolve to an address rejected by validation.

Written by the indexing model from the issue text.

Assessment

Tech stack
python
Domain
security
Issue type
Bug
Difficulty
4/5
Estimated time
3-5 days
Activity status
Active
Clarity
Mostly clear
Newbie friendliness
48/100

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.