feat(contact): sanitize user-submitted data
This commit is contained in:
@@ -18,6 +18,23 @@ use crate::settings::{EmailSettings, Starttls};
|
||||
pub mod errors;
|
||||
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 {
|
||||
type Error = lettre::transport::smtp::Error;
|
||||
|
||||
@@ -72,6 +89,14 @@ struct ContactRequest {
|
||||
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 {
|
||||
type Error = ContactError;
|
||||
|
||||
@@ -160,7 +185,7 @@ impl ContactApi {
|
||||
body: Json<ContactRequest>,
|
||||
remote_addr: Option<poem::web::Data<&poem::web::RemoteAddr>>,
|
||||
) -> ContactApiResponse {
|
||||
let body = body.0;
|
||||
let mut body = body.0;
|
||||
if let Some(ref honeypot) = body.honeypot
|
||||
&& !honeypot.trim().is_empty()
|
||||
{
|
||||
@@ -172,6 +197,7 @@ impl ContactApi {
|
||||
);
|
||||
return ContactApiResponse::Ok(ContactResponse::honeypot_response().into());
|
||||
}
|
||||
body.sanitize();
|
||||
if let Err(e) = body.validate() {
|
||||
return ContactApiResponse::BadRequest(
|
||||
<validator::ValidationErrors as std::convert::Into<ContactResponse>>::into(e)
|
||||
@@ -1002,4 +1028,76 @@ mod tests {
|
||||
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