feat(contact): sanitize user-submitted data

This commit is contained in:
2026-06-02 00:57:01 +02:00
parent 5baa73d272
commit 85621d9364
+99 -1
View File
@@ -18,6 +18,23 @@ use crate::settings::{EmailSettings, Starttls};
pub mod errors; pub mod errors;
use errors::ContactError; use errors::ContactError;
/// Strips control characters that could enable protocol injection
///
/// When `keep_newlines` is true, `\n` is preserved (needed for
/// multi-line fields). For name and email fields, all control
/// characters are removed - no assumptions are made about valid name
/// *content*.
fn strip_control_chars(s: &str, keep_newlines: bool) -> String {
s.chars()
.filter(|c| {
if keep_newlines && (*c == '\n') {
return true;
}
!c.is_control()
})
.collect()
}
impl TryFrom<&EmailSettings> for SmtpTransport { impl TryFrom<&EmailSettings> for SmtpTransport {
type Error = lettre::transport::smtp::Error; type Error = lettre::transport::smtp::Error;
@@ -72,6 +89,14 @@ struct ContactRequest {
honeypot: Option<String>, honeypot: Option<String>,
} }
impl ContactRequest {
fn sanitize(&mut self) {
self.name = strip_control_chars(&self.name, false);
self.email = strip_control_chars(&self.email, false);
self.message = strip_control_chars(&self.message, true);
}
}
impl TryFrom<&ContactRequest> for lettre::message::Mailbox { impl TryFrom<&ContactRequest> for lettre::message::Mailbox {
type Error = ContactError; type Error = ContactError;
@@ -160,7 +185,7 @@ impl ContactApi {
body: Json<ContactRequest>, body: Json<ContactRequest>,
remote_addr: Option<poem::web::Data<&poem::web::RemoteAddr>>, remote_addr: Option<poem::web::Data<&poem::web::RemoteAddr>>,
) -> ContactApiResponse { ) -> ContactApiResponse {
let body = body.0; let mut body = body.0;
if let Some(ref honeypot) = body.honeypot if let Some(ref honeypot) = body.honeypot
&& !honeypot.trim().is_empty() && !honeypot.trim().is_empty()
{ {
@@ -172,6 +197,7 @@ impl ContactApi {
); );
return ContactApiResponse::Ok(ContactResponse::honeypot_response().into()); return ContactApiResponse::Ok(ContactResponse::honeypot_response().into());
} }
body.sanitize();
if let Err(e) = body.validate() { if let Err(e) = body.validate() {
return ContactApiResponse::BadRequest( return ContactApiResponse::BadRequest(
<validator::ValidationErrors as std::convert::Into<ContactResponse>>::into(e) <validator::ValidationErrors as std::convert::Into<ContactResponse>>::into(e)
@@ -1002,4 +1028,76 @@ mod tests {
e => panic!("Expected CouldNotSendEmail, got {e:?}"), e => panic!("Expected CouldNotSendEmail, got {e:?}"),
} }
} }
#[test]
fn strip_control_chars_removes_null_bytes() {
let result = strip_control_chars("John\x00Doe", false);
assert_eq!(result, "JohnDoe");
}
#[test]
fn contact_request_sanatize_strips_all_control_chars() {
let mut request = ContactRequest {
name: "John\x00Doe".into(),
email: "john\x00@example.com".into(),
message: "Test\x00message".into(),
honeypot: None,
};
request.sanitize();
assert_eq!(request.name, "JohnDoe");
assert_eq!(request.email, "john@example.com");
assert_eq!(request.message, "Testmessage");
}
#[test]
fn contact_request_sanitize_preserves_newlines_in_message() {
let mut request = ContactRequest {
name: "John\nDoe".into(),
email: "john@example.com".into(),
message: "Line 1\nLine 2\r\nLine 3".into(),
honeypot: None,
};
request.sanitize();
assert_eq!(request.name, "JohnDoe");
assert_eq!(request.email, "john@example.com");
assert_eq!(request.message, "Line 1\nLine 2\nLine 3");
}
#[test]
fn contact_request_sanatize_preserves_unicode_name() {
let mut request_jp = ContactRequest {
name: "田中さん".into(),
email: "tanaka@example.com".into(),
message: "こんにちは!".into(),
honeypot: None,
};
request_jp.sanitize();
assert_eq!(request_jp.name, "田中さん");
assert_eq!(request_jp.email, "tanaka@example.com");
assert_eq!(request_jp.message, "こんにちは!");
let mut request_ar = ContactRequest {
name: "عبدالله".into(),
email: "abdullah@example.com".into(),
message: "مرحباً".into(),
honeypot: None,
};
request_ar.sanitize();
assert_eq!(request_ar.name, "عبدالله");
assert_eq!(request_ar.email, "abdullah@example.com");
assert_eq!(request_ar.message, "مرحباً");
let mut request_uk = ContactRequest {
name: "Олексáндр".into(),
email: "oleksandr@example.com".into(),
message: "Привіт".into(),
honeypot: None,
};
request_uk.sanitize();
assert_eq!(request_uk.name, "Олексáндр");
assert_eq!(request_uk.email, "oleksandr@example.com");
assert_eq!(request_uk.message, "Привіт");
}
} }