Files
sogo/UI/Templates/UIxPageFrame.wox
T
Jordi Mallach a863283680 feat(web): add optional Content Security Policy
AngularJS (EOL since 2022-01) and AngularJS Material no longer receive
security fixes, so any new vulnerability in them will stay unpatched.
In AngularJS, templates are code: any attacker-controlled text that
ends up in a compiled template is evaluated as an expression, and
since 1.6 there is no expression sandbox, so a single
{{constructor.constructor('...')()}} gives full script execution.
SOGo renders untrusted data (mail bodies, invitations, attachment
names, contact fields) into such templates and currently relies on
server-side filtering, like stripping "{{" and "}}", to prevent this.
Any gap in that filtering, or a bypass of $sanitize/ng-bind-html,
would mean XSS in the webmail session: reading and sending mail as
the user, and access to calendars and contacts.

A Content Security Policy adds a second layer in the browser that
doesn't depend on catching every injection. The new
SOGoContentSecurityPolicy system default ("report-only" or "enforce",
disabled when unset) sends a strict policy with the pages of the web
interface:

  script-src 'self' 'nonce-<random>'   (no 'unsafe-inline'/'unsafe-eval')
  style-src 'self' 'unsafe-inline'     (Angular Material, CKEditor 5)
  object-src 'none'; base-uri 'self'; frame-ancestors 'self'; ...

The policy is generated by sogod, since a fresh 128-bit nonce is needed
per response: it is shared by all components of the response and set
on the inline configuration scripts of the page frame and module views.
The page also gets ng-csp="no-unsafe-eval", so AngularJS evaluates
expressions with its interpreter instead of Function().

With the policy enforced, an expression injected into a compiled
template, such as {{constructor.constructor('...')()}}, can no longer
reach Function() and fails with an EvalError, and injected inline
scripts are blocked. Injected expressions are limited to calling
functions available on the scope.

Tested with Chromium in both modes on login, Mail (HTML messages,
remote content, invitations, compose), Calendar and Contacts editors,
Preferences and Administration: no violations from SOGo's own code.
With the default unset, the generated output is unchanged.

form-action is deliberately not restricted, so SAML2/CAS/OpenID
logins posting to an external IdP keep working. Scripts listed in
SOGoUIAdditionalJSFiles are served from WebServerResources and allowed,
but must not load scripts from other origins, use eval() or rely on
inline handlers; their dependencies have to be installed and listed
alongside them.
2026-11-06 16:22:03 +01:00

175 lines
8.3 KiB
XML

