Skip to content

Security model

Trust boundaries

BoundaryWho is trusted
The Lumen WorkerThe operator (via deploy)
PrismIdentity provider — fully trusted
The configured teamOwners/co-owners can manage app config + addresses
Team membersTrusted to receive/send their own mail
Senders of inbound mailNot trusted — content is sanitized
Servers behind imagesNot trusted — fetches go through proxy

Authentication

  • Sessions live in httpOnly; Secure; SameSite=Lax cookies backed by KV.
  • Sessions inherit Prism token expiry; the access token is silently refreshed when within 5 minutes of expiry, provided offline_access was granted.
  • The user's role on the configured team is read fresh from Prism on every login. Owners promoting/demoting members in Prism takes effect the next time the affected member signs in.

Authorization

  • All mailbox endpoints scope by user_id = session.userId.
  • Admin endpoints (/api/admin/*) require role ∈ { owner, co-owner }.
  • The init flow (POST /api/init/setup) is open ONLY before the first configuration is staged; once staged, only an OAuth callback by a team owner can flip init:configured.
  • WebSocket upgrade rejects cross-origin requests and re-checks the session cookie.

Inbound HTML email

Every HTML body is sanitized at ingest:

  • <script>, <style>, <iframe>, <object>, <embed>, <form>, <svg>, <math>, <base>, <meta>, <link> are dropped (tag and contents).
  • Every on* event handler attribute is dropped.
  • href/src/action URLs must match ^(https?:|mailto:|cid:|tel:|#|/).
  • Inline style attributes are dropped (no expression(), no url(javascript:...), no smuggled CSS imports).
  • <a> tags get target=_blank + rel="noopener noreferrer nofollow".
  • <img> tags get loading=lazy + referrerpolicy=no-referrer, and external http(s) URLs are rewritten through the image proxy.

The sanitized HTML is then rendered in the browser inside a sandboxed <iframe sandbox="allow-popups allow-popups-to-escape-sandbox" srcdoc=…> with a strict Content-Security-Policy meta tag. JS doesn't run, plugins don't load, and the document treats itself as a unique origin (no parent cookie access).

Outbound HTML

The composer is plain text today. The body the user types is wrapped into a multipart/alternative (text + sanitized HTML), built with a builder that strips CR/LF from header values, validates Message-ID, validates attachment filenames + MIME types, and rejects malformed addresses.

Image proxy

See Storage for the full SSRF defense list.

Rate / payload limits

  • Bulk patch endpoints cap at 500 ids per request.
  • Image proxy caps at 10 MB / 8 s per fetch, ≤ 3 redirects.
  • Per-user storage cap (admin-configurable) blocks runaway costs from inbound senders or runaway compose loops.
  • Composer cap (browser-side) stops clearly-too-large attachments before upload.

What's NOT defended

  • Spam. Use Cloudflare Email Routing rules + your own Spamhaus/SPF policy at the edge.
  • Phishing. Lumen displays mail as-is; it doesn't verify SPF/DKIM/DMARC results from headers. (Cloudflare Email Routing already filters obvious forgeries before delivery, but you should treat anything in your inbox as potentially hostile.)
  • Sender reputation / DKIM signing. Outgoing mail uses Cloudflare's Email Sending. The shared sender reputation depends on Cloudflare's configuration, not Lumen's.

Reporting an issue

Open a GitHub issue under siiway/lumen with [security] in the title, or email the operator listed on the project's README.