From 0d66d7503d8fd147842e87b9b4f1b921986ef44b Mon Sep 17 00:00:00 2001 From: Jan Kahmen <36455663+kah-ja@users.noreply.github.com> Date: Mon, 17 Aug 2026 23:03:41 +0200 Subject: [PATCH 1/3] fix(mail): stop interpreting iMIP card text fields as markup the whole server built html of an iMIP card is rendered through sg-compile (Message.service.js sets part.compile for UIxMailPartICalViewer), and the user comment and the description are emitted with escapeHTML="NO". event data from the invitation is therefore both an angular template and raw html. 47133fdf3 closed the description against interpolation with ng-non-bindable. this closes both paths for every remaining field: * ng-non-bindable on the user comment, the location, the organizer common name and the raw body shown when the calendar cannot be parsed * the user comment and the description are read through accessors that escape them, so the url detection and insertBR produce the only markup left in the output the marker sits on the leaf containers, not on md-card-content, so the buttons, the delegation autocomplete and the attendee chips keep being compiled. url detection still turns a bare url in a comment or a description into a link. --- UI/MailPartViewers/UIxMailPartICalViewer.h | 2 ++ UI/MailPartViewers/UIxMailPartICalViewer.m | 11 ++++++++++ .../MailPartViewers/UIxMailPartICalViewer.wox | 20 +++++++++---------- 3 files changed, 23 insertions(+), 10 deletions(-) diff --git a/UI/MailPartViewers/UIxMailPartICalViewer.h b/UI/MailPartViewers/UIxMailPartICalViewer.h index 1ffde026e..bf12be30a 100644 --- a/UI/MailPartViewers/UIxMailPartICalViewer.h +++ b/UI/MailPartViewers/UIxMailPartICalViewer.h @@ -46,6 +46,8 @@ - (BOOL) isEndDateOnSameDay; - (BOOL) hasLocation; - (NSString *)location; +- (NSString *) userComment; +- (NSString *) eventDescription; @end diff --git a/UI/MailPartViewers/UIxMailPartICalViewer.m b/UI/MailPartViewers/UIxMailPartICalViewer.m index f7cba20fd..d6dedac4b 100644 --- a/UI/MailPartViewers/UIxMailPartICalViewer.m +++ b/UI/MailPartViewers/UIxMailPartICalViewer.m @@ -32,6 +32,7 @@ #import #import +#import #import @@ -612,4 +613,14 @@ return [[self inEvent] location]; } +- (NSString *) userComment +{ + return [[[self inEvent] userComment] stringByEscapingHTMLString]; +} + +- (NSString *) eventDescription +{ + return [[[self authorativeEvent] comment] stringByEscapingHTMLString]; +} + @end /* UIxMailPartICalViewer */ diff --git a/UI/Templates/MailPartViewers/UIxMailPartICalViewer.wox b/UI/Templates/MailPartViewers/UIxMailPartICalViewer.wox index 873554c04..5ebf7a6b3 100644 --- a/UI/Templates/MailPartViewers/UIxMailPartICalViewer.wox +++ b/UI/Templates/MailPartViewers/UIxMailPartICalViewer.wox @@ -17,7 +17,7 @@ -
+
@@ -137,7 +137,7 @@

-

@@ -147,7 +147,7 @@

- + @@ -233,14 +233,14 @@ - +

