App Suite UI (latest)
All versions
Imprint
All versions
Imprint
  • Feature Catalog
    • General
      • Getting around
      • Search and notifications
      • Settings and appearance
      • Signing in
      • Account and security
      • Onboarding and devices
      • Working with content
      • Sharing and delegation
      • Storage, plans and feedback
      • Progressive Web App
      • Feedback
      • Upsell
      • Triggers
    • Mail
      • Reading and organizing
      • Attachments
      • Writing and sending
      • Security and trust
      • Accounts and automation
      • Signatures and templates
      • Working with the other apps
      • BIMI
    • Calendar
      • Seeing your day
      • Appointments
      • People and invitations
      • Calendars and subscriptions
      • Rooms and resources
      • Video meetings
      • Search, print and transfer
    • Address Book
      • Contacts
      • Address books
      • Finding people
      • Lists and the other apps
    • Tasks
      • Tasks
      • Task lists and delegation
      • Working with the other apps
    • Drive
      • Working with files
      • Finding and arranging
      • Sharing
      • Storages and capacity
      • Working with the other apps
      • OpenCloud Drive Integration
    • Portal
    • Enterprise and Provider Edition
    • Compliance

      • Accessibility
      • Accessibility Conformance Report
      • Data protection
  • Upgrade Guide
    • Everything new since 7.10.6
    • From 7.10.6 to 8.35
    • From 8.35 to 8.47
    • From 8.47 to 8.55
    • Breaking changes and requirements
  • Deployment Guide

    • Configuration
    • Settings list
    • Login page
    • What's New dialog
    • Mail assets
    • Mail rendering and security
    • All messages folder
    • Unseen messages folder
    • Theming
    • Authentication
    • Browser support
  • Customize & Extend
    • Manifests
    • Toolbars and menus
    • Portal widget
    • Sign In
    • Internationalization
  • Architecture
    • Core UI service

Mail rendering and security

HTML email is untrusted content. App Suite UI renders every HTML mail body in the browser, inside a sandboxed iframe under a strict Content-Security-Policy, after sanitizing it — so a message can never run a script, pull in an external stylesheet, or silently phone home. The pipeline is fixed; two parts of it are operator-configurable: allow-listing a brand web font, and the optional external-image privacy proxy.

How an HTML message is rendered

Every HTML mail body passes through the same pipeline before it is shown:

  • Sanitize — DOMPurify removes scripts, event-handler attributes and dangerous tags against the operator's allowlist. No mail markup can execute code.
  • Isolate — the sanitized body is rendered inside a sandboxed iframe under a strict CSP (see below), never in the main document. Even if something slipped past the sanitizer, the CSP is a hard second gate.
  • Block external content by default — remote images are blocked until the user, or a trust rule, allows them. External fonts, media and stylesheets are blocked outright by the CSP and are not user-revealable; they only affect appearance, not content.

The same sanitizing applies wherever mail content is shown or edited: reply and forward quoting, the compose editor, signatures and templates, and the print view.

Why rendering happens in the browser

Historically the middleware pre-processed HTML mail for the detail view: it sanitized the body, normalized the markup, and proxied every external image through the server. Since 8.46 the UI fetches the unmodified mail and owns rendering end to end — sanitizing, external-content control and the Content-Security-Policy all run in the browser. Three reasons drove the move:

  • Correctness — the browser is the best HTML/CSS engine; rendering the original markup avoids a class of server-side normalization bugs.
  • CDNs block centralized proxies — this is inherent to any image proxy, not a flaw in ours: a single server fetching images on behalf of everyone reads as bot traffic, so CDNs (Cloudflare, Akamai, …) block it and images break for whole deployments. A direct browser fetch looks like ordinary user traffic and is not singled out. The proxy is still available as an opt-in privacy feature — see below.
  • One place owns it — the client already sanitized the body for years, with DOMPurify; moving the rest there removes a confusing split of responsibilities between server and client.

This is not "sanitization moved to the client" — that was already client-side. What moved is the external-content blocking, the markup normalization and the image proxy. The other 8.46 changes are listed under Breaking changes and requirements.

The Content-Security-Policy, and the attack it closes

Dropping the server image proxy re-armed an attack the proxy had been masking: an external image whose HTTP response carries a Link: …; rel=preload; as=font (or as=style) header can chain to a 401 WWW-Authenticate response, which pops a native browser credential dialog — with attacker-chosen realm text — over the App Suite UI. The email markup itself is harmless; the trick is entirely in the network response, so more sanitizing would not help.

