fix: Ignore protected headers in outer message part (#6357)

Delta Chat always adds protected headers to the inner encrypted or signed message, so if a protected
header is only present in the outer part, it should be ignored because it's probably added by the
server or somebody else. The exceptions are Subject and List-ID because there are known cases when
they are only present in the outer message part.

Also treat any Chat-* headers as protected. This fixes e.g. a case when the server injects a
"Chat-Version" IMF header tricking Delta Chat into thinking that it's a chat message.

Also handle "Auto-Submitted" and "Autocrypt-Setup-Message" as protected headers on the receiver
side, this was apparently forgotten.
This commit is contained in:
iequidoo
2024-12-27 22:20:44 -03:00
committed by iequidoo
parent a0f6bdffeb
commit 6d8dff54a7
4 changed files with 81 additions and 21 deletions

View File

@@ -178,7 +178,8 @@ mod tests {
let bob = TestContext::new_bob().await; let bob = TestContext::new_bob().await;
receive_imf(&bob, attachment_mime, false).await?; receive_imf(&bob, attachment_mime, false).await?;
let msg = bob.get_last_msg().await; let msg = bob.get_last_msg().await;
assert_eq!(msg.text, "Hello from Thunderbird!"); // Subject should be prepended because the attachment doesn't have "Chat-Version".
assert_eq!(msg.text, "Hello, Bob! Hello from Thunderbird!");
Ok(()) Ok(())
} }

View File

