summaryrefslogtreecommitdiff
path: root/crates/common
diff options
context:
space:
mode:
authormdecimus <mauro@stalw.art>2024-09-20 11:43:25 +0200
committermdecimus <mauro@stalw.art>2024-09-20 11:43:25 +0200
commitea241060450504643d02d84b1f0ff48539a566f3 (patch)
treeefa614cef1248d7862642a8e477eccaf3ce9cb7b /crates/common
parent6aa5686cd3cde108229866b73ac47fc07cc8282c (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.toml2
-rw-r--r--crates/common/src/enterprise/mod.rs12
-rw-r--r--crates/common/src/lib.rs33
-rw-r--r--crates/common/src/scripts/plugins/lookup.rs29
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[..]))