mirror of
https://github.com/inverse-inc/sogo.git
synced 2026-11-08 02:07:09 +00:00
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.
272 lines
12 KiB
XML
272 lines
12 KiB
XML
<?xml version="1.0" standalone="yes"?>
|
|
<!DOCTYPE var:component>
|
|
<var:component
|
|
xmlns="http://www.w3.org/1999/xhtml"
|
|
xmlns:var="http://www.skyrix.com/od/binding"
|
|
xmlns:const="http://www.skyrix.com/od/constant"
|
|
xmlns:rsrc="OGo:url"
|
|
xmlns:label="OGo:label"
|
|
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">
|
|
<script type="text/javascript" var:nonce="cspNonce">
|
|
var mailAccounts = <var:string value="mailAccounts" const:escapeHTML="NO" />;
|
|
var userNames = <var:string value="userNames" const:escapeHTML="NO" />;
|
|
var unseenCountFolders = <var:string value="unseenCountFolders" const:escapeHTML="NO" />;
|
|
</script>
|
|
|
|
<main class="view"
|
|
layout="row" layout-fill="layout-fill"
|
|
ui-view="mailboxes"
|
|
ng-controller="navController"><!-- mailboxes list --></main>
|
|
|
|
<sg-draggable-helper>
|
|
<md-icon>email</md-icon>
|
|
<sg-draggable-helper-counter class="md-default-theme md-warn md-hue-1 md-bg"><!-- count --></sg-draggable-helper-counter>
|
|
</sg-draggable-helper>
|
|
|
|
<script type="text/ng-template" id="UIxMailMainFrame">
|
|
|
|
<!-- Sidenav -->
|
|
<md-sidenav class="md-sidenav-left md-whiteframe-z1" layout="column" layout-fill="layout-fill"
|
|
md-component-id="left" md-is-locked-open="isGtMedium"
|
|
ng-class="{ 'sg-close': leftIsClose }">
|
|
<var:component className="UIxSidenavToolbarTemplate" />
|
|
<md-content class="md-flex" layout="column" md-scroll-y="md-scroll-y"
|
|
md-colors="::{ backgroundColor: 'default-background-300' }">
|
|
<section class="sg-account-section"
|
|
layout="column"
|
|
ng-repeat="account in ::app.accounts track by account.id"
|
|
ng-class="{ 'md-flex': account.$expanded }">
|
|
<md-list class="md-dense">
|
|
<md-list-item ng-click="app.toggleAccountState(account)">
|
|
<div class="sg-no-wrap"
|
|
aria-label="{{ ::'Toggle visibility' | loc }}"
|
|
ng-bind="::account.name"><!-- account name --></div>
|
|
<md-menu class="md-secondary">
|
|
<md-icon label:aria-label="Options"
|
|
ng-click="$mdMenu.open($event)"
|
|
md-menu-origin="md-menu-origin">more_vert</md-icon>
|
|
<md-menu-content width="3">
|
|
<md-menu-item ng-show="::account.id == 0">
|
|
<md-button
|
|
label:aria-label="Delegation..."
|
|
ng-click="app.delegate(account)">
|
|
<var:string label:value="Delegation..."/>
|
|
</md-button>
|
|
</md-menu-item>
|
|
<md-menu-item ng-show="::app.showSubscribedOnly == 1">
|
|
<md-button
|
|
label:aria-label="Subscribe..."
|
|
ng-click="app.subscribe(account)">
|
|
<var:string label:value="Subscribe..."/>
|
|
</md-button>
|
|
</md-menu-item>
|
|
<md-menu-item>
|
|
<md-button
|
|
label:aria-label="New Folder..."
|
|
ng-click="app.newFolder(account)">
|
|
<var:string label:value="New Folder..."/>
|
|
</md-button>
|
|
</md-menu-item>
|
|
<var:if condition="isCleanMailboxEnabled">
|
|
<md-menu-item>
|
|
<md-button type="button" ng-click="app.showCleanMailboxPanel(null, account)">
|
|
<var:string label:value="Clean mailbox"/>...
|
|
</md-button>
|
|
</md-menu-item>
|
|
</var:if>
|
|
<md-menu-item ng-show="::account.id == 0">
|
|
<md-button
|
|
label:aria-label="Search"
|
|
ng-click="app.showAdvancedSearch()">
|
|
<var:string label:value="Search"/>
|
|
</md-button>
|
|
</md-menu-item>
|
|
</md-menu-content>
|
|
</md-menu>
|
|
</md-list-item>
|
|
</md-list>
|
|
<div class="sg-quota ng-hide" ng-show="::account.$quota">
|
|
<md-progress-linear md-mode="determinate"
|
|
ng-class="{ 'md-warn': account.$quota.percent > 70 }"
|
|
value="{{account.$quota.percent}}"><!-- quota --></md-progress-linear>
|
|
<div class="sg-md-caption md-default-theme md-fg md-primary"
|
|
ng-show="account.$expanded"
|
|
ng-class="{ 'md-warn': account.$quota.percent > 70 }">{{account.$quota.description}}</div>
|
|
</div>
|
|
<md-virtual-repeat-container class="md-flex">
|
|
<md-list class="md-dense">
|
|
<md-list-item
|
|
class="sg-mailbox-list-item md-clickable md-default-theme md-background md-hue-1"
|
|
ng-class="{'md-bg sg-selected': folder.id == app.service.selectedFolder.id}"
|
|
md-virtual-repeat="folder in account" md-item-size="40" md-on-demand="md-on-demand"
|
|
sg-mailbox="folder"
|
|
sg-droppable="app.isDroppableFolder(dragFolder, folder)"
|
|
sg-drop="app.dragSelectedMessages(dragFolder, folder, dragMode)">
|
|
<!-- sgMailboxListItem directive -->
|
|
</md-list-item>
|
|
</md-list>
|
|
</md-virtual-repeat-container>
|
|
<md-divider><!-- divider --></md-divider>
|
|
</section>
|
|
</md-content>
|
|
</md-sidenav>
|
|
|
|
<section layout="column" layout-fill="layout-fill" ui-view="mailbox">
|
|
<md-toolbar layout="row" layout-align="space-between center" class="toolbar-main">
|
|
<var:component className="UIxTopnavToolbar"/>
|
|
</md-toolbar>
|
|
<md-content class="md-flex"
|
|
md-colors="::{backgroundColor: 'default-background-200'}"
|
|
layout="column" layout-align="center center">
|
|
<div class="sg-md-title"
|
|
md-colors="::{color: 'default-background-500'}">
|
|
<var:string label:value="No mailbox selected"/>
|
|
</div>
|
|
</md-content>
|
|
</section>
|
|
</script>
|
|
|
|
<script type="text/ng-template" id="UIxMailFolderTemplate">
|
|
|
|
<md-toolbar layout="row" layout-align="space-between center" class="toolbar-main">
|
|
<var:component className="UIxTopnavToolbar"/>
|
|
</md-toolbar>
|
|
|
|
<div layout="row" class="md-flex sg-block-print">
|
|
<var:component className="UIxMailFolderTemplate" />
|
|
</div>
|
|
</script>
|
|
|
|
<!-- template of contextual menu for a mailbox -->
|
|
<script type="text/ng-template" id="UIxMailFolderMenu">
|
|
<div md-whiteframe="3">
|
|
<md-menu-content width="3">
|
|
<md-menu-item>
|
|
<md-button type="button" md-menu-align-target="md-menu-align-target"
|
|
ng-click="$menuCtrl.markFolderRead()">
|
|
<var:string label:value="Mark Folder Read"/>
|
|
</md-button>
|
|
</md-menu-item>
|
|
<md-menu-item ng-hide="::$menuCtrl.folder.$isNoInferiors">
|
|
<md-button type="button" ng-click="$menuCtrl.newFolder()">
|
|
<var:string label:value="New Subfolder..."/>
|
|
</md-button>
|
|
</md-menu-item>
|
|
<md-menu-item ng-show="::$menuCtrl.folder.$isEditable">
|
|
<md-button type="button" ng-click="$menuCtrl.editFolder($event)">
|
|
<var:string label:value="Rename"/>
|
|
</md-button>
|
|
</md-menu-item>
|
|
<md-menu-item ng-show="::$menuCtrl.folder.$isEditable">
|
|
<md-menu md-position-mode="cascade target">
|
|
<md-button label:aria-label="Move To" ng-click="$mdMenu.open($event)">
|
|
<var:string label:value="Move To"/>
|
|
</md-button>
|
|
<md-menu-content class="md-dense" width="3">
|
|
<div ng-repeat="folder in
|
|
$menuCtrl.folder.$account.$flattenMailboxes({ all: true })
|
|
track by folder.path">
|
|
<md-menu-item>
|
|
<md-button class="sg-no-wrap"
|
|
aria-label="{{folder.$displayName}}"
|
|
ng-disabled="$menuCtrl.isParentOf(folder.path)"
|
|
ng-click="$menuCtrl.moveFolder(folder.path)">
|
|
<span ng-class="::('sg-child-level-' + folder.level)"
|
|
ng-bind="folder.$displayName"><!-- mailbox name --></span>
|
|
</md-button>
|
|
</md-menu-item>
|
|
</div>
|
|
</md-menu-content>
|
|
</md-menu>
|
|
</md-menu-item>
|
|
<md-menu-item>
|
|
<md-button type="button" ng-click="$menuCtrl.compactFolder()">
|
|
<var:string label:value="Compact"/>
|
|
</md-button>
|
|
</md-menu-item>
|
|
<var:if condition="isCleanMailboxEnabled">
|
|
<md-menu-item ng-show="::($menuCtrl.folder.type != 'trash')">
|
|
<md-button type="button" ng-click="$menuCtrl.cleanMailbox()">
|
|
<var:string label:value="Clean folder"/>...
|
|
</md-button>
|
|
</md-menu-item>
|
|
</var:if>
|
|
<md-menu-item ng-show="::$menuCtrl.folder.$isEditable">
|
|
<md-button type="button" ng-click="$menuCtrl.confirmDelete()">
|
|
<var:string label:value="Delete"/>
|
|
</md-button>
|
|
</md-menu-item>
|
|
<md-menu-item ng-show="::($menuCtrl.folder.type == 'trash')">
|
|
<md-button type="button" ng-click="$menuCtrl.emptyTrashFolder()">
|
|
<var:string label:value="Empty Trash"/>
|
|
</md-button>
|
|
</md-menu-item>
|
|
<md-menu-item ng-show="::($menuCtrl.folder.type == 'junk')">
|
|
<md-button type="button" ng-click="$menuCtrl.emptyJunkFolder()">
|
|
<var:string label:value="Empty Junk Folder"/>
|
|
</md-button>
|
|
</md-menu-item>
|
|
<var:if condition="isMailExportEnabled">
|
|
<md-menu-item>
|
|
<md-button type="button" ng-click="$menuCtrl.folder.exportFolder()">
|
|
<var:string label:value="Export"/>
|
|
</md-button>
|
|
</md-menu-item>
|
|
</var:if>
|
|
<md-menu-item>
|
|
<md-button type="button" ng-click="$menuCtrl.showAdvancedSearch()">
|
|
<var:string label:value="Search"/>
|
|
</md-button>
|
|
</md-menu-item>
|
|
<md-divider ng-show="::$menuCtrl.folder.$canFolderAs()"><!-- divider --></md-divider>
|
|
<md-menu-item ng-show="::$menuCtrl.folder.$canFolderAs()">
|
|
<md-button type="button" ng-click="$menuCtrl.setFolderAs('Drafts')">
|
|
<var:string label:value="Set as Drafts"/>
|
|
</md-button>
|
|
</md-menu-item>
|
|
<md-menu-item ng-show="::$menuCtrl.folder.$canFolderAs()">
|
|
<md-button type="button" ng-click="$menuCtrl.setFolderAs('Sent')">
|
|
<var:string label:value="Set as Sent"/>
|
|
</md-button>
|
|
</md-menu-item>
|
|
<md-menu-item ng-show="::$menuCtrl.folder.$canFolderAs()">
|
|
<md-button type="button" ng-click="$menuCtrl.setFolderAs('Trash')">
|
|
<var:string label:value="Set as Trash"/>
|
|
</md-button>
|
|
</md-menu-item>
|
|
<md-menu-item ng-show="::$menuCtrl.folder.$canFolderAs()">
|
|
<md-button type="button" ng-click="$menuCtrl.setFolderAs('Junk')">
|
|
<var:string label:value="Set as Junk"/>
|
|
</md-button>
|
|
</md-menu-item>
|
|
<md-menu-item ng-show="::$menuCtrl.folder.$canFolderAs()">
|
|
<md-button type="button" ng-click="$menuCtrl.setFolderAs('Templates')">
|
|
<var:string label:value="Set as Templates"/>
|
|
</md-button>
|
|
</md-menu-item>
|
|
<var:if condition="isMailSharingEnabled">
|
|
<md-divider ng-show="::($menuCtrl.folder.type != 'additional')"><!-- divider --></md-divider>
|
|
<md-menu-item ng-show="::($menuCtrl.folder.type != 'additional')">
|
|
<md-button type="button" ng-click="$menuCtrl.share()">
|
|
<var:string label:value="Sharing..."/>
|
|
</md-button>
|
|
</md-menu-item>
|
|
</var:if>
|
|
</md-menu-content>
|
|
</div>
|
|
</script>
|
|
|
|
<!-- template of modal for mailbox sharing options -->
|
|
<script type="text/ng-template" id="UIxUserRightsEditor">
|
|
<var:component className="UIxMailUserRightsEditor" />
|
|
</script>
|
|
|
|
<!-- template of mail viewer -->
|
|
<script type="text/ng-template" id="UIxMailViewTemplate">
|
|
<var:component className="UIxMailViewTemplate" />
|
|
</script>
|
|
|
|
</var:component>
|