diff --git a/src/core/config/proxy.rs b/src/core/config/proxy.rs index 77c4531a7..b8f7a8305 100644 --- a/src/core/config/proxy.rs +++ b/src/core/config/proxy.rs @@ -50,6 +50,15 @@ impl ProxyConfig { })), }) } + + #[must_use] + pub fn url_for(&self, url: &Url) -> Option<&Url> { + match self { + | Self::None => None, + | Self::Global { url } => Some(url), + | Self::ByDomain(proxies) => proxies.iter().find_map(|proxy| proxy.for_url(url)), + } + } } #[derive(Clone, Debug, Deserialize)] diff --git a/src/service/pusher/mod.rs b/src/service/pusher/mod.rs index df82d7bb4..eacabe55b 100644 --- a/src/service/pusher/mod.rs +++ b/src/service/pusher/mod.rs @@ -1,6 +1,6 @@ mod webpush; -use std::{fmt::Debug, mem, sync::Arc}; +use std::{fmt::Debug, mem, net::IpAddr, sync::Arc}; use bytes::BytesMut; use conduwuit::utils::response::LimitReadExt; @@ -32,6 +32,7 @@ use ruma::{ uint, }; use ruminuwuity::pushers::{Pusher, PusherKind, set_pusher::v3::PusherAction}; +use url::Host; pub use self::webpush::ActivationOutcome; use self::webpush::Vapid; @@ -129,10 +130,11 @@ impl Service { match &pusher.kind { | PusherKind::Http(http) => { - self.validate_push_url(&http.url, PUSH_GATEWAY_SCHEMES)?; + self.validate_push_url(&http.url, PUSH_GATEWAY_SCHEMES) + .await?; }, | PusherKind::WebPush(data) => { - self.validate_push_url(&data.url, WEBPUSH_SCHEMES)?; + self.validate_push_url(&data.url, WEBPUSH_SCHEMES).await?; webpush::decode_pushkey(pushkey)?; webpush::decode_auth(&data.auth)?; @@ -235,7 +237,7 @@ impl Service { /// Checks that a pusher URL is well-formed and not pointing at a forbidden /// address. - fn validate_push_url(&self, url: &str, allowed: &[&str]) -> Result { + async fn validate_push_url(&self, url: &str, allowed: &[&str]) -> Result { let parsed = url::Url::parse(url).map_err(|e| { err!(Request(InvalidParam(warn!(%url, "Pusher URL is not a valid URL: {e}")))) })?; @@ -248,20 +250,53 @@ impl Service { )))); } - if let Some(host) = parsed.host_str() { - if let Ok(ip) = IPAddress::parse(host) { - if !self.services.client.valid_cidr_range(&ip) { - return Err!(Request(InvalidParam(warn!( + // Proxies that resolve names remotely must enforce the denylist + // themselves. + let resolves_remotely = + matches!(self.proxy_scheme(&parsed), Some("http" | "https" | "socks4a" | "socks5h")); + + let forbidden = match parsed.host() { + | Some(Host::Ipv4(ip)) => !self.allowed_ip(ip.into()), + | Some(Host::Ipv6(ip)) => !self.allowed_ip(ip.into()), + | Some(Host::Domain(host)) if !resolves_remotely => self + .services + .client + .dns_resolver + .lookup_ip(host) + .await + .map_err(|e| { + err!(Request(InvalidParam(warn!( %url, - "Pusher URL is a forbidden remote address" - )))); - } - } + "Pusher URL host could not be resolved: {e}" + )))) + })? + .iter() + .any(|ip| !self.allowed_ip(ip)), + | _ => false, + }; + if forbidden { + return Err!(Request(InvalidParam(warn!( + %url, + "Pusher URL is a forbidden remote address" + )))); } Ok(parsed) } + fn proxy_scheme(&self, url: &url::Url) -> Option<&str> { + self.services + .config + .proxy + .url_for(url) + .map(url::Url::scheme) + } + + fn allowed_ip(&self, ip: IpAddr) -> bool { + IPAddress::parse(ip.to_string()) + .is_ok_and(|ip| self.services.client.valid_cidr_range(&ip)) + } + #[tracing::instrument(skip(self, dest, request))] pub async fn send_request(&self, dest: &str, request: T) -> Result where @@ -319,21 +354,29 @@ impl Service { trace!("Checking request URL for IP"); if let Ok(ip) = IPAddress::parse(url_host) { if !self.services.client.valid_cidr_range(&ip) { - return Err!(BadServerResponse("Not allowed to send requests to this IP")); + return Err!(BadServerResponse(warn!( + %url_host, + "Not allowed to send requests to this IP" + ))); } } } let dest = request.url().clone(); + let proxied = self.proxy_scheme(&dest).is_some(); + let mut response = client.execute(request).await.map_err(|e| { err!(BadServerResponse(warn!(%dest, "Could not send request to pusher: {e}"))) })?; - trace!("Checking response destination's IP"); - if let Some(remote_addr) = response.remote_addr() { - if let Ok(ip) = IPAddress::parse(remote_addr.ip().to_string()) { - if !self.services.client.valid_cidr_range(&ip) { - return Err!(BadServerResponse("Not allowed to send requests to this IP")); + if !proxied { + trace!("Checking response destination's IP"); + if let Some(remote_addr) = response.remote_addr() { + if !self.allowed_ip(remote_addr.ip()) { + return Err!(BadServerResponse(warn!( + %dest, + "Pusher host resolved to a forbidden address after validation" + ))); } } } @@ -482,7 +525,8 @@ impl Service { // TODO: email match &pusher.kind { | PusherKind::Http(http) => { - self.validate_push_url(&http.url, PUSH_GATEWAY_SCHEMES)?; + self.validate_push_url(&http.url, PUSH_GATEWAY_SCHEMES) + .await?; let notify = self .build_notification( diff --git a/src/service/pusher/webpush/mod.rs b/src/service/pusher/webpush/mod.rs index 5fb8ad19b..2a428d539 100644 --- a/src/service/pusher/webpush/mod.rs +++ b/src/service/pusher/webpush/mod.rs @@ -270,7 +270,7 @@ impl Service { payload: Vec, prio: NotificationPriority, ) -> Result { - let endpoint = self.validate_push_url(&data.url, WEBPUSH_SCHEMES)?; + let endpoint = self.validate_push_url(&data.url, WEBPUSH_SCHEMES).await?; let authorization = self.vapid.authorization(&endpoint)?; let pushkey = pusher.ids.pushkey.as_str();