The fix is a strict Content-Security-Policy on the render frame, which simply refuses the offending sub-resource loads:

default-src 'none'; script-src 'none'; style-src 'unsafe-inline';
img-src data: cid: https:; font-src 'none'; media-src 'none';
form-action 'none'; base-uri 'none'
  • script-src 'none' — no scripts, ever; the hard second gate behind DOMPurify.
  • style-src 'unsafe-inline' with no host — a mail's own inline styles work, external stylesheets do not.
  • img-src … https: — images may load, after consent; their 401s are browser-suppressed, so they cannot pop the dialog.
  • font-src 'none' and media-src 'none' — close the credential-dialog vector for every preload variant.

The policy is hardcoded apart from font-src.

Why the render frame still allows scripts on Safari

The render frame is sandboxed, but on Safari — and on every browser on iOS, all of which are WebKit — it must additionally be granted allow-scripts. This is not to run scripts from the message. WebKit has a long-standing bug (218086) where it otherwise refuses to deliver events such as clicks and keystrokes to the application's own handlers on the frame, even though those handlers are the app's and not the message's. Scripts in a message still never run: the CSP's script-src 'none' is the actual gate. Other browser engines deliver the events without the flag, so it is omitted there — a stricter sandbox, and no browser console warning about the flag combination.

Allowing a brand web font

Because font-src is 'none', an external or corporate web font falls back to a system font, which is purely cosmetic. io.ox/mail//csp/fontSrc is the one overridable directive: an operator who serves a brand font from a trusted host can allow-list it. The setting applies to the compose editor as well, so a signature using that font looks the same while writing a mail as it does when reading one.

The value is validated against a strict allowlist — only specific https hosts, or left-most-label wildcards on a domain you control; bare schemes and over-broad wildcards are rejected, so an allow-list entry cannot re-open the vector. The default, the full rules and examples are documented with the setting itself, under Content Security Policy in the settings list.

External images

What the user sees — the Show images notice, trusting a sender, and when images are blocked regardless — is described under Message security in the Feature Catalog.

Optional: the external-image privacy proxy

Removing the proxy was a security decision, not a privacy one — the CSP closes the attack regardless of how images load. But some deployments want the proxy's privacy side-effect back: fetching images through the server hides the recipient's IP address from the sender. This is available as an opt-in feature.

When enabled, external images load through the middleware proxy instead of directly once the user consents, or for a trusted sender, so the sender sees the proxy's IP rather than the user's.

# also requires the middleware to provide the image-proxy endpoint,
# advertised via the `proxy` capability
io.ox/core//features/proxy: true

The toggle is off by default; see features/proxy in the settings list.

Caveat — the inherent CDN limitation. This re-introduces the very problem that motivated dropping the proxy: CDN-fronted senders often block the proxy's fetch, so some images may not load. The UI handles this gracefully — a failed image shows a placeholder, and a banner reports how many images could not be shown, together with a Load images directly action. Choosing it loads the images without the proxy for that one message: a deliberate, per-message trade-off that reveals the IP address to the sender.

Custom proxy logic

Image resolution runs through the io.ox/mail/externalImages/proxy extension point. The built-in resolver calls the middleware endpoint; a deployment that runs its own image-proxy service can register its own resolver on that point, and disable the default, without touching the rest of the rendering pipeline.

Notes

  • cid: inline images embedded in a message always render — they are part of the mail, not external content.
  • External media (video and audio) is blocked by the CSP; the default operator allowlist drops those tags anyway.
  • Print reuses the same sanitizer. It is a separate document, so there is no render frame and no CSP there, but printed mail is script-free too.
  • Message size limit (processing, not security) — the client caps the size of the body it fetches and renders (io.ox/mail//maxSize/{desktop,mobile}/view, default 3 MB desktop and 1 MB mobile) so that a pathologically large mail cannot hang the view. A truncated mail shows a notice with Show the entire message, which re-fetches up to a hard cap (…/viewLimit, default 10 MB, aligned to the middleware's own bodyDisplaySizeLimit); beyond that it stays truncated and can be downloaded instead. This is a robustness and performance guard, not a security control — see the settings list for the values.
Last Updated: 10/7/26, 2:41 PM
Prev
Mail assets
Next
All messages folder