From 20779cf3531b52f3bb0795e9f17d9d580918cf2f Mon Sep 17 00:00:00 2001 From: Hadrien Blanc Date: Tue, 29 Sep 2026 09:17:53 +0200 Subject: [PATCH] fix(mail): sanitize the domain part of generated message-ids generateMessageID: appended the substring following the last '@' without sanitizing it, so a sender value such as 'Doe, John ' produced 'Message-Id: >' with a duplicated closing bracket on calendar invitations. A nil domain also left the message-id unterminated. The domain part is now cut at the first address delimiter or whitespace - a domain is a single token, and anything else would allow header injection through generated message-ids. Fixes #6201 --- SoObjects/Mailer/NSString+Mail.m | 12 ++++++--- Tests/Unit/GNUmakefile | 4 ++- Tests/Unit/TestNSString+Mail.m | 45 ++++++++++++++++++++++++++++++++ 3 files changed, 57 insertions(+), 4 deletions(-) create mode 100644 Tests/Unit/TestNSString+Mail.m diff --git a/SoObjects/Mailer/NSString+Mail.m b/SoObjects/Mailer/NSString+Mail.m index bb580b96c..eb2775484 100644 --- a/SoObjects/Mailer/NSString+Mail.m +++ b/SoObjects/Mailer/NSString+Mail.m @@ -564,7 +564,7 @@ { NSMutableString *messageID; NSString *_domain; - NSRange r; + NSRange r, cutRange; messageID = [NSMutableString string]; [messageID appendFormat: @"<%@", [SOGoObject mailUniqueMessageId]]; @@ -578,9 +578,15 @@ } else _domain = mailOrDomain; - [messageID appendFormat: @"@%@>", _domain]; + _domain = [[_domain componentsSeparatedByString: @">"] objectAtIndex: 0]; + cutRange = [_domain rangeOfCharacterFromSet: + [NSCharacterSet whitespaceAndNewlineCharacterSet]]; + if (cutRange.location != NSNotFound) + _domain = [_domain substringToIndex: cutRange.location]; + if ([_domain length] > 0) + [messageID appendFormat: @"@%@", _domain]; } - + [messageID appendString: @">"]; return [messageID lowercaseString]; } diff --git a/Tests/Unit/GNUmakefile b/Tests/Unit/GNUmakefile index 2cdee513f..8fe7e2149 100644 --- a/Tests/Unit/GNUmakefile +++ b/Tests/Unit/GNUmakefile @@ -14,6 +14,7 @@ $(TEST_TOOL)_OBJC_FILES += \ SOGoTest.m \ SOGoTestRunner.m \ SaxXMLReaderFactory+SOGoTests.m \ + ../../SoObjects/Mailer/NSString+Mail.m \ \ TestVersit.m \ TestiCalTimeZonePeriod.m \ @@ -31,6 +32,7 @@ $(TEST_TOOL)_OBJC_FILES += \ TestNSString+Crypto.m \ TestNSString+URLEscaping.m \ TestNSString+Utilities.m \ + TestNSString+Mail.m \ TestNSURL+misc.m \ TestNGMailAddressParser.m \ TestNGInternetSocketAddress.m \ @@ -43,7 +45,7 @@ $(TEST_TOOL)_OBJC_FILES += \ TEST_TOOL_NAME = $(TEST_TOOL) $(TEST_TOOL)_CPPFLAGS += \ - -Wall -D_GNU_SOURCE -I../../SOPE/ -I../../SoObjects/ -I../../UI/ -I../../OpenChange + -Wall -D_GNU_SOURCE -I../../SOPE/ -I../../SoObjects/ -I../../UI/ -I../../OpenChange `xml2-config --cflags` ADDITIONAL_LIB_DIRS += \ -L../../SoObjects/SOGo/SOGo.framework/Versions/Current/sogo -L../../SOPE/NGCards/obj -L../../SOPE/GDLContentStore/obj -lSOGo -lNGMime -lNGCards -lGDLContentStore -lNGExtensions -lSBJson -lobjc \ diff --git a/Tests/Unit/TestNSString+Mail.m b/Tests/Unit/TestNSString+Mail.m new file mode 100644 index 000000000..785cf513c --- /dev/null +++ b/Tests/Unit/TestNSString+Mail.m @@ -0,0 +1,45 @@ +#import "SOGoTest.h" + +#import "Mailer/NSString+Mail.h" + +static NSString * +MessageIDShape(NSString *mailOrDomain) +{ + NSString *messageID = [NSString generateMessageID: mailOrDomain]; + + return [@""); + testEquals(MessageIDShape(@"Example.ORG"), @""); +} + +- (void) test_generateMessageID_fromSenderWithDisplayName +{ + testEquals(MessageIDShape(@"Doe, John "), @""); + testEquals(MessageIDShape(@"Doe, John (work)"), @""); + testEquals(MessageIDShape(@"user@example.org>"), @""); +} + +- (void) test_generateMessageID_rejectsInjectedDomains +{ + testEquals(MessageIDShape(@"user@example.org\r\nBcc: victim"), + @""); + testEquals(MessageIDShape(@"user@example.org Bcc: victim"), + @""); +} + +- (void) test_generateMessageID_withoutDomain +{ + testEquals(MessageIDShape(@""), @""); + testEquals(MessageIDShape(nil), @""); +} + +@end