fix(api_spec): exclude specification extensions and non-operation fields from server extraction

- Restrict operation server extraction in _collect_path_item_servers strictly to valid HTTP methods (_OPENAPI_OPERATION_METHODS).
- Skip vendor extension properties (x-*) at the paths and webhooks dictionary level.
- Ensure non-operation dictionary structures (e.g. x-deployment, parameters) never authorize unintended hosts into scan scope.
- Add regression test test_openapi_ignores_vendor_extensions_in_paths_and_operations.
This commit is contained in:
MuchoRio 2026-10-10 19:31:21 +07:00
parent 5346bc53e4
commit c73b018f1d
2 changed files with 39 additions and 2 deletions

View file

@ -123,13 +123,21 @@ def _resolve_server_url(url: str, variables: Any) -> str:
return _SERVER_VAR_PATTERN.sub(lambda m: defaults.get(m.group(1), m.group(0)), url)
_OPENAPI_OPERATION_METHODS = frozenset(
{"get", "put", "post", "delete", "options", "head", "patch", "trace"}
)
def _collect_path_item_servers(path_item: Any, server_objs: list[dict[str, Any]]) -> None:
if not isinstance(path_item, dict):
return
path_servers = path_item.get("servers")
if isinstance(path_servers, list):
server_objs.extend(s for s in path_servers if isinstance(s, dict))
for op in path_item.values():
# Inspect only standard HTTP method operations; skip metadata, parameters,
# and vendor extensions (x-*).
for method in _OPENAPI_OPERATION_METHODS:
op = path_item.get(method)
if isinstance(op, dict):
op_servers = op.get("servers")
if isinstance(op_servers, list):
@ -148,7 +156,9 @@ def _openapi_base_urls(raw: dict[str, Any]) -> list[str]:
for section_key in ("paths", "webhooks"):
section = raw.get(section_key)
if isinstance(section, dict):
for path_item in section.values():
for path_key, path_item in section.items():
if str(path_key).startswith("x-"):
continue
_collect_path_item_servers(path_item, server_objs)
return _absolute_urls(

View file

@ -208,6 +208,33 @@ def test_openapi_extracts_webhook_servers() -> None:
assert spec_base_urls(raw) == ["https://webhook-receiver.test"]
def test_openapi_ignores_vendor_extensions_in_paths_and_operations() -> None:
raw = {
"openapi": "3.0.0",
"info": {"title": "Extensions Test"},
"paths": {
"x-internal-path": {
"servers": [{"url": "https://leaked-internal.test"}],
"get": {"servers": [{"url": "https://leaked-internal-op.test"}]},
},
"/api/valid": {
"servers": [{"url": "https://valid.test"}],
"get": {
"servers": [{"url": "https://valid-get.test"}],
"summary": "Valid GET operation",
},
"x-deployment": {
"servers": [{"url": "https://admin-extension.test"}],
},
"parameters": [
{"name": "id", "in": "query", "servers": [{"url": "https://param.test"}]}
],
},
},
}
assert spec_base_urls(raw) == ["https://valid.test", "https://valid-get.test"]
def test_swagger_base_urls_built_from_host() -> None:
assert spec_base_urls(SWAGGER_JSON) == ["https://legacy.test/api"]