mirror of
https://github.com/stalwartlabs/mail-server.git
synced 2024-11-14 21:34:52 +08:00
Limit the amount of data that can be fetched from untrusted external HTTP resources
This commit is contained in:
parent
6aa5686cd3
commit
ea24106045
9 changed files with 148 additions and 36 deletions
2
Cargo.lock
generated
2
Cargo.lock
generated
|
@ -5164,10 +5164,12 @@ dependencies = [
|
|||
"sync_wrapper 1.0.1",
|
||||
"tokio",
|
||||
"tokio-rustls 0.26.0",
|
||||
"tokio-util",
|
||||
"tower-service",
|
||||
"url",
|
||||
"wasm-bindgen",
|
||||
"wasm-bindgen-futures",
|
||||
"wasm-streams",
|
||||
"web-sys",
|
||||
"webpki-roots 0.26.6",
|
||||
"windows-registry",
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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();
|
||||
}
|
||||
|
|
|
@ -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))
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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[..]))
|
||||
|
|
|
@ -608,6 +608,8 @@ impl serde::Serialize for Principal {
|
|||
}
|
||||
}
|
||||
|
||||
const MAX_STRING_LEN: usize = 512;
|
||||
|
||||
impl<'de> serde::Deserialize<'de> for PrincipalValue {
|
||||
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
|
||||
where
|
||||
|
@ -647,14 +649,22 @@ impl<'de> serde::Deserialize<'de> for PrincipalValue {
|
|||
where
|
||||
E: de::Error,
|
||||
{
|
||||
Ok(PrincipalValue::String(value))
|
||||
if value.len() <= MAX_STRING_LEN {
|
||||
Ok(PrincipalValue::String(value))
|
||||
} else {
|
||||
Err(serde::de::Error::custom("string too long"))
|
||||
}
|
||||
}
|
||||
|
||||
fn visit_str<E>(self, v: &str) -> Result<Self::Value, E>
|
||||
fn visit_str<E>(self, value: &str) -> Result<Self::Value, E>
|
||||
where
|
||||
E: de::Error,
|
||||
{
|
||||
Ok(PrincipalValue::String(v.to_string()))
|
||||
if value.len() <= MAX_STRING_LEN {
|
||||
Ok(PrincipalValue::String(value.to_string()))
|
||||
} else {
|
||||
Err(serde::de::Error::custom("string too long"))
|
||||
}
|
||||
}
|
||||
|
||||
fn visit_seq<A>(self, mut seq: A) -> Result<Self::Value, A::Error>
|
||||
|
@ -666,7 +676,13 @@ impl<'de> serde::Deserialize<'de> for PrincipalValue {
|
|||
|
||||
while let Some(value) = seq.next_element::<StringOrU64>()? {
|
||||
match value {
|
||||
StringOrU64::String(s) => vec_string.push(s),
|
||||
StringOrU64::String(s) => {
|
||||
if s.len() <= MAX_STRING_LEN {
|
||||
vec_string.push(s);
|
||||
} else {
|
||||
return Err(serde::de::Error::custom("string too long"));
|
||||
}
|
||||
}
|
||||
StringOrU64::U64(u) => vec_u64.push(u),
|
||||
}
|
||||
}
|
||||
|
@ -720,12 +736,24 @@ impl<'de> serde::Deserialize<'de> for Principal {
|
|||
})?;
|
||||
|
||||
let value = match key {
|
||||
PrincipalField::Name => PrincipalValue::String(map.next_value()?),
|
||||
PrincipalField::Name => {
|
||||
PrincipalValue::String(map.next_value::<String>().and_then(|v| {
|
||||
if v.len() <= MAX_STRING_LEN {
|
||||
Ok(v)
|
||||
} else {
|
||||
Err(serde::de::Error::custom("string too long"))
|
||||
}
|
||||
})?)
|
||||
}
|
||||
PrincipalField::Description
|
||||
| PrincipalField::Tenant
|
||||
| PrincipalField::Picture => {
|
||||
if let Some(v) = map.next_value::<Option<String>>()? {
|
||||
PrincipalValue::String(v)
|
||||
if v.len() <= MAX_STRING_LEN {
|
||||
PrincipalValue::String(v)
|
||||
} else {
|
||||
return Err(serde::de::Error::custom("string too long"));
|
||||
}
|
||||
} else {
|
||||
continue;
|
||||
}
|
||||
|
@ -798,7 +826,22 @@ impl<'de> serde::Deserialize<'de> for StringOrU64 {
|
|||
where
|
||||
E: de::Error,
|
||||
{
|
||||
Ok(StringOrU64::String(value.to_string()))
|
||||
if value.len() <= MAX_STRING_LEN {
|
||||
Ok(StringOrU64::String(value.to_string()))
|
||||
} else {
|
||||
Err(serde::de::Error::custom("string too long"))
|
||||
}
|
||||
}
|
||||
|
||||
fn visit_string<E>(self, v: String) -> Result<Self::Value, E>
|
||||
where
|
||||
E: de::Error,
|
||||
{
|
||||
if v.len() <= MAX_STRING_LEN {
|
||||
Ok(StringOrU64::String(v))
|
||||
} else {
|
||||
Err(serde::de::Error::custom("string too long"))
|
||||
}
|
||||
}
|
||||
|
||||
fn visit_u64<E>(self, value: u64) -> Result<Self::Value, E>
|
||||
|
@ -837,7 +880,22 @@ impl<'de> serde::Deserialize<'de> for StringOrMany {
|
|||
where
|
||||
E: de::Error,
|
||||
{
|
||||
Ok(StringOrMany::One(value.to_string()))
|
||||
if value.len() <= MAX_STRING_LEN {
|
||||
Ok(StringOrMany::One(value.to_string()))
|
||||
} else {
|
||||
Err(serde::de::Error::custom("string too long"))
|
||||
}
|
||||
}
|
||||
|
||||
fn visit_string<E>(self, v: String) -> Result<Self::Value, E>
|
||||
where
|
||||
E: de::Error,
|
||||
{
|
||||
if v.len() <= MAX_STRING_LEN {
|
||||
Ok(StringOrMany::One(v))
|
||||
} else {
|
||||
Err(serde::de::Error::custom("string too long"))
|
||||
}
|
||||
}
|
||||
|
||||
fn visit_seq<A>(self, mut seq: A) -> Result<Self::Value, A::Error>
|
||||
|
|
|
@ -59,6 +59,8 @@ pub enum Type {
|
|||
)]
|
||||
#[serde(rename_all = "kebab-case")]
|
||||
pub enum Permission {
|
||||
// WARNING: add new ids at the end (TODO: use static ids)
|
||||
|
||||
// Admin
|
||||
Impersonate,
|
||||
UnlimitedRequests,
|
||||
|
@ -238,6 +240,7 @@ pub enum Permission {
|
|||
SieveRenameScript,
|
||||
SieveCheckScript,
|
||||
SieveHaveSpace,
|
||||
// WARNING: add new ids at the end (TODO: use static ids)
|
||||
}
|
||||
|
||||
pub type Permissions = Bitset<
|
||||
|
|
|
@ -4,7 +4,7 @@
|
|||
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
|
||||
*/
|
||||
|
||||
use common::config::smtp::session::MTAHook;
|
||||
use common::{config::smtp::session::MTAHook, HttpLimitResponse};
|
||||
|
||||
use super::{Request, Response};
|
||||
|
||||
|
@ -28,22 +28,12 @@ pub(super) async fn send_mta_hook_request(
|
|||
.map_err(|err| format!("Hook request failed: {err}"))?;
|
||||
|
||||
if response.status().is_success() {
|
||||
if response
|
||||
.content_length()
|
||||
.map_or(false, |len| len as usize > mta_hook.max_response_size)
|
||||
{
|
||||
return Err(format!(
|
||||
"Hook response too large ({} bytes)",
|
||||
response.content_length().unwrap()
|
||||
));
|
||||
}
|
||||
|
||||
// TODO: Stream response body to limit response size
|
||||
serde_json::from_slice(
|
||||
response
|
||||
.bytes()
|
||||
.bytes_with_limit(mta_hook.max_response_size)
|
||||
.await
|
||||
.map_err(|err| format!("Failed to parse Hook response: {}", err))?
|
||||
.ok_or_else(|| "Hook response too large".to_string())?
|
||||
.as_ref(),
|
||||
)
|
||||
.map_err(|err| format!("Failed to parse Hook response: {}", err))
|
||||
|
|
|
@ -20,6 +20,12 @@ use crate::core::SMTP;
|
|||
|
||||
use super::{parse::ParsePolicy, Error};
|
||||
|
||||
#[cfg(not(feature = "test_mode"))]
|
||||
use common::HttpLimitResponse;
|
||||
|
||||
#[cfg(not(feature = "test_mode"))]
|
||||
const MAX_POLICY_SIZE: usize = 1024 * 1024;
|
||||
|
||||
#[allow(unused_variables)]
|
||||
impl SMTP {
|
||||
pub async fn lookup_mta_sts_policy<'x>(
|
||||
|
@ -61,11 +67,12 @@ impl SMTP {
|
|||
.timeout(timeout)
|
||||
.redirect(reqwest::redirect::Policy::none())
|
||||
.build()?
|
||||
.get(&format!("https://mta-sts.{domain}/.well-known/mta-sts.txt"))
|
||||
.get(format!("https://mta-sts.{domain}/.well-known/mta-sts.txt"))
|
||||
.send()
|
||||
.await?
|
||||
.bytes()
|
||||
.await?;
|
||||
.bytes_with_limit(MAX_POLICY_SIZE)
|
||||
.await?
|
||||
.ok_or_else(|| Error::InvalidPolicy("Policy too large".to_string()))?;
|
||||
#[cfg(feature = "test_mode")]
|
||||
let bytes = STS_TEST_POLICY.lock().clone();
|
||||
|
||||
|
|
Loading…
Reference in a new issue