Skip to content

[BUG]: Gateway test endpoint bypasses global SSRF protection flag #5022

@bogdanmariusc10

Description

@bogdanmariusc10

🐞 Bug Summary

The /admin/gateways/test endpoint allows administrators to test gateway connectivity by sending HTTP requests to specified URLs. While the endpoint includes comprehensive validation logic, it unconditionally enforces its own SSRF protections regardless of the global ssrf_protection_enabled flag. This creates an inconsistency where disabling SSRF protection globally (e.g., for development/testing) does not affect the gateway test endpoint, and the endpoint's protections cannot be disabled even when intentionally needed.


🧩 Affected Component

  • mcpgateway - API
  • mcpgateway - UI (admin panel)
  • mcpgateway.wrapper - stdio wrapper
  • Federation or Transports
  • CLI, Makefiles, or shell scripts
  • Container setup (Docker/Podman/Compose)
  • Other (explain below)

🔁 Steps to Reproduce

Scenario 1: SSRF Protection Bypass

  1. Set SSRF_PROTECTION_ENABLED=false in environment configuration
  2. Verify that other endpoints (gateway creation, tool registration) respect this flag
  3. Authenticate as a user with gateways.read permission
  4. Send a POST request to /admin/gateways/test with a base_url pointing to a private IP
  5. Observe that the request is rejected despite global SSRF protection being disabled

Scenario 2: Allowlist Configuration Gap

  1. Deploy ContextForge with gateway_test_allow_registered_only=false and empty gateway_test_allowed_hosts
  2. Authenticate as a user with gateways.read permission
  3. Send a POST request to /admin/gateways/test with a base_url pointing to an arbitrary external host
  4. Observe that the request is processed (allowlist enforcement depends on configuration)

Example Request:

curl -X POST https://gateway.example.com/admin/gateways/test \
  -H "Authorization: Bearer $TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "method": "GET",
    "base_url": "https://arbitrary-external-host.com",
    "path": "/api/endpoint"
  }'

🤔 Expected Behavior