@@ -246,6 +246,7 @@ impl MimeMessage {
MimeMessage::merge_headers( MimeMessage::merge_headers(
context, context,
&mut headers, &mut headers,
&mut headers_removed,
&mut recipients, &mut recipients,
&mut past_members, &mut past_members,
&mut from, &mut from,
@@ -273,6 +274,7 @@ impl MimeMessage {
MimeMessage::merge_headers( MimeMessage::merge_headers(
context, context,
&mut headers, &mut headers,
&mut headers_removed,
&mut recipients, &mut recipients,
&mut past_members, &mut past_members,
&mut from, &mut from,
@@ -446,26 +448,11 @@ impl MimeMessage {
}); });
if let (Ok(mail), true) = (mail, is_encrypted) { if let (Ok(mail), true) = (mail, is_encrypted) {
if !signatures.is_empty() { if !signatures.is_empty() {
// Remove unsigned opportunistically protected headers from messages considered // Unsigned "Subject" mustn't be prepended to messages shown as encrypted
// Autocrypt-encrypted / displayed with padlock. // (<https://github.com/deltachat/deltachat-core-rust/issues/1790>).
// For "Subject" see <https://github.com/deltachat/deltachat-core-rust/issues/1790>. // Other headers are removed by `MimeMessage::merge_headers()` except for "List-ID".
for h in [ remove_header(&mut headers, "subject", &mut headers_removed);
HeaderDef::Subject, remove_header(&mut headers, "list-id", &mut headers_removed);
HeaderDef::ChatGroupId,
HeaderDef::ChatGroupName,
HeaderDef::ChatGroupNameChanged,
HeaderDef::ChatGroupNameTimestamp,
HeaderDef::ChatGroupAvatar,
HeaderDef::ChatGroupMemberRemoved,
HeaderDef::ChatGroupMemberAdded,
HeaderDef::ChatGroupMemberTimestamps,
HeaderDef::ChatGroupPastMembers,
HeaderDef::ChatDelete,
HeaderDef::ChatEdit,
HeaderDef::ChatUserAvatar,
] {
remove_header(&mut headers, h.get_headername(), &mut headers_removed);
}
} }
// let known protected headers from the decrypted // let known protected headers from the decrypted
@@ -478,6 +465,7 @@ impl MimeMessage {
MimeMessage::merge_headers( MimeMessage::merge_headers(
context, context,
&mut headers, &mut headers,
&mut headers_removed,
&mut recipients, &mut recipients,
&mut past_members, &mut past_members,
&mut inner_from, &mut inner_from,
@@ -1558,6 +1546,7 @@ impl MimeMessage {
fn merge_headers( fn merge_headers(
context: &Context, context: &Context,
headers: &mut HashMap<String, String>, headers: &mut HashMap<String, String>,
headers_removed: &mut HashSet<String>,
recipients: &mut Vec<SingleInfo>, recipients: &mut Vec<SingleInfo>,
past_members: &mut Vec<SingleInfo>, past_members: &mut Vec<SingleInfo>,
from: &mut Option<SingleInfo>, from: &mut Option<SingleInfo>,
@@ -1565,6 +1554,12 @@ impl MimeMessage {
chat_disposition_notification_to: &mut Option<SingleInfo>, chat_disposition_notification_to: &mut Option<SingleInfo>,
fields: &[mailparse::MailHeader<'_>], fields: &[mailparse::MailHeader<'_>],
) { ) {
headers.retain(|k, _| {
!is_protected(k) || {
headers_removed.insert(k.to_string());
false
}
});
for field in fields { for field in fields {
// lowercasing all headers is technically not correct, but makes things work better // lowercasing all headers is technically not correct, but makes things work better
let key = field.get_key().to_lowercase(); let key = field.get_key().to_lowercase();
@@ -2005,6 +2000,32 @@ pub(crate) fn parse_message_id(ids: &str) -> Result<String> {
} }
} }
/// Returns whether the outer header value must be ignored if the message contains a signed (and
/// optionally encrypted) part.
///
/// NB: There are known cases when Subject and List-ID only appear in the outer headers of
/// signed-only messages. Such messages are shown as unencrypted anyway.
fn is_protected(key: &str) -> bool {
key.starts_with("chat-")
|| matches!(
key,
"return-path"
| "auto-submitted"
| "autocrypt-setup-message"
| "date"
| "from"
| "sender"
| "reply-to"
| "to"
| "cc"
| "bcc"
| "message-id"
| "in-reply-to"
| "references"
| "secure-join"
)
}
/// Returns if the header is hidden and must be ignored in the IMF section. /// Returns if the header is hidden and must be ignored in the IMF section.
pub(crate) fn is_hidden(key: &str) -> bool { pub(crate) fn is_hidden(key: &str) -> bool {
matches!( matches!(

View File

@@ -1402,6 +1402,26 @@ async fn test_x_microsoft_original_message_id_precedence() -> Result<()> {
Ok(()) Ok(())
} }
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_extra_imf_chat_header() -> Result<()> {
let mut tcm = TestContextManager::new();
let t = &tcm.alice().await;
let chat_id = t.get_self_chat().await.id;
chat::send_text_msg(t, chat_id, "hi!".to_string()).await?;
let sent_msg = t.pop_sent_msg().await;
// Check removal of some nonexistent "Chat-*" header to protect the code from future breakages.
let payload = sent_msg
.payload
.replace("Message-ID:", "Chat-Forty-Two: 42\r\nMessage-ID:");
let msg = MimeMessage::from_bytes(t, payload.as_bytes(), None)
.await
.unwrap();
assert!(msg.headers.contains_key("chat-version"));
assert!(!msg.headers.contains_key("chat-forty-two"));
Ok(())
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)] #[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_long_in_reply_to() -> Result<()> { async fn test_long_in_reply_to() -> Result<()> {
let t = TestContext::new_alice().await; let t = TestContext::new_alice().await;

View File

@@ -3682,6 +3682,24 @@ async fn test_unsigned_chat_group_hdr() -> Result<()> {
Ok(()) Ok(())
} }
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_ignore_protected_headers_in_outer_msg() -> Result<()> {
let mut tcm = TestContextManager::new();
let alice = &tcm.alice().await;
let bob = &tcm.bob().await;
let bob_chat_id = tcm.send_recv_accept(alice, bob, "hi").await.chat_id;
send_text_msg(bob, bob_chat_id, "hi all!".to_string()).await?;
let mut sent_msg = bob.pop_sent_msg().await;
sent_msg.payload = sent_msg.payload.replace(
"Chat-Version:",
"Auto-Submitted: auto-generated\r\nChat-Version:",
);
alice.recv_msg(&sent_msg).await;
let ab_contact = alice.add_or_lookup_contact(bob).await;
assert!(!ab_contact.is_bot());
Ok(())
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)] #[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_sync_member_list_on_rejoin() -> Result<()> { async fn test_sync_member_list_on_rejoin() -> Result<()> {
let mut tcm = TestContextManager::new(); let mut tcm = TestContextManager::new();