Skip to content

feat(security): add ALLOWED_PRIVATE_HOSTS allowlist for SSRF block#4677

Open
waleedlatif1 wants to merge 1 commit into
stagingfrom
waleedlatif1/allowed-private-hosts
Open

feat(security): add ALLOWED_PRIVATE_HOSTS allowlist for SSRF block#4677
waleedlatif1 wants to merge 1 commit into
stagingfrom
waleedlatif1/allowed-private-hosts

Conversation

@waleedlatif1
Copy link
Copy Markdown
Collaborator

@waleedlatif1 waleedlatif1 commented May 20, 2026

Summary

Adds an ALLOWED_PRIVATE_HOSTS env var that lets self-hosted operators allowlist specific hostnames, IPs, and CIDRs from the SSRF private-IP block. Generalizes the pattern of ALLOWED_MCP_DOMAINS so 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_DOMAINS provides 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 — schema
  • apps/sim/lib/core/config/feature-flags.tsgetAllowedPrivateHostsFromEnv() and isAllowlistedPrivateHost()
  • apps/sim/lib/core/security/input-validation*.ts — wire into validators
  • apps/sim/lib/mcp/domain-check.ts — wire into MCP SSRF check
  • helm/sim/values.yaml — chart exposure
  • apps/docs/content/docs/en/self-hosting/{environment-variables,troubleshooting}.mdx — docs
  • packages/testing/src/mocks/feature-flags.mock.ts — test mock

Test plan

  • Unit tests for parser (feature-flags.test.ts): hostnames, IPs, IPv4/IPv6 CIDRs, mixed lists, caching, fallback to hostname on bad CIDR.
  • Unit tests for matcher: case-insensitive hostname match, exact IP, CIDR, IPv4/IPv6 separation, unparseable IPs.
  • Integration tests in domain-check.test.ts: allowlisted IP literal, allowlisted resolved hostname, allowlisted resolved IP, non-override of hosted-mode loopback block.
  • Existing tests pass — 1,090 across lib/core, lib/mcp, lib/data-drains.
  • bun run check:api-validation passes.
  • helm lint helm/sim passes.
  • Manual: deploy with ALLOWED_PRIVATE_HOSTS=gitlab.example.internal,10.0.0.0/8, confirm an HTTP block reaches the internal host.

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>
@vercel
Copy link
Copy Markdown

vercel Bot commented May 20, 2026

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
docs Ready Ready Preview, Comment May 20, 2026 7:29pm

Request Review

@cursor
Copy link
Copy Markdown

cursor Bot commented May 20, 2026

PR Summary

Medium Risk
Touches SSRF/URL validation paths across HTTP, database, and MCP, so misconfiguration or parsing bugs could unintentionally weaken outbound request protections. Default behavior remains unchanged unless ALLOWED_PRIVATE_HOSTS is set.

Overview
Adds a new ALLOWED_PRIVATE_HOSTS env var to selectively bypass the private/reserved IP SSRF block for operator-approved hostnames, IPs, and CIDRs, with cached parsing and matching via isAllowlistedPrivateHost().

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.

Copy link
Copy Markdown

@cursor cursor Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cursor Bugbot has reviewed your changes and found 1 potential issue.

Fix All in Cursor

❌ 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 })
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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)
Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit 2ad06cb. Configure here.

@greptile-apps
Copy link
Copy Markdown
Contributor

greptile-apps Bot commented May 20, 2026

Greptile Summary

This PR adds an ALLOWED_PRIVATE_HOSTS environment variable that lets self-hosted operators allowlist specific hostnames, literal IPs, and CIDR ranges from the SSRF private-IP block, generalising the existing ALLOWED_MCP_DOMAINS escape hatch to cover all outbound surfaces (HTTP, webhook, database, MCP, and hostname validators).

  • Core logic (feature-flags.ts): parses the env var once at startup into a Set<string> of lowercased hostnames and a pre-parsed Array<[ipaddr, prefixLen]> of CIDR ranges, with a undefined/null sentinel cache and a test-only reset hook.
  • Validator wiring: isAllowlistedPrivateHost is injected as a final override in validateUrlWithDNS, validateDatabaseHost, validateExternalUrl, validateHostname, and validateMcpServerSsrf.
  • Loopback inconsistency: validateMcpServerSsrf correctly separates the loopback check from the allowlist gate, but validateDatabaseHost and validateUrlWithDNS do not — setting ALLOWED_PRIVATE_HOSTS=127.0.0.1 bypasses loopback protection in those two validators, contradicting the PR's documented guarantee.

Confidence Score: 3/5

The 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

Filename Overview
apps/sim/lib/core/config/feature-flags.ts Adds getAllowedPrivateHostsFromEnv and isAllowlistedPrivateHost with process-level caching, CIDR parsing via ipaddr.js, and a test-only reset hook. Logic is sound; CIDR kind-matching guards prevent IPv4/IPv6 cross-match.
apps/sim/lib/core/security/input-validation.server.ts Wires allowlist into validateUrlWithDNS and validateDatabaseHost; loopback IPs (127.0.0.1) can be allowlisted because there is no pre-allowlist loopback gate, unlike validateMcpServerSsrf.
apps/sim/lib/core/security/input-validation.ts Allowlist wired into validateHostname and validateExternalUrl; allowlisted non-IP hostnames trigger an early return that bypasses the RFC hostname format regex.
apps/sim/lib/mcp/domain-check.ts validateMcpServerSsrf correctly separates loopback check from the allowlist guard, matching the intended contract.
apps/sim/lib/core/config/feature-flags.test.ts New test file with comprehensive unit coverage for parser and matcher, including edge cases (bad CIDRs, IPv4/IPv6 separation, case-insensitivity, caching).
apps/sim/lib/mcp/domain-check.test.ts Integration tests added for the MCP SSRF allowlist; hosted-mode loopback override test correctly verifies the allowlist cannot bypass loopback on the hosted platform.

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]
Loading

Comments Outside Diff (1)

  1. apps/sim/lib/core/security/input-validation.server.ts, line 175-199 (link)

    P1 Loopback IPs can be allowlisted in validateDatabaseHost

    validateDatabaseHost blocks "localhost" as a string (line 171) but then gates the IP-literal check with !isAllowlistedPrivateHost. Because isPrivateOrReservedIP("127.0.0.1") returns true (loopback range ≠ unicast), setting ALLOWED_PRIVATE_HOSTS=127.0.0.1 bypasses the loopback block entirely — both the literal-IP path and the post-DNS-lookup path. The PR description explicitly states "operators can't use this to point at loopback in production," but neither validateDatabaseHost nor validateUrlWithDNS enforces this. validateMcpServerSsrf gets it right by checking isLoopbackIP(address) in a separate branch before reaching the allowlist guard — the same pattern should be applied here.

Reviews (1): Last reviewed commit: "feat(security): add ALLOWED_PRIVATE_HOST..." | Re-trigger Greptile

Comment on lines +414 to 416
} else if (isAllowlistedPrivateHost({ hostname: lowerHostname })) {
return { isValid: true, sanitized: lowerHostname }
}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant