diff options
author | mdecimus <mauro@stalw.art> | 2024-09-20 11:43:25 +0200 |
---|---|---|
committer | mdecimus <mauro@stalw.art> | 2024-09-20 11:43:25 +0200 |
commit | ea241060450504643d02d84b1f0ff48539a566f3 (patch) | |
tree | efa614cef1248d7862642a8e477eccaf3ce9cb7b /crates/common | |
parent | 6aa5686cd3cde108229866b73ac47fc07cc8282c (diff) |
Limit the amount of data that can be fetched from untrusted external HTTP resources
Diffstat (limited to 'crates/common')
-rw-r--r-- | crates/common/Cargo.toml | 2 | ||||
-rw-r--r-- | crates/common/src/enterprise/mod.rs | 12 | ||||
-rw-r--r-- | crates/common/src/lib.rs | 33 | ||||
-rw-r--r-- | crates/common/src/scripts/plugins/lookup.rs | 29 |
4 files changed, 64 insertions, 12 deletions
diff --git a/crates/common/Cargo.toml b/crates/common/Cargo.toml index 253424eb..6f30e404 100644 --- a/crates/common/Cargo.toml +++ b/crates/common/Cargo.toml @@ -31,7 +31,7 @@ tokio = { version = "1.23", features = ["net", "macros"] } tokio-rustls = { version = "0.26", default-features = false, features = ["ring", "tls12"] } futures = "0.3" rcgen = "0.12" -reqwest = { version = "0.12", default-features = false, features = ["rustls-tls-webpki-roots", "http2"]} +reqwest = { version = "0.12", default-features = false, features = ["rustls-tls-webpki-roots", "http2", "stream"]} serde = { version = "1.0", features = ["derive"]} serde_json = "1.0" base64 = "0.22" diff --git a/crates/common/src/enterprise/mod.rs b/crates/common/src/enterprise/mod.rs index e4a2230e..69608023 100644 --- a/crates/common/src/enterprise/mod.rs +++ b/crates/common/src/enterprise/mod.rs @@ -25,7 +25,7 @@ use store::Store; use trc::{AddContext, EventType, MetricType}; use utils::config::cron::SimpleCron; -use crate::{expr::Expression, manager::webadmin::Resource, Core}; +use crate::{expr::Expression, manager::webadmin::Resource, Core, HttpLimitResponse}; #[derive(Clone)] pub struct Enterprise { @@ -121,6 +121,8 @@ impl Core { } pub async fn logo_resource(&self, domain: &str) -> trc::Result<Option<Resource<Vec<u8>>>> { + const MAX_IMAGE_SIZE: usize = 1024 * 1024; + if self.is_enterprise_edition() { let domain = psl::domain_str(domain).unwrap_or(domain); let logo = { self.security.logos.lock().get(domain).cloned() }; @@ -180,7 +182,7 @@ impl Core { .to_string(); let contents = response - .bytes() + .bytes_with_limit(MAX_IMAGE_SIZE) .await .map_err(|err| { trc::ResourceEvent::DownloadExternal @@ -188,7 +190,11 @@ impl Core { .details("Failed to download logo") .reason(err) })? - .to_vec(); + .ok_or_else(|| { + trc::ResourceEvent::DownloadExternal + .into_err() + .details("Download exceeded maximum size") + })?; logo = Resource::new(content_type, contents).into(); } diff --git a/crates/common/src/lib.rs b/crates/common/src/lib.rs index d602a1ca..e8dbf60e 100644 --- a/crates/common/src/lib.rs +++ b/crates/common/src/lib.rs @@ -30,6 +30,7 @@ use directory::{ Principal, QueryBy, Type, }; use expr::if_block::IfBlock; +use futures::StreamExt; use listener::{ blocked::{AllowedIps, BlockedIps}, tls::TlsManager, @@ -38,6 +39,7 @@ use mail_send::Credentials; use manager::webadmin::Resource; use parking_lot::Mutex; +use reqwest::Response; use sieve::Sieve; use store::{ write::{QueueClass, ValueClass}, @@ -415,3 +417,34 @@ impl Clone for Security { } } } + +pub trait HttpLimitResponse: Sync + Send { + fn bytes_with_limit( + self, + limit: usize, + ) -> impl std::future::Future<Output = reqwest::Result<Option<Vec<u8>>>> + Send; +} + +impl HttpLimitResponse for Response { + async fn bytes_with_limit(self, limit: usize) -> reqwest::Result<Option<Vec<u8>>> { + if self + .content_length() + .map_or(false, |len| len as usize > limit) + { + return Ok(None); + } + + let mut bytes = Vec::with_capacity(std::cmp::min(limit, 1024)); + let mut stream = self.bytes_stream(); + + while let Some(chunk) = stream.next().await { + let chunk = chunk?; + if bytes.len() + chunk.len() > limit { + return Ok(None); + } + bytes.extend_from_slice(&chunk); + } + + Ok(Some(bytes)) + } +} diff --git a/crates/common/src/scripts/plugins/lookup.rs b/crates/common/src/scripts/plugins/lookup.rs index 404961ed..f19721c5 100644 --- a/crates/common/src/scripts/plugins/lookup.rs +++ b/crates/common/src/scripts/plugins/lookup.rs @@ -14,7 +14,9 @@ use mail_auth::flate2; use sieve::{runtime::Variable, FunctionMap}; use store::{Deserialize, Value}; -use crate::{config::scripts::RemoteList, scripts::into_sieve_value, USER_AGENT}; +use crate::{ + config::scripts::RemoteList, scripts::into_sieve_value, HttpLimitResponse, USER_AGENT, +}; use super::PluginContext; @@ -144,6 +146,8 @@ pub async fn exec_remote(ctx: PluginContext<'_>) -> trc::Result<Variable> { } } +const MAX_RESOURCE_SIZE: usize = 10 * 1024 * 1024; + async fn exec_remote_(ctx: &PluginContext<'_>) -> trc::Result<Variable> { let resource = ctx.arguments[0].to_string(); let item = ctx.arguments[1].to_string(); @@ -228,13 +232,22 @@ async fn exec_remote_(ctx: &PluginContext<'_>) -> trc::Result<Variable> { })?; if response.status().is_success() { - let bytes = response.bytes().await.map_err(|err| { - trc::SieveEvent::RuntimeError - .into_err() - .reason(err) - .ctx(trc::Key::Url, resource.to_string()) - .details("Failed to fetch resource") - })?; + let bytes = response + .bytes_with_limit(MAX_RESOURCE_SIZE) + .await + .map_err(|err| { + trc::SieveEvent::RuntimeError + .into_err() + .reason(err) + .ctx(trc::Key::Url, resource.to_string()) + .details("Failed to fetch resource") + })? + .ok_or_else(|| { + trc::SieveEvent::RuntimeError + .into_err() + .ctx(trc::Key::Url, resource.to_string()) + .details("Resource is too large") + })?; let reader: Box<dyn std::io::Read> = if resource.ends_with(".gz") { Box::new(flate2::read::GzDecoder::new(&bytes[..])) |