🐞 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
🔁 Steps to Reproduce
Scenario 1: SSRF Protection Bypass
- Set
SSRF_PROTECTION_ENABLED=false in environment configuration
- Verify that other endpoints (gateway creation, tool registration) respect this flag
- Authenticate as a user with
gateways.read permission
- Send a POST request to
/admin/gateways/test with a base_url pointing to a private IP
- Observe that the request is rejected despite global SSRF protection being disabled
Scenario 2: Allowlist Configuration Gap
- Deploy ContextForge with
gateway_test_allow_registered_only=false and empty gateway_test_allowed_hosts
- Authenticate as a user with
gateways.read permission
- Send a POST request to
/admin/gateways/test with a base_url pointing to an arbitrary external host
- 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:
- SSRF Flag Consistency: When
ssrf_protection_enabled=false, the endpoint should honor this setting (currently it doesn't)
- Layered Security: The endpoint should apply BOTH global SSRF protection (when enabled) AND its own allowlist enforcement
- Default Mode: When
gateway_test_allow_registered_only=true (current default), only URLs matching registered gateway base URLs should be testable
- Explicit Allowlist Mode: When
gateway_test_allow_registered_only=false, only hosts matching patterns in gateway_test_allowed_hosts should be testable
- 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:
-
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
-
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
-
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
-
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
-
FQDN Normalization (validators.py:1627-1629):
- Strips trailing dots to prevent bypass attempts (e.g.,
evil.com.)
-
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):
- Keep Default Mode: Leave
gateway_test_allow_registered_only=true (current default)
- 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"
- Monitor Usage: Review audit logs for gateway test requests to detect anomalous patterns
- RBAC Enforcement: Ensure only trusted administrators have
gateways.read permission
- Understand SSRF Flag Behavior: Be aware that
ssrf_protection_enabled=false does NOT disable gateway test endpoint protections
Documentation Updates Needed:
- Clarify SSRF Flag Scope: Document that
ssrf_protection_enabled affects most endpoints but NOT the gateway test endpoint
- Security Advisory: Add deployment documentation emphasizing the importance of proper allowlist configuration
- Example Configurations: Include configurations for common deployment scenarios (dev/staging/prod)
- Two-Mode Operation: Document the security implications of each mode
- Monitoring Recommendations: Add guidance for monitoring gateway test endpoint usage
- Development Workflow: Document how to test internal services when gateway test endpoint blocks private IPs
Code Locations:
🐞 Bug Summary
The
/admin/gateways/testendpoint 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 globalssrf_protection_enabledflag. 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- APImcpgateway- UI (admin panel)mcpgateway.wrapper- stdio wrapper🔁 Steps to Reproduce
Scenario 1: SSRF Protection Bypass
SSRF_PROTECTION_ENABLED=falsein environment configurationgateways.readpermission/admin/gateways/testwith abase_urlpointing to a private IPScenario 2: Allowlist Configuration Gap
gateway_test_allow_registered_only=falseand emptygateway_test_allowed_hostsgateways.readpermission/admin/gateways/testwith abase_urlpointing to an arbitrary external hostExample Request:
🤔 Expected Behavior
The endpoint should respect the global
ssrf_protection_enabledflag for consistency with other endpoints, while maintaining its additional allowlist-based restrictions. Specifically:ssrf_protection_enabled=false, the endpoint should honor this setting (currently it doesn't)gateway_test_allow_registered_only=true(current default), only URLs matching registered gateway base URLs should be testablegateway_test_allow_registered_only=false, only hosts matching patterns ingateway_test_allowed_hostsshould be testable📓 Logs / Error Output
When
ssrf_protection_enabled=false, the gateway test endpoint still blocks private IPs:Root Cause Analysis:
The
validate_gateway_test_url()method callsvalidate_url()which does respectssrf_protection_enabledat 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:Current Implementation:
SecurityValidator.validate_gateway_test_url()SecurityValidator.validate_url()- respectsssrf_protection_enabledSecurityValidator._validate_ssrf()- respectsssrf_protection_enabledadmin_test_gateway()ssrf_protection_enabled,gateway_test_allowed_hosts,gateway_test_allow_registered_only🧠 Environment Info
main branchPython 3.11+, FastAPIAll platformsDocker/Podman/native🧩 Additional Context
Current Security Controls:
Dual-Layer SSRF Protection (Inconsistent):
validators.py:1613): Callsvalidate_url()which respectsssrf_protection_enabledflagvalidators.py:1631-1654): Unconditional private IP blocking that ignores the global flagAllowlist Enforcement (
validators.py:1702-1728):*.example.com)Unconditional Private IP Blocking (
validators.py:1631-1654):ssrf_protection_enabledflagDNS Resolution Validation (
validators.py:1661-1700):FQDN Normalization (
validators.py:1627-1629):evil.com.)Two-Mode Operation (
admin.py:14274-14299):Recommended Fixes:
Option 1: Respect Global SSRF Flag (Recommended)
Modify
validate_gateway_test_url()to checksettings.ssrf_protection_enabledbefore applying unconditional private IP blocking:Option 2: Add Separate Gateway Test SSRF Flag
Create a new configuration flag
gateway_test_ssrf_protection_enabledthat defaults toTruebut can be independently controlled: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):
gateway_test_allow_registered_only=true(current default)gateway_test_allowed_hostswith specific patterns:gateways.readpermissionssrf_protection_enabled=falsedoes NOT disable gateway test endpoint protectionsDocumentation Updates Needed:
ssrf_protection_enabledaffects most endpoints but NOT the gateway test endpointCode Locations:
mcpgateway/common/validators.py:1528-1734mcpgateway/common/validators.py:1200-1310mcpgateway/common/validators.py:1400-1526mcpgateway/admin.py:14245-14450mcpgateway/config.py:675-730mcpgateway/config.py:733-757mcpgateway/middleware/token_scoping.py:177