feat(contact): sanitize user-submitted data
This commit is contained in:
@@ -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, "Привіт");
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user