<?xml version="1.0" standalone="yes"?>
<!DOCTYPE container>
<container
xmlns="http://www.w3.org/1999/xhtml"
xmlns:xml="http://www.w3.org/XML/1998/namespace"
xmlns:var="http://www.skyrix.com/od/binding"
xmlns:const="http://www.skyrix.com/od/constant"
xmlns:rsrc="OGo:url"
xmlns:label="OGo:label"
xmlns:bool="OGo:bool">
<var:string var:value="doctype" const:escapeHTML="NO" />
<var:if condition="hideFrame" const:negate="YES">
<html const:xmlns="http://www.w3.org/1999/xhtml" var:lang="localeCode" var:ng-csp="ngCSP">
<head>
<title var:sg-default="title"><var:string value="title"/></title>
<meta name="hideFrame" var:content="hideFrame" />
<meta name="description" content="SOGo Web Interface" />
<meta name="author" content="Inverse inc." />
<meta name="robots" content="noindex, nofollow" />
<meta name="build" var:content="buildDate" />
<meta name="viewport" content="width=device-width, initial-scale=1,maximum-scale=1" />
<link href="mailto:support@sogo.nu" rev="made" />
<link rel="shortcut icon" var:href="siteFavicon" type="image/x-icon" />
<var:if condition="isUIxDebugEnabled" const:negate="YES">
<link type="text/css" rel="stylesheet" rsrc:href="css/theme-default.css" />
</var:if>
<link type="text/css" rel="stylesheet" rsrc:href="css/styles.css" />
<var:if condition="hasPageSpecificCSS">
<link type="text/css" rel="stylesheet" var:href="pageCSSURL"/>
</var:if>
<var:foreach list="additionalCSSFiles" item="item">
<link type="text/css" rel="stylesheet" var:href="item"/>
</var:foreach>
<var:if-ie>
<link type="text/css" rel="stylesheet" rsrc:href="css/no-animation.css"/>
</var:if-ie>
</head>
<body var:class="bodyClasses" var:ng-app="angularModule" bool:md-no-ink="disableInk">
<noscript>
<div layout="column" layout-align="center center" layout-fill="layout-fill">
<div class="md-padding">
<var:string label:value="noJavascriptError"/>
</div>
<div>
<a class="md-button" var:href="context.applicationURL">
<var:string label:value="noJavascriptRetry"/>
</a>
</div>
</div>
</noscript>
<var:if condition="isCompatibleBrowser">
<var:if condition="singleWindowModeEnabled">
<div id="popupFrame" style="display: none;">
<iframe width="100%" height="100%" src="/SOGo/loading"><!-- space --></iframe>
</div>
</var:if>
<!-- MAIN CONTENT ROW -->
<var:component-content />
<script type="text/javascript" var:nonce="cspNonce">
var ApplicationBaseURL = '<var:string value="modulePath" />';
var ResourcesURL = '<var:string value="applicationPath" />.woa/WebServerResources';
var minimumSearchLength = <var:string value="minimumSearchLength" />;
var minimumSearchLengthLabel = '<var:string value="minimumSearchLengthLabel" />';
<var:if condition="isUIxDebugEnabled">
var DebugEnabled = true;
</var:if>
<var:if condition="isUIxDebugEnabled" const:negate="YES">
var DebugEnabled = false;
</var:if>
<var:if condition="isSuperUser">
var IsSuperUser = true;
</var:if>
<var:if condition="isSuperUser" const:negate="YES">
var IsSuperUser = false;
</var:if>
<var:if condition="usesCASAuthentication">
var usesCASAuthentication = true;
</var:if>
<var:if condition="usesCASAuthentication" const:negate="YES">
var usesCASAuthentication = false;
</var:if>
<var:if condition="usesSAML2Authentication">
var usesSAML2Authentication = true;
</var:if>
<var:if condition="usesSAML2Authentication" const:negate="YES">
var usesSAML2Authentication = false;
</var:if>
<var:if condition="shortUserNameForDisplay" const:value="anonymous" const:negate="YES">
var UserFolderURL = '<var:string value="userFolderPath" const:escapeHTML="NO" />';
var UserLogin = '<var:string value="shortUserNameForDisplay" const:escapeHTML="NO" />';
var UserIdentification = <var:string value="userIdentification.doubleQuotedString" const:escapeHTML="NO" />;
var UserEmail = '<var:string value="userEmail" const:escapeHTML="NO" />';
var UserLanguage = '<var:string value="userLanguage" const:escapeHTML="NO" />';
// Application paths
var UserCalendarPath = '';
<var:if condition="userHasCalendarAccess">
UserCalendarPath = '<var:string value="relativeCalendarPath" const:escapeHTML="NO" />';
</var:if>
var UserContactsPath = '<var:string value="relativeContactsPath" const:escapeHTML="NO" />';
var UserMailPath = '';
<var:if condition="userHasMailAccess">
UserMailPath = '<var:string value="relativeMailPath" const:escapeHTML="NO" />';
</var:if>
var UserPreferencesPath = '<var:string value="relativePreferencesPath" const:escapeHTML="NO" />';
<var:if condition="isSuperUser">
var UserAdministrationPath = '<var:string value="relativeAdministrationPath" const:escapeHTML="NO" />';
</var:if>
var UserLogoffPath = '';
<var:if condition="canLogoff">
UserLogoffPath = '<var:string value="logoffPath" const:escapeHTML="NO" />';
</var:if>
var HelpURL = '<var:string value="helpURL" const:escapeHTML="NO" />';
</var:if>
// This is the equivalent of an AJAX call to /SOGo/so/_UserLogin_/date
var currentDay = <var:string value="currentDayDescription" const:escapeHTML="NO"/>;
<var:string value="commonLocalizableStrings" const:escapeHTML="NO" />
<var:string value="productLocalizableStrings" const:escapeHTML="NO" />
</script>
<var:if condition="shortUserNameForDisplay" const:value="anonymous" const:negate="YES">
<var:component className="UIxJSONPreferences"/>
</var:if>
<!-- JAVASCRIPT IMPORTS -->
<script type="text/javascript" rsrc:src="js/vendor/lodash.min.js"><!-- space --></script>
<script type="text/javascript" rsrc:src="js/vendor/angular.min.js"><!-- space --></script>
<script type="text/javascript" rsrc:src="js/vendor/angular-animate.min.js"><!-- space --></script>
<script type="text/javascript" rsrc:src="js/vendor/angular-sanitize.min.js"><!-- space --></script>
<script type="text/javascript" rsrc:src="js/vendor/angular-aria.min.js"><!-- space --></script>
<script type="text/javascript" rsrc:src="js/vendor/angular-cookies.min.js"><!-- space --></script>
<script type="text/javascript" rsrc:src="js/vendor/angular-messages.min.js"><!-- space --></script>
<script type="text/javascript" rsrc:src="js/vendor/angular-material.min.js"><!-- space --></script>
<script type="text/javascript" rsrc:src="js/vendor/angular-ui-router.min.js"><!-- space --></script>
<var:if condition="hasProductSpecificJavaScript">
<script type="text/javascript" var:src="productJavaScriptURL"><!-- space --></script>
</var:if>
<var:if condition="hasPageSpecificJavaScript">
<script type="text/javascript" var:src="pageJavaScriptURL"><!-- space --></script>
</var:if>
<var:foreach list="additionalJSFiles" item="item">
<script type="text/javascript" var:src="item"><!-- space --></script>
</var:foreach>
<var:foreach list="systemAdditionalJSFiles" item="item">
<script type="text/javascript" var:src="item"><!-- space --></script>
</var:foreach>
</var:if>
<var:if condition="isCompatibleBrowser" const:negate="YES">
<var:component className="SOGoBrowsersPanel"/>
</var:if>
</body>
</html>
</var:if>
<var:if condition="hideFrame">
<var:component-content />
</var:if>
</container>