Repository navigation
.Net: Python: allowed_base_urls path prefix can be escaped with dot segments (.. / %2e) — validator and HTTP client disagree on the path #14662
Description
Summary
When allowed_base_urls contains a non-root path prefix, a URL whose path only looks inside that prefix passes validation, but the HTTP client normalizes the path before sending it, so the server receives a request outside the prefix. This is a two-parser differential: the validator checks the raw path string, httpx checks the normalized path.
Reproduction (validator level, no server needed)
from semantic_kernel.connectors.openapi_plugin.server_url_validator import (
ServerUrlValidationOptions,
validate_server_url,
)
options = ServerUrlValidationOptions(
allowed_base_urls=["https://api.example.com/api"]
)
# Currently returns [] (allowed). The request httpx actually sends is
# https://api.example.com/admin/delete-all.
await validate_server_url(
"https://api.example.com/api/../admin/delete-all", options
)The same holds for /api/v1/../../admin against an /api/v1 prefix, and for percent-encoded dot segments such as /api/%2e%2e/admin (see scope note below).
Root cause
python/semantic_kernel/connectors/openapi_plugin/server_url_validator.py:
_matches_path_prefixis a plain case-insensitive string-prefix check onurl.pathexactly asurllib.parse.urlparsereturns it.urlparseperforms no dot-segment removal, so/api/../adminstarts with/api/and passes.- On a match,
validate_server_urlreturns[]immediately, skipping every other check. openapi_runner.pyvalidates, then sends the same URL string via httpx (unpinned in the allowlist case). httpx normalizes dot segments before transmission (RFC 3986 section 5.2.4):/api/../secretbecomes/secret,/api/v1/../../adminbecomes/admin(verified against a current httpx release).
Impact and scope — stated plainly
This is a same-host path-scope escape, not a private-network SSRF and not CVE-class. It requires all three of:
- an allowlist configured with a non-root path prefix (the feature exists to fence an agent to endpoints on a host),
- attacker influence over the operation URL path, and
- a sensitive endpoint outside the prefix on the same host.
Proposed fix
Normalize the path once, in _parse_absolute_url, so every consumer — including the allowlist matcher, which parses both the request URL and each configured base URL through that function — compares canonical paths:
- RFC 3986 section 5.2.4 dot-segment removal;
..above the root is clamped at the root; empty segments (//) are preserved, matching httpx. - Percent-encoded dot segments (
%2e, any case) are decoded before resolving. httpx sends those literally, but a downstream server may decode before routing, so the validator deliberately fails closed on ambiguous encodings. Other percent-encodings are left untouched, and only./..segments are special. - Backslash separators are out of scope: httpx does not normalize
\to/in paths (verified), so no change is needed there.
The patch is minimal: one new _normalize_path function and one changed return line. No change to the runner, the pinning logic, or the scheme/host/port checks. I have a prepared patch and tests covering the escape cases, the encoded variants, and the legitimate cases that must keep passing (/api/./users, /api/users, /api, /apiusers still rejected), and I can open a PR from it if maintainers agree with the approach.
Note for maintainers (not part of this change)
The .NET SDK has a parallel validator with the same raw-prefix shape (ServerUrlValidator.cs / PublicNetworkAddressValidator.cs). This proposal covers the Python SDK only; the .NET side is flagged here for a follow-up by someone working in that codebase.
Activity
[-]Python: allowed_base_urls path prefix can be escaped with dot segments (.. / %2e) — validator and HTTP client disagree on the path[/-][+].Net: Python: allowed_base_urls path prefix can be escaped with dot segments (.. / %2e) — validator and HTTP client disagree on the path[/+]