fix: SSRF bypass via raw httpx.get in API tool schema fetch (#37746)

Co-authored-by: 归青 <guiqing.lzw@antgroup.com>
Co-authored-by: QuantumGhost <obelisk.reg+git@gmail.com>
This commit is contained in:
Snakinya 2026-07-14 16:43:57 +08:00 committed by GitHub
parent f09d2b191f
commit ae0d6ee214
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
4 changed files with 37 additions and 6 deletions

View File

@ -5,10 +5,10 @@ from json import loads as json_loads
from json.decoder import JSONDecodeError
from typing import Any, TypedDict
import httpx
from flask import has_request_context, request
from yaml import YAMLError, safe_load
from core.helper import ssrf_proxy
from core.tools.entities.common_entities import I18nObject
from core.tools.entities.tool_bundle import ApiToolBundle
from core.tools.entities.tool_entities import ApiProviderSchemaType, ToolParameter
@ -376,7 +376,7 @@ class ApiBasedToolSchemaParser:
raise ToolNotSupportedError("Only openapi is supported now.")
# get openapi yaml
response = httpx.get(
response = ssrf_proxy.get(
api_url, headers={"User-Agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) "}, timeout=5
)

View File

@ -2,12 +2,12 @@ import json
import logging
from typing import Any, Literal, TypedDict, cast
from httpx import get
from pydantic import TypeAdapter
from sqlalchemy import select
from sqlalchemy.orm import sessionmaker
from core.entities.provider_entities import ProviderConfig, ProviderConfigType
from core.helper import ssrf_proxy
from core.tools.__base.tool_runtime import ToolRuntime
from core.tools.custom_tool.provider import ApiToolProviderController
from core.tools.entities.api_entities import ToolApiEntity, ToolProviderApiEntity
@ -231,7 +231,7 @@ class ApiToolManageService:
}
try:
response = get(url, headers=headers, timeout=10)
response = ssrf_proxy.get(url, headers=headers, timeout=10)
if response.status_code != 200:
raise ValueError(f"Got status code {response.status_code}")
schema = response.text

View File

@ -327,7 +327,7 @@ def test_parse_openai_plugin_json_branches(app):
def test_parse_openai_plugin_json_http_branches(app):
with app.test_request_context():
response = type("Resp", (), {"status_code": 500, "text": "", "close": Mock()})()
with patch("core.tools.utils.parser.httpx.get", return_value=response):
with patch("core.tools.utils.parser.ssrf_proxy.get", return_value=response):
with pytest.raises(ToolProviderNotFoundError, match="cannot get openapi yaml"):
ApiBasedToolSchemaParser.parse_openai_plugin_json_to_tool_bundle(
'{"api": {"url": "https://x", "type": "openapi"}}'
@ -335,7 +335,7 @@ def test_parse_openai_plugin_json_http_branches(app):
response.close.assert_called_once()
success_response = type("Resp", (), {"status_code": 200, "text": "openapi: 3.0.0", "close": Mock()})()
with patch("core.tools.utils.parser.httpx.get", return_value=success_response):
with patch("core.tools.utils.parser.ssrf_proxy.get", return_value=success_response):
with patch(
"core.tools.utils.parser.ApiBasedToolSchemaParser.parse_openapi_yaml_to_tool_bundle",
return_value=["bundle"],

View File

@ -0,0 +1,31 @@
from unittest.mock import Mock
from services.tools.api_tools_manage_service import ApiToolManageService
def test_get_api_tool_provider_remote_schema_uses_ssrf_proxy_get(monkeypatch) -> None:
schema = """
{
"openapi": "3.0.0",
"info": {"title": "Demo API", "version": "1.0.0"},
"servers": [{"url": "https://api.example.com"}],
"paths": {}
}
"""
url = "https://example.com/openapi.json"
expected_headers = {
"User-Agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko)"
" Chrome/120.0.0.0 Safari/537.36 Edg/120.0.0.0",
"Accept": "*/*",
}
mock_get = Mock(return_value=Mock(status_code=200, text=schema))
mock_parser = Mock()
monkeypatch.setattr("services.tools.api_tools_manage_service.ssrf_proxy.get", mock_get)
monkeypatch.setattr(ApiToolManageService, "parser_api_schema", mock_parser)
result = ApiToolManageService.get_api_tool_provider_remote_schema("user-1", "tenant-1", url)
assert result == {"schema": schema}
mock_get.assert_called_once_with(url, headers=expected_headers, timeout=10)
mock_parser.assert_called_once_with(schema)