diff --git a/Documentation/SOGoInstallationGuide.asciidoc b/Documentation/SOGoInstallationGuide.asciidoc index 9e582aab8..07fcb475d 100644 --- a/Documentation/SOGoInstallationGuide.asciidoc +++ b/Documentation/SOGoInstallationGuide.asciidoc @@ -2411,6 +2411,26 @@ Defaults to `SOGo` when unset. an additional icon will appear near the logout button in SOGo's web interface. The URL will always be open in a separate page. +|S |SOGoContentSecurityPolicy +|Parameter used to send a Content Security Policy with the pages of +the Web interface. The policy only allows the scripts served by SOGo +and the inline scripts it generates itself, and forbids `eval()`, which +protects against script injection. Possible values are: + +[options="compact"] +* `report-only` - send the policy as `Content-Security-Policy-Report-Only`: + violations are only reported in the browser console +* `enforce` - send the policy as `Content-Security-Policy` + +The files listed in `SOGoUIAdditionalJSFiles` are served by SOGo and +allowed by the policy. However, they will fail if they load further +scripts from another origin, evaluate strings with `eval()` or +`new Function()`, or add inline scripts or event handlers (`onclick=`). +Install any script they depend on under `WebServerResources` and list it +in `SOGoUIAdditionalJSFiles` as well. + +The policy is disabled when unset. + |U |SOGoLoginModule |Parameter used to specify which module to show after login. Possible values are: diff --git a/SoObjects/SOGo/SOGoSystemDefaults.h b/SoObjects/SOGo/SOGoSystemDefaults.h index 713a1773b..7e5de83a2 100644 --- a/SoObjects/SOGo/SOGoSystemDefaults.h +++ b/SoObjects/SOGo/SOGoSystemDefaults.h @@ -81,6 +81,7 @@ static const NSString *kDisableSharingCalendar = @"Calendar"; - (BOOL) uixAdditionalPreferences; - (BOOL) uixDebugEnabled; +- (NSString *) contentSecurityPolicy; - (BOOL) easDebugEnabled; - (BOOL) openIdDebugEnabled; - (BOOL) apiDebugEnabled; diff --git a/SoObjects/SOGo/SOGoSystemDefaults.m b/SoObjects/SOGo/SOGoSystemDefaults.m index 6afbedd7e..2d4d825b1 100644 --- a/SoObjects/SOGo/SOGoSystemDefaults.m +++ b/SoObjects/SOGo/SOGoSystemDefaults.m @@ -644,6 +644,18 @@ _injectConfigurationFromFile (NSMutableDictionary *defaultsDict, return [self boolForKey: @"SOGoUIxDebugEnabled"]; } +/* Returns "report-only", "enforce" or nil when disabled */ +- (NSString *) contentSecurityPolicy +{ + NSString *mode; + + mode = [[self stringForKey: @"SOGoContentSecurityPolicy"] lowercaseString]; + if ([mode isEqualToString: @"report-only"] || [mode isEqualToString: @"enforce"]) + return mode; + + return nil; +} + - (BOOL) easDebugEnabled { return [self boolForKey: @"SOGoEASDebugEnabled"]; diff --git a/UI/Common/UIxPageFrame.m b/UI/Common/UIxPageFrame.m index a5d4a8386..22abcafae 100644 --- a/UI/Common/UIxPageFrame.m +++ b/UI/Common/UIxPageFrame.m @@ -25,6 +25,7 @@ #import /* for locale strings */ #import +#import #import #import @@ -675,6 +676,53 @@ [[ud animationMode] isEqualToString: @"none"]; } +/* Content Security Policy */ + +// +// AngularJS must evaluate expressions without eval() when the policy +// doesn't allow 'unsafe-eval'. +// +- (NSString *) ngCSP +{ + if ([[SOGoSystemDefaults sharedSystemDefaults] contentSecurityPolicy]) + return @"no-unsafe-eval"; + + return nil; +} + +- (NSString *) contentSecurityPolicyHeader +{ + return [NSString stringWithFormat: + @"default-src 'self'; " + @"script-src 'self' 'nonce-%@'; " + @"style-src 'self' 'unsafe-inline'; " + @"img-src * data: blob:; " + @"font-src 'self' data:; " + @"media-src 'self' data: blob:; " + @"frame-src 'self' data: blob:; " + @"worker-src 'self' blob:; " + @"connect-src 'self'; " + @"object-src 'none'; " + @"base-uri 'self'; " + @"frame-ancestors 'self'", + [self cspNonce]]; +} + +- (void) appendToResponse: (WOResponse *) aResponse + inContext: (WOContext *) aContext +{ + NSString *mode; + + [super appendToResponse: aResponse inContext: aContext]; + + mode = [[SOGoSystemDefaults sharedSystemDefaults] contentSecurityPolicy]; + if (mode && ![self hideFrame] && [self cspNonce]) + [aResponse setHeader: [self contentSecurityPolicyHeader] + forKey: ([mode isEqualToString: @"enforce"] + ? @"content-security-policy" + : @"content-security-policy-report-only")]; +} + - (BOOL) isCompatibleBrowser { WEClientCapabilities *cc; diff --git a/UI/SOGoUI/UIxComponent.h b/UI/SOGoUI/UIxComponent.h index dd909f576..b91de933f 100644 --- a/UI/SOGoUI/UIxComponent.h +++ b/UI/SOGoUI/UIxComponent.h @@ -124,6 +124,9 @@ - (NSString *) buildDate; - (BOOL) isUIxDebugEnabled; +/* Content Security Policy */ +- (NSString *) cspNonce; + @end #endif /* __UIxComponent_H_ */ diff --git a/UI/SOGoUI/UIxComponent.m b/UI/SOGoUI/UIxComponent.m index 5cd0c27fc..bf73a4134 100644 --- a/UI/SOGoUI/UIxComponent.m +++ b/UI/SOGoUI/UIxComponent.m @@ -29,8 +29,10 @@ #import #import #import +#import #import +#import #import #import #import @@ -776,6 +778,32 @@ static SoProduct *commonProduct = nil; return [sd uixDebugEnabled]; } +/* Content Security Policy */ + +// +// Nonce allowing the inline scripts of the current response to run. It +// is shared by all the components of the response through the context +// and is nil when no policy is configured, so no attribute is rendered. +// +- (NSString *) cspNonce +{ + NSString *nonce; + + if (![[SOGoSystemDefaults sharedSystemDefaults] contentSecurityPolicy]) + return nil; + + nonce = [context objectForKey: @"SOGoCSPNonce"]; + if (!nonce) + { + nonce = [[NSData generateSaltForLength: 16 withPrintable: NO] + stringByEncodingBase64]; + if (nonce) + [context setObject: nonce forKey: @"SOGoCSPNonce"]; + } + + return nonce; +} + // // Protection against XSRF // diff --git a/UI/Templates/ContactsUI/UIxContactFoldersView.wox b/UI/Templates/ContactsUI/UIxContactFoldersView.wox index 11119e5bb..f1643ee66 100644 --- a/UI/Templates/ContactsUI/UIxContactFoldersView.wox +++ b/UI/Templates/ContactsUI/UIxContactFoldersView.wox @@ -9,7 +9,7 @@ className="UIxPageFrame" title="moduleName" const:jsFiles="vendor/ckeditor/build/ckeditor.js, Common/sgCkeditor.component.js, Common.js, Preferences.services.js, Mailer.services.js, Contacts.js, Contacts.services.js, vendor/angular-file-upload.min.js, vendor/FileSaver.min.js, vendor/punycode.js"> - diff --git a/UI/Templates/MailerUI/UIxMailMainFrame.wox b/UI/Templates/MailerUI/UIxMailMainFrame.wox index 2a20e5eae..c0b0f0687 100644 --- a/UI/Templates/MailerUI/UIxMailMainFrame.wox +++ b/UI/Templates/MailerUI/UIxMailMainFrame.wox @@ -9,7 +9,7 @@ className="UIxPageFrame" title="moduleName" const:jsFiles="vendor/ckeditor/build/ckeditor.js, Common/sgCkeditor.component.js, Common.js, Preferences.services.js, Contacts.services.js, Scheduler.services.js, Mailer.js, Mailer.services.js, vendor/angular-file-upload.min.js, vendor/FileSaver.min.js, vendor/punycode.js, vendor/mark.min.js"> - diff --git a/UI/Templates/UIxPageFrame.wox b/UI/Templates/UIxPageFrame.wox index b5bab3285..afee52b18 100644 --- a/UI/Templates/UIxPageFrame.wox +++ b/UI/Templates/UIxPageFrame.wox @@ -11,7 +11,7 @@ - + <var:string value="title"/> @@ -62,7 +62,7 @@ -