feat(security): add ALLOWED_PRIVATE_HOSTS allowlist for SSRF block#4677
feat(security): add ALLOWED_PRIVATE_HOSTS allowlist for SSRF block#4677waleedlatif1 wants to merge 1 commit into
Conversation
Self-hosted operators frequently need agents, webhooks, database blocks, and MCP servers to reach internal services (on-prem GitLab, internal SIEM, in-cluster Postgres) whose hostnames resolve to private IPs. Today the SSRF block is binary — only ALLOWED_MCP_DOMAINS provides an escape, and only for MCP. ALLOWED_PRIVATE_HOSTS accepts a comma-separated list of hostnames, literal IPs, and CIDRs. Entries are matched against both the original hostname and the resolved IP, so "gitlab.internal" or "10.112.12.56" or "10.0.0.0/8" all work. The default (unset) preserves today's full private-IP block. Loopback handling and the hosted-mode tightening are unchanged — the allowlist only narrows the private/reserved range check. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
PR SummaryMedium Risk Overview Wires this allowlist into outbound validation surfaces (DNS-pinned URL validation, generic hostname/URL validation, database host validation, and MCP SSRF checks) while still preserving hosted-mode loopback restrictions. Updates Helm values, testing mocks, and docs (including a new troubleshooting entry for the blocked-IP error), and adds unit/integration tests covering parsing, caching, and allowlist behavior. Reviewed by Cursor Bugbot for commit 2ad06cb. Configure here. |
There was a problem hiding this comment.
Cursor Bugbot has reviewed your changes and found 1 potential issue.
❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.
Reviewed by Cursor Bugbot for commit 2ad06cb. Configure here.
| if ( | ||
| isPrivateOrReservedIP(address) && | ||
| !(isLocalhost && resolvedIsLoopback && !isHosted) && | ||
| !isAllowlistedPrivateHost({ hostname: cleanHostname, ip: address }) |
There was a problem hiding this comment.
Allowlist bypasses loopback SSRF guard
High Severity
isAllowlistedPrivateHost can clear blocks for loopback and other reserved targets because callers treat it as a blanket override on isPrivateOrReservedIP. On hosted, an allowlisted hostname that resolves to 127.0.0.0/8 can reach local services; MCP already rejects loopback before the allowlist, but validateUrlWithDNS, validateDatabaseHost, and validateHostname do not.
Additional Locations (2)
Reviewed by Cursor Bugbot for commit 2ad06cb. Configure here.
Greptile SummaryThis PR adds an
Confidence Score: 3/5The feature is well-structured and the MCP validator handles loopback correctly, but the database and URL validators have a gap where explicitly allowlisting loopback IPs bypasses what the PR documentation describes as an inviolable constraint. The core parser and CIDR matcher are solid and well-tested. However, validateDatabaseHost and validateUrlWithDNS allow an operator to add 127.0.0.1 to ALLOWED_PRIVATE_HOSTS and bypass loopback protection — a behaviour the PR explicitly says cannot happen. In multi-user self-hosted deployments where end-users supply database or HTTP block URLs, this gap could let users reach services on the host loopback interface if an operator has incautiously broadened their allowlist. apps/sim/lib/core/security/input-validation.server.ts — the loopback pre-gate is missing from both validateDatabaseHost and validateUrlWithDNS. Important Files Changed
Flowchart%%{init: {'theme': 'neutral'}}%%
flowchart TD
A[Inbound URL / hostname / DB host] --> B{Is loopback?}
B -- "Yes (MCP path only)" --> C{isHosted?}
C -- Yes --> D[Block - McpSsrfError]
C -- No --> E[Allow - local dev]
B -- No --> F{isPrivateOrReservedIP?}
F -- No --> G[Allow - public IP]
F -- Yes --> H{isAllowlistedPrivateHost?}
H -- "Yes (hostname or CIDR match)" --> I[Allow - operator allowlist]
H -- No --> J[Block - SSRF protection]
|
| } else if (isAllowlistedPrivateHost({ hostname: lowerHostname })) { | ||
| return { isValid: true, sanitized: lowerHostname } | ||
| } |
There was a problem hiding this comment.
Allowlisted hostnames bypass the RFC hostname format check
When isAllowlistedPrivateHost({ hostname: lowerHostname }) is true, validateHostname returns early at line 415, skipping the hostnamePattern regex entirely. The intent of the allowlist is to bypass the SSRF private-IP block, not format validation — but a hostname like my_service.internal (with an underscore, invalid per strict RFC 1123) or any other non-standard entry that happens to be in the allowlist would be returned as isValid: true without a format check. Callers that rely on validateHostname to guarantee a well-formed hostname get a weaker guarantee for allowlisted entries than for ordinary public hostnames.


Summary
Adds an
ALLOWED_PRIVATE_HOSTSenv var that lets self-hosted operators allowlist specific hostnames, IPs, and CIDRs from the SSRF private-IP block. Generalizes the pattern ofALLOWED_MCP_DOMAINSso it applies to every outbound surface (HTTP block, webhooks, database blocks, MCP servers, generic URL validation).Motivation
Today the SSRF block is binary — only
ALLOWED_MCP_DOMAINSprovides an escape, and only for MCP. Self-hosted customers regularly need to call internal services whose hostnames resolve to private IPs (on-prem GitLab, internal SIEM, in-cluster Postgres, internal LLM gateways). Without this, they either fork-and-patch the SSRF code or stand up a public reverse proxy in front of every internal service.Files
apps/sim/lib/core/config/env.ts— schemaapps/sim/lib/core/config/feature-flags.ts—getAllowedPrivateHostsFromEnv()andisAllowlistedPrivateHost()apps/sim/lib/core/security/input-validation*.ts— wire into validatorsapps/sim/lib/mcp/domain-check.ts— wire into MCP SSRF checkhelm/sim/values.yaml— chart exposureapps/docs/content/docs/en/self-hosting/{environment-variables,troubleshooting}.mdx— docspackages/testing/src/mocks/feature-flags.mock.ts— test mockTest plan
feature-flags.test.ts): hostnames, IPs, IPv4/IPv6 CIDRs, mixed lists, caching, fallback to hostname on bad CIDR.domain-check.test.ts: allowlisted IP literal, allowlisted resolved hostname, allowlisted resolved IP, non-override of hosted-mode loopback block.lib/core,lib/mcp,lib/data-drains.bun run check:api-validationpasses.helm lint helm/simpasses.ALLOWED_PRIVATE_HOSTS=gitlab.example.internal,10.0.0.0/8, confirm an HTTP block reaches the internal host.