fix(pusher): Enforce ip_range_denylist on resolved pusher hosts

This commit is contained in:
Erwan Leboucher authored and Jade Ellis committed 2026-10-12 11:14:02 +01:00
1 parent fe0ab62124
commit 805d4d9416
3 files changed
+73 -20

No files matched your search

+9
View File
@@ -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)]
+63 -19
View File
@@ -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<url::Url> {
async fn validate_push_url(&self, url: &str, allowed: &[&str]) -> Result<url::Url> {
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<T>(&self, dest: &str, request: T) -> Result<T::IncomingResponse>
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(
+1 -1
View File
@@ -270,7 +270,7 @@ impl Service {
payload: Vec<u8>,
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();