-
+
- +
@@ -251,7 +251,7 @@
-
+
@@ -268,7 +268,7 @@
-
+
@@ -294,14 +294,14 @@
- +
- +
From 8813677ecb171e5d27cd582989c1d87b308c6df3 Mon Sep 17 00:00:00 2001 From: Jan Kahmen <36455663+kah-ja@users.noreply.github.com> Date: Mon, 17 Aug 2026 23:03:41 +0200 Subject: [PATCH 2/3] fix(core): keep the @import cleanup working in stringWithoutHTMLInjection the trailing while loop reuses the regex variable, which by then points at the angular brace pattern whenever stripAngular is YES. that pattern has two capture groups while the loop substitutes $1@im****$3, so NSRegularExpression raises NSInvalidArgumentException and the NS_HANDLER swallows it. give the @import pattern its own variable and use it in both the replacement and the loop. the replacement also used the template of the block above, which dropped the whole style element instead of masking the @import in place, so test_stringWithoutHTMLInjection failed. it passes again, and a case with stripAngular YES covers the loop. --- SoObjects/SOGo/NSString+Utilities.m | 11 ++++++----- Tests/Unit/TestNSString+Utilities.m | 2 ++ 2 files changed, 8 insertions(+), 5 deletions(-) diff --git a/SoObjects/SOGo/NSString+Utilities.m b/SoObjects/SOGo/NSString+Utilities.m index 441d471eb..9e5898a15 100644 --- a/SoObjects/SOGo/NSString+Utilities.m +++ b/SoObjects/SOGo/NSString+Utilities.m @@ -942,12 +942,13 @@ static int cssEscapingCount; NSScanner *theScanner; NSError *error; NSUInteger numberOfMatches; - NSRegularExpression *regex; + NSRegularExpression *regex, *importRegex; text = nil; error = nil; result = [NSString stringWithString: self]; regex = nil; + importRegex = nil; NS_DURING { @@ -1056,9 +1057,9 @@ static int cssEscapingCount; result = [NSString stringWithString: newResult]; // Remove @import css (in style tags) - regex = [NSRegularExpression regularExpressionWithPattern:@"(<[\\s\\u200B \\\\0]*s[\\s\\u200B \\\\0]*t[\\s\\u200B \\\\0]*y[\\s\\u200B \\\\0]*l[\\s\\u200B \\\\0]*e.*)([\\s\\u200B \\\\0]*@[\\s\\u200B \\\\0]*i[\\s\\u200B \\\\0]*m[\\s\\u200B \\\\0]*p[\\s\\u200B \\\\0]*o[\\s\\u200B \\\\0]*r[\\s\\u200B \\\\0]*t)(.*<[\\s\\u200B \\\\0]*\\/[\\s\\u200B \\\\0]*s[\\s\\u200B \\\\0]*t[\\s\\u200B \\\\0]*y[\\s\\u200B \\\\0]*l[\\s\\u200B \\\\0]*e[\\s\\u200B \\\\0]*>)" + importRegex = [NSRegularExpression regularExpressionWithPattern:@"(<[\\s\\u200B \\\\0]*s[\\s\\u200B \\\\0]*t[\\s\\u200B \\\\0]*y[\\s\\u200B \\\\0]*l[\\s\\u200B \\\\0]*e.*)([\\s\\u200B \\\\0]*@[\\s\\u200B \\\\0]*i[\\s\\u200B \\\\0]*m[\\s\\u200B \\\\0]*p[\\s\\u200B \\\\0]*o[\\s\\u200B \\\\0]*r[\\s\\u200B \\\\0]*t)(.*<[\\s\\u200B \\\\0]*\\/[\\s\\u200B \\\\0]*s[\\s\\u200B \\\\0]*t[\\s\\u200B \\\\0]*y[\\s\\u200B \\\\0]*l[\\s\\u200B \\\\0]*e[\\s\\u200B \\\\0]*>)" options: NSRegularExpressionCaseInsensitive error:&error]; - newResult = [regex stringByReplacingMatchesInString:result options:0 range:NSMakeRange(0, [result length]) withTemplate:@"onrep***="]; + newResult = [importRegex stringByReplacingMatchesInString:result options:0 range:NSMakeRange(0, [result length]) withTemplate:@"$1@im****$3"]; result = [NSString stringWithString: newResult]; @@ -1075,8 +1076,8 @@ static int cssEscapingCount; } newResult = result; - while([regex numberOfMatchesInString:newResult options:0 range:NSMakeRange(0, [newResult length])] > 0) { - newResult = [regex stringByReplacingMatchesInString:newResult options:0 range:NSMakeRange(0, [newResult length]) withTemplate:@"$1@im****$3"]; + while([importRegex numberOfMatchesInString:newResult options:0 range:NSMakeRange(0, [newResult length])] > 0) { + newResult = [importRegex stringByReplacingMatchesInString:newResult options:0 range:NSMakeRange(0, [newResult length]) withTemplate:@"$1@im****$3"]; } result = [NSString stringWithString: newResult]; } diff --git a/Tests/Unit/TestNSString+Utilities.m b/Tests/Unit/TestNSString+Utilities.m index 94efa8db9..fdddf10f1 100644 --- a/Tests/Unit/TestNSString+Utilities.m +++ b/Tests/Unit/TestNSString+Utilities.m @@ -110,6 +110,8 @@ testEquals([[NSString stringWithString:@"foobar
ABC
"] stringWithoutHTMLInjection: NO stripAngular: NO], @"
ABC
"); + // the @import cleanup must still run when angular interpolation is stripped as well + testEquals([[NSString stringWithString:@""] stringWithoutHTMLInjection: NO stripAngular: YES], @""); } - (void) test_stringCleanInvalidHTMLTags From 045a0b9c0ac57aeea1b619f281acff0427cd81c7 Mon Sep 17 00:00:00 2001 From: Jan Kahmen <36455663+kah-ja@users.noreply.github.com> Date: Mon, 17 Aug 2026 23:43:06 +0200 Subject: [PATCH 3/3] fix(mail): escape mail data placed in attributes of a compiled part the generic attribute writer used for these two spots does not escape what it writes, so a value taken from the message ends the attribute and starts a new one. both are inside a part that is compiled, which is where an injected handler runs. * the organizer link took inEvent.organizer.email verbatim. a value such as mailto:x@y" onpointerover="... produced a live handler on the anchor, and an entity encoded scheme such as javascript: reached the href, where the browser decodes it. the href is now built as mailto: plus the parsed address and escaped as an attribute value. * the attachment name paragraph of the image and the link viewer took filenameForDisplay verbatim. a quote inside an RFC 2231 encoded filename ended the title attribute. the writer escapes & < > there but not the quote, so the accessor drops the quote instead of escaping it, which keeps a plain filename such as A&B.pdf unchanged in the tooltip. the img title of the image viewer is left alone: attributes of that element are escaped by the framework already. --- UI/MailPartViewers/UIxMailPartICalViewer.h | 1 + UI/MailPartViewers/UIxMailPartICalViewer.m | 12 ++++++++++++ UI/MailPartViewers/UIxMailPartViewer.h | 1 + UI/MailPartViewers/UIxMailPartViewer.m | 8 ++++++++ .../MailPartViewers/UIxMailPartICalViewer.wox | 4 ++-- .../MailPartViewers/UIxMailPartImageViewer.wox | 2 +- .../MailPartViewers/UIxMailPartLinkViewer.wox | 2 +- 7 files changed, 26 insertions(+), 4 deletions(-) diff --git a/UI/MailPartViewers/UIxMailPartICalViewer.h b/UI/MailPartViewers/UIxMailPartICalViewer.h index bf12be30a..dada2cb3e 100644 --- a/UI/MailPartViewers/UIxMailPartICalViewer.h +++ b/UI/MailPartViewers/UIxMailPartICalViewer.h @@ -46,6 +46,7 @@ - (BOOL) isEndDateOnSameDay; - (BOOL) hasLocation; - (NSString *)location; +- (NSString *) organizerHref; - (NSString *) userComment; - (NSString *) eventDescription; diff --git a/UI/MailPartViewers/UIxMailPartICalViewer.m b/UI/MailPartViewers/UIxMailPartICalViewer.m index d6dedac4b..6d214485e 100644 --- a/UI/MailPartViewers/UIxMailPartICalViewer.m +++ b/UI/MailPartViewers/UIxMailPartICalViewer.m @@ -418,6 +418,18 @@ return YES; } +- (NSString *) organizerHref +{ + NSString *address; + + address = [[[self inEvent] organizer] rfc822Email]; + if (![address length]) + return nil; + + return [[NSString stringWithFormat: @"mailto:%@", address] + stringByEscapingHTMLAttributeValue]; +} + - (NSString *) organizerDisplayName { iCalPerson *organizer; diff --git a/UI/MailPartViewers/UIxMailPartViewer.h b/UI/MailPartViewers/UIxMailPartViewer.h index 0cb3622e3..7a3c230c2 100644 --- a/UI/MailPartViewers/UIxMailPartViewer.h +++ b/UI/MailPartViewers/UIxMailPartViewer.h @@ -86,6 +86,7 @@ - (NSString *)preferredPathExtension; - (NSString *)filename; - (NSString *)filenameForDisplay; +- (NSString *)filenameForTitle; - (NSFormatter *)sizeFormatter; /* caches */ diff --git a/UI/MailPartViewers/UIxMailPartViewer.m b/UI/MailPartViewers/UIxMailPartViewer.m index 38e80422f..0501908c9 100644 --- a/UI/MailPartViewers/UIxMailPartViewer.m +++ b/UI/MailPartViewers/UIxMailPartViewer.m @@ -340,6 +340,14 @@ : (id)@"untitled"; } +- (NSString *) filenameForTitle +{ + /* the generic attribute writer escapes & but not the quote, so a quote in the + filename would end the title attribute and start a new one */ + return [[self filenameForDisplay] stringByReplacingOccurrencesOfString: @"\"" + withString: @""]; +} + - (NSFormatter *) sizeFormatter { return [UIxMailSizeFormatter sharedMailSizeFormatter]; diff --git a/UI/Templates/MailPartViewers/UIxMailPartICalViewer.wox b/UI/Templates/MailPartViewers/UIxMailPartICalViewer.wox index 5ebf7a6b3..d18f5c8db 100644 --- a/UI/Templates/MailPartViewers/UIxMailPartICalViewer.wox +++ b/UI/Templates/MailPartViewers/UIxMailPartICalViewer.wox @@ -137,7 +137,7 @@

-

@@ -147,7 +147,7 @@

- + diff --git a/UI/Templates/MailPartViewers/UIxMailPartImageViewer.wox b/UI/Templates/MailPartViewers/UIxMailPartImageViewer.wox index 29c8b57ea..62fb6d8ce 100644 --- a/UI/Templates/MailPartViewers/UIxMailPartImageViewer.wox +++ b/UI/Templates/MailPartViewers/UIxMailPartImageViewer.wox @@ -10,7 +10,7 @@ -

+

diff --git a/UI/Templates/MailPartViewers/UIxMailPartLinkViewer.wox b/UI/Templates/MailPartViewers/UIxMailPartLinkViewer.wox index 174dfbf08..1e5e749ca 100644 --- a/UI/Templates/MailPartViewers/UIxMailPartLinkViewer.wox +++ b/UI/Templates/MailPartViewers/UIxMailPartLinkViewer.wox @@ -6,7 +6,7 @@ xmlns:label="OGo:label"> -

+