diff --git a/server/src/at_record_server/url_guard.gleam b/server/src/at_record_server/url_guard.gleam index 8ef6bfd..70c0526 100644 --- a/server/src/at_record_server/url_guard.gleam +++ b/server/src/at_record_server/url_guard.gleam @@ -3,6 +3,7 @@ //// cover URL. Same rule everywhere, so it lives in one place rather than //// being re-derived per call site. +import envoy import gleam/int import gleam/list import gleam/option.{Some} @@ -18,15 +19,26 @@ pub fn is_safe_pds_url(url: String) -> Bool { Error(_) -> False Ok(parsed) -> case parsed.scheme, parsed.host { + Some("http"), Some(_) -> allow_private_endpoints() Some("https"), Some(host) -> - !has_bracketed_authority(url) - && !string.starts_with(host, "[") - && is_safe_host(host) + allow_private_endpoints() + || { + !has_bracketed_authority(url) + && !string.starts_with(host, "[") + && is_safe_host(host) + } _, _ -> False } } } +// The e2e devnet runs every service on loopback http, which the strict rule +// exists to forbid; deliberately env-gated and named unsafe so it never +// creeps into a real deploy. +fn allow_private_endpoints() -> Bool { + envoy.get("UNSAFE_ALLOW_PRIVATE_ENDPOINTS") == Ok("1") +} + // A real atproto PDS is always a DNS hostname, never a bare IP literal, so // IPv6 is rejected wholesale rather than range-classified (no loopback/ULA/ // link-local carve-out to get right). That sidesteps a real bug in the diff --git a/server/test/url_guard_test.gleam b/server/test/url_guard_test.gleam index 495422b..83bea6b 100644 --- a/server/test/url_guard_test.gleam +++ b/server/test/url_guard_test.gleam @@ -8,6 +8,7 @@ //// is even attempted, so this suite never touches the network. import at_record_server/url_guard +import envoy import gleam/list pub fn https_is_required_test() { @@ -96,3 +97,13 @@ pub fn cgnat_range_is_rejected_test() { pub fn limited_broadcast_is_rejected_test() { assert !url_guard.is_safe_pds_url("https://255.255.255.255") } + +pub fn private_endpoints_allowed_only_behind_the_unsafe_env_gate_test() { + assert !url_guard.is_safe_pds_url("http://localhost:3000") + envoy.set("UNSAFE_ALLOW_PRIVATE_ENDPOINTS", "1") + assert url_guard.is_safe_pds_url("http://localhost:3000") + assert url_guard.is_safe_pds_url("https://127.0.0.1:6790") + assert !url_guard.is_safe_pds_url("not a url") + envoy.unset("UNSAFE_ALLOW_PRIVATE_ENDPOINTS") + assert !url_guard.is_safe_pds_url("http://localhost:3000") +}