From 76d75de17b568264aa9dccebaccdd32beebcba59 Mon Sep 17 00:00:00 2001 From: Hivert Quentin Date: Tue, 10 Nov 2026 14:59:59 +0100 Subject: [PATCH] #3122: fix sieve injection --- SoObjects/SOGo/SOGoSieveManager.m | 24 +++++--- UI/PreferencesUI/UIxPreferences.m | 95 ++++++++++++++++++++++++++++++- 2 files changed, 109 insertions(+), 10 deletions(-) diff --git a/SoObjects/SOGo/SOGoSieveManager.m b/SoObjects/SOGo/SOGoSieveManager.m index a92db13a4..918402c46 100644 --- a/SoObjects/SOGo/SOGoSieveManager.m +++ b/SoObjects/SOGo/SOGoSieveManager.m @@ -1094,8 +1094,8 @@ static NSString *sieveScriptName = @"sogo"; if ([[values objectForKey: @"startTimeEnabled"] boolValue]) { startTime = [values objectForKey: @"startTime"]; - timeCondition = [NSString stringWithFormat: @"date :value \"ge\" :zone \"%@\" \"date\" \"time\" \"%@:00\"", - timeZone, startTime]; + timeCondition = [NSString stringWithFormat: @"date :value \"ge\" :zone \"%@\" \"date\" \"time\" %@", + timeZone, [[NSString stringWithFormat: @"%@:00", startTime] asSieveQuotedString]]; [timeConditions addObject: timeCondition]; } @@ -1103,8 +1103,8 @@ static NSString *sieveScriptName = @"sogo"; if ([[values objectForKey: @"endTimeEnabled"] boolValue]) { endTime = [values objectForKey: @"endTime"]; - timeCondition = [NSString stringWithFormat: @"date :value \"lt\" :zone \"%@\" \"date\" \"time\" \"%@:00\"", - timeZone, endTime]; + timeCondition = [NSString stringWithFormat: @"date :value \"lt\" :zone \"%@\" \"date\" \"time\" %@", + timeZone, [[NSString stringWithFormat: @"%@:00", endTime] asSieveQuotedString]]; [timeConditions addObject: timeCondition]; } @@ -1176,7 +1176,7 @@ static NSString *sieveScriptName = @"sogo"; [vacation_script appendString: @" :addresses ["]; for (i = 0; i < [addresses count]; i++) { - [vacation_script appendFormat: @"\"%@\"", [addresses objectAtIndex: i]]; + [vacation_script appendString: [[NSString stringWithFormat: @"%@", [addresses objectAtIndex: i]] asSieveQuotedString]]; if (i == [addresses count]-1) [vacation_script appendString: @"] "]; @@ -1184,7 +1184,7 @@ static NSString *sieveScriptName = @"sogo"; [vacation_script appendString: @", "]; } - [vacation_script appendFormat: @"text:\r\n%@\r\n.\r\n;\r\n", text]; + [vacation_script appendFormat: @"%@;\r\n", [text asSieveQuotedString]]; // Should we discard incoming mails during vacation? if (discardMails) @@ -1226,7 +1226,7 @@ static NSString *sieveScriptName = @"sogo"; v = [addresses objectAtIndex: i]; if (v && [v length] > 0) { - redirect = [NSString stringWithFormat: @"redirect \"%@\";\r\n", v]; + redirect = [NSString stringWithFormat: @"redirect %@;\r\n", [v asSieveQuotedString]]; if (alwaysSend) [script insertString: redirect atIndex: 0]; else @@ -1265,7 +1265,11 @@ static NSString *sieveScriptName = @"sogo"; addresses = [addresses componentsSeparatedByString: @","]; message = [values objectForKey: @"notificationMessage"]; + if (![message isKindOfClass: [NSString class]]) + message = @""; notificationTranslated = [values objectForKey: @"notificationTranslated"]; + if (![notificationTranslated isKindOfClass: [NSString class]]) + notificationTranslated = @""; for (i = 0; i < [addresses count]; i++) { @@ -1274,8 +1278,10 @@ static NSString *sieveScriptName = @"sogo"; { notify = @"if header :matches \"subject\" \"*\" {\r\n set \"subject\" \"${1}\";\r\n}\r\n"; notify = [notify stringByAppendingString: @"if header :matches \"From\" \"*<*>\" {\r\n set \"from\" \"${1} ${2}\";\r\n}\r\n"]; - notify = [notify stringByAppendingFormat: @"set :encodeurl \"body_param\" \"%@\";\r\n", message]; - notify = [notify stringByAppendingFormat: @"notify :message \"%@: '${subject}' from: ${from}\" \"mailto:%@?body=${body_param}\";\r\n", notificationTranslated, v]; + notify = [notify stringByAppendingFormat: @"set :encodeurl \"body_param\" %@;\r\n", [message asSieveQuotedString]]; + notify = [notify stringByAppendingFormat: @"notify :message %@ %@;\r\n", + [[NSString stringWithFormat: @"%@: '${subject}' from: ${from}", notificationTranslated] asSieveQuotedString], + [[NSString stringWithFormat: @"mailto:%@?body=${body_param}", v] asSieveQuotedString]]; [script appendString: notify]; } diff --git a/UI/PreferencesUI/UIxPreferences.m b/UI/PreferencesUI/UIxPreferences.m index 47c73c0af..096056222 100644 --- a/UI/PreferencesUI/UIxPreferences.m +++ b/UI/PreferencesUI/UIxPreferences.m @@ -1747,6 +1747,76 @@ static NSArray *reminderValues = nil; return YES; } +// +// Checks a list of addresses (array, or comma-separated string as found in +// the Notification preference) against SOGoForwardConstraints. +// +- (BOOL) _addressesMatchForwardConstraints: (id) addresses +{ + NSMutableArray *mails; + id address; + + if ([[[context activeUser] domainDefaults] forwardConstraints] <= 0) + return YES; + + if ([addresses isKindOfClass: [NSString class]]) + addresses = [addresses componentsSeparatedByString: @","]; + + if (![addresses isKindOfClass: [NSArray class]]) + return NO; + + mails = [NSMutableArray array]; + for (address in addresses) + { + if (![address isKindOfClass: [NSString class]]) + return NO; + if ([address length]) + [mails addObject: address]; + } + + return [self doForwardsMatchTheConstraints: mails]; +} + +// +// Checks the arguments of 'redirect' and 'notify' actions of Sieve filters +// against SOGoForwardConstraints, as the web interface does client-side. +// +- (BOOL) _sieveFiltersMatchForwardConstraints: (id) filters +{ + NSMutableArray *mails; + id filter, actions, action, method, argument; + + if ([[[context activeUser] domainDefaults] forwardConstraints] <= 0 + || ![filters isKindOfClass: [NSArray class]]) + return YES; + + mails = [NSMutableArray array]; + for (filter in filters) + { + if (![filter isKindOfClass: [NSDictionary class]]) + continue; + actions = [filter objectForKey: @"actions"]; + if (![actions isKindOfClass: [NSArray class]]) + continue; + for (action in actions) + { + if (![action isKindOfClass: [NSDictionary class]]) + continue; + method = [action objectForKey: @"method"]; + if ([method isKindOfClass: [NSString class]] + && ([method isEqualToString: @"redirect"] || [method isEqualToString: @"notify"])) + { + argument = [action objectForKey: @"argument"]; + if (![argument isKindOfClass: [NSString class]]) + return NO; + [mails addObject: argument]; + } + } + } + + return [self doForwardsMatchTheConstraints: mails]; +} + /** * @api {post} /so/:username/Preferences/save Save user's defaults and settings * @apiVersion 1.0.0 @@ -1780,7 +1850,7 @@ static NSArray *reminderValues = nil; { NSMutableDictionary *sanitizedLabels; NSArray *allKeys, *accounts, *identities, *forwardMails; - NSDictionary *newLabels, *forwardPref; + NSDictionary *newLabels, *forwardPref, *notificationPref; NSString *name; id loginModule; @@ -1833,6 +1903,29 @@ static NSArray *reminderValues = nil; [v removeObjectForKey: @"Forward"]; } + // Notification addresses and filters' redirect/notify actions are + // subject to the same forward constraints. Since the submitted values + // replace all the stored ones, keep the currently stored value when + // the new one is rejected (otherwise all the filters would be lost). + notificationPref = [v objectForKey: @"Notification"]; + if (notificationPref && [notificationPref isKindOfClass: [NSDictionary class]] + && [[notificationPref objectForKey: @"enabled"] boolValue] + && ![self _addressesMatchForwardConstraints: [notificationPref objectForKey: @"notificationAddress"]]) + { + if ([[user userDefaults] notificationOptions]) + [v setObject: [[user userDefaults] notificationOptions] forKey: @"Notification"]; + else + [v removeObjectForKey: @"Notification"]; + } + + if (![self _sieveFiltersMatchForwardConstraints: [v objectForKey: @"SOGoSieveFilters"]]) + { + if ([[user userDefaults] sieveFilters]) + [v setObject: [[user userDefaults] sieveFilters] forKey: @"SOGoSieveFilters"]; + else + [v removeObjectForKey: @"SOGoSieveFilters"]; + } + if ([self userHasMailAccess]) { //