Local Capability Surfaces Need Defense in Depth
Local capability surfaces need defense in depth
Local is not the same thing as harmless. A local daemon usually has more authority than a normal app window: it can read a database, mutate state, open files, talk to providers, and sometimes run user-configured hooks. Once a client can reach that daemon, it is not "just localhost" anymore. It is a capability surface.
The mxr security pass made this concrete. The system was already local-first and basically sound, but several edges were relying on friendly defaults: Unix socket permissions inherited from umask, OpenAPI docs exposed without bridge auth, attachment destinations accepted from IPC, and remote image reads used response.bytes() without a hard cap.
None of those were dramatic by themselves. Together they were a smell: too much trust was implicit.
The pattern
Treat every local entrypoint as if a confused or malicious local client might reach it:
- Unix socket: put it in a user-owned directory and set the socket file owner-only (
0600is enough for a stream socket; directory0700matters too). - Loopback HTTP bridge: require bearer auth, check Host, restrict CORS, add security headers, and hide loopback-only helper endpoints with
404when the peer is wrong. - API docs: do not special-case them as public just because they contain no user data. The route list is still a map of authority.
- Filesystem writes: canonicalize or normalize destinations, reject
.., reject symlink targets, and require the final path to live under an allowed root. - Remote fetches: cap the body even when
Content-Lengthlies or is absent.
The point is not paranoia. The point is that local tools age. They gain a web UI, a CLI, an agent skill, a script ecosystem, then somebody binds a bridge through a tunnel. Explicit boundaries survive that growth better than "it only runs on my machine."
What changed in mxr
Validated against code on 2026-05-28:
crates/daemon/src/server.rssets the daemon socket to0600afterUnixListener::bind.crates/web/src/middleware.rsinjectsX-Frame-Options,X-Content-Type-Options, andReferrer-Policy.crates/web/src/router.rsauth-gates/api/v1/docsand/api/v1/openapi.json.crates/daemon/src/handler/mod.rsconstrains attachment downloads to the configured downloads directory, current directory, or system temp directory.crates/daemon/src/handler/mailbox.rscaps remote HTML asset bodies at 25 MB.
Why this generalizes
Any local-first app with a daemon has the same shape: sensitive local state, multiple clients, and a bridge somewhere. Mxr is email, Lazydap is debugging, a future memory daemon would be personal knowledge. The domain changes. The capability-surface problem does not.