The endpoint should respect the global ssrf_protection_enabled flag for consistency with other endpoints, while maintaining its additional allowlist-based restrictions. Specifically:

  1. SSRF Flag Consistency: When ssrf_protection_enabled=false, the endpoint should honor this setting (currently it doesn't)
  2. Layered Security: The endpoint should apply BOTH global SSRF protection (when enabled) AND its own allowlist enforcement
  3. Default Mode: When gateway_test_allow_registered_only=true (current default), only URLs matching registered gateway base URLs should be testable
  4. Explicit Allowlist Mode: When gateway_test_allow_registered_only=false, only hosts matching patterns in gateway_test_allowed_hosts should be testable
  5. Empty Allowlist: When allowlist is empty and registered-only mode is disabled, all requests should be rejected

📓 Logs / Error Output

When ssrf_protection_enabled=false, the gateway test endpoint still blocks private IPs:

Gateway test URL validation failed for https://192.168.1.1/ by user admin@example.com: Gateway test URL is not allowed

Root Cause Analysis:

The validate_gateway_test_url() method calls validate_url() which does respect ssrf_protection_enabled at line 1291. However, validate_gateway_test_url() then applies additional unconditional SSRF checks at lines 1631-1654 that block private IPs regardless of the global flag:

# Line 1631-1654: Unconditional private IP blocking
# This runs AFTER validate_url() and ignores ssrf_protection_enabled
if ip_addr.is_private or ip_addr.is_loopback or ip_addr.is_link_local or ...:
    raise ValueError(f"{field_name} is not allowed")

Current Implementation:


🧠 Environment Info

Key Value
Version or commit main branch
Runtime Python 3.11+, FastAPI
Platform / OS All platforms
Container Docker/Podman/native

🧩 Additional Context

Current Security Controls:

  1. Dual-Layer SSRF Protection (Inconsistent):

    • Layer 1 (validators.py:1613): Calls validate_url() which respects ssrf_protection_enabled flag
    • Layer 2 (validators.py:1631-1654): Unconditional private IP blocking that ignores the global flag
    • Issue: Layer 2 cannot be disabled, creating inconsistency with other endpoints
  2. Allowlist Enforcement (validators.py:1702-1728):

    • Validates URLs against configured host patterns
    • Supports exact hostnames and wildcard subdomains (*.example.com)
    • Returns generic error messages to prevent allowlist enumeration
  3. Unconditional Private IP Blocking (validators.py:1631-1654):

    • Always blocks RFC 1918 private IPs (10.0.0.0/8, 172.16.0.0/12, 192.168.0.0/16)
    • Always blocks loopback addresses (127.0.0.0/8, ::1)
    • Always blocks link-local addresses (169.254.0.0/16, fe80::/10)
    • Always blocks carrier-grade NAT (100.64.0.0/10)
    • Always blocks multicast, reserved, and unspecified addresses
    • Cannot be disabled via ssrf_protection_enabled flag
  4. DNS Resolution Validation (validators.py:1661-1700):

    • Resolves hostnames and validates all resolved IPs
    • Pins safe resolved IP for outbound connections (DNS rebinding mitigation)
    • Bounded DNS resolution with configurable timeout
  5. FQDN Normalization (validators.py:1627-1629):

    • Strips trailing dots to prevent bypass attempts (e.g., evil.com.)
  6. Two-Mode Operation (admin.py:14274-14299):

    • Mode 1 (default): Only registered gateway URLs are testable
    • Mode 2: Uses explicit allowlist from configuration

Recommended Fixes:

Option 1: Respect Global SSRF Flag (Recommended)
Modify validate_gateway_test_url() to check settings.ssrf_protection_enabled before applying unconditional private IP blocking:

# Line 1631: Add conditional check
if settings.ssrf_protection_enabled:
    # Unconditionally block private IPs, loopback, and link-local addresses
    # This prevents testing internal services regardless of allowlist
    try:
        ip_addr = ipaddress.ip_address(hostname_normalized)
        # ... existing blocking logic ...

Option 2: Add Separate Gateway Test SSRF Flag
Create a new configuration flag gateway_test_ssrf_protection_enabled that defaults to True but can be independently controlled:

gateway_test_ssrf_protection_enabled: bool = Field(
    default=True,
    description="Enable SSRF protection specifically for /admin/gateways/test endpoint. "
                "Independent of global ssrf_protection_enabled flag."
)

Option 3: Document Current Behavior
If the unconditional blocking is intentional, document that the gateway test endpoint has stricter SSRF controls that cannot be disabled, and explain the security rationale.

Configuration Hardening (Regardless of Fix):

  1. Keep Default Mode: Leave gateway_test_allow_registered_only=true (current default)
  2. Explicit Allowlist: If switching to allowlist mode, configure gateway_test_allowed_hosts with specific patterns:
    gateway_test_allowed_hosts:
      - "api.trusted-partner.com"
      - "*.internal-services.company.com"
  3. Monitor Usage: Review audit logs for gateway test requests to detect anomalous patterns
  4. RBAC Enforcement: Ensure only trusted administrators have gateways.read permission
  5. Understand SSRF Flag Behavior: Be aware that ssrf_protection_enabled=false does NOT disable gateway test endpoint protections

Documentation Updates Needed:

  1. Clarify SSRF Flag Scope: Document that ssrf_protection_enabled affects most endpoints but NOT the gateway test endpoint
  2. Security Advisory: Add deployment documentation emphasizing the importance of proper allowlist configuration
  3. Example Configurations: Include configurations for common deployment scenarios (dev/staging/prod)
  4. Two-Mode Operation: Document the security implications of each mode
  5. Monitoring Recommendations: Add guidance for monitoring gateway test endpoint usage
  6. Development Workflow: Document how to test internal services when gateway test endpoint blocks private IPs

Code Locations:

Metadata

Metadata

Labels

MUSTP1: Non-negotiable, critical requirements without which the product is non-functional or unsafeapiREST API Related itembugSomething isn't workingicaICA related issuessecurityImproves security

Type

No type
No fields configured for issues without a type.

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions