Security model
Trust boundaries
| Boundary | Who is trusted |
|---|---|
| The Lumen Worker | The operator (via deploy) |
| Prism | Identity provider — fully trusted |
| The configured team | Owners/co-owners can manage app config + addresses |
| Team members | Trusted to receive/send their own mail |
| Senders of inbound mail | Not trusted — content is sanitized |
| Servers behind images | Not trusted — fetches go through proxy |
Authentication
- Sessions live in
httpOnly; Secure; SameSite=Laxcookies backed by KV. - Sessions inherit Prism token expiry; the access token is silently refreshed when within 5 minutes of expiry, provided
offline_accesswas 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/*) requirerole ∈ { 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 flipinit: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/actionURLs must match^(https?:|mailto:|cid:|tel:|#|/).- Inline
styleattributes are dropped (noexpression(), nourl(javascript:...), no smuggled CSS imports). <a>tags gettarget=_blank+rel="noopener noreferrer nofollow".<img>tags getloading=lazy+referrerpolicy=no-referrer, and externalhttp(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.