Uh oh!
There was an error while loading. Please reload this page.
Make local dev-mailbox delivery opt-in and stop advertising /dev/mailbox - #697
Conversation
timujinne
commented
Aug 10, 2026
Code review — GLM-5.2 (elixir-review persona), verified against the branchI have verified every claim against the actual code. Here is my review. SummaryThe core security fix is correct and complete for the intended threat model. I verified the key claims:
No correctness or security hole in the main flow. The findings below are all minor / test-hygiene / edge-case. MINOR — Test isolation: |
timujinne
commented
Aug 10, 2026
Code review, round 2 — GLM-5.2 (elixir-review persona), cascaded self-verification passI've now verified every claim against the actual code. Let me do my final cascade pass by confirming the two remaining structural points: that I confirmed:
Here is my consolidated review. Review — PR #697 (dev-mailbox opt-in, issue #687)SummaryThe PR makes local-mailbox (
I found no critical, major, or minor behavioral defect. The findings below are a documentation gap and two low-severity observations. FindingsMINOR — |
timujinne
commented
Aug 10, 2026
The round-2 actionable item landed: |
POST /api/upload took the file owner from params["user_uuid"] with no check when unauthenticated — an anonymous client could attribute a 100 MB upload and its variant-processing job to any account. GET /api/files/:uuid/info handed out freshly-signed capability URLs for any file uuid to anyone, and its 200/404 split was a file-existence oracle. Both live in the [:browser, :phoenix_kit_auto_setup] scope that fetches but never requires a user. Upload now authorizes before touching the body: unauthenticated -> 401, the user_uuid override honored only for an admin, otherwise attributed to the uploader; the action is rate-limited per account (30/min). File-info requires auth and scopes to the owner (or admin), returning an identical :not_found for a foreign or missing file so it is no longer an oracle.
timujinne
commented
Aug 10, 2026
Added a second core security fix to this branch (
Fixes:
Tests: Deliberately not in this PR (need their own design, not a rushed graft): widening the 16-bit |
timujinne
commented
Aug 10, 2026
Final pre-publication review — Kimi K3 (elixir-review persona), verified against |
…ner/Admin - Rate-limit POST /api/upload on the authenticated uploader, not the resolved owner: an admin override attributed the limit to the victim uuid, giving the uploader a fresh window per victim. - Gate the upload user_uuid override and the file-info cross-user read on Scope.system_role?/1 (Owner/Admin) instead of can_access_admin_area?/1, which is also true for any holder of a single module permission. Adds tests proving a permission-holder-but-not-system-role user is refused both. - Update the store_file/2 signing-contract doc: /api/files/:uuid/info is no longer an anonymous signed-URL handout. - Move the resolved_send_path/0 + mailer_local?/0 tests out of the DataCase module into a plain ExUnit.Case so the headline BeamLabEU#687 fix keeps executable coverage on a database-less run. - Note the suppressed dev-mailbox path short-circuits check_recipient_allowed/1, and correct the upload comment (Plug has already spooled the temp file).
timujinne
commented
Aug 10, 2026
Applied the review's actionable items in
CHANGELOG (LOW) is maintainer-owned in this repo, so it's left for the release cut — the two consumer-visible breaks (dev Local installs stop filling /dev/mailbox; the two storage endpoints 401 anonymous callers) belong in the 2.0.0 entry.
|
… findings Post-merge review of #697. The mailbox gate is well built — it sits before the tracking pipeline, the suppressed return breaks none of the three deliver_email/2 callers (checked each), and production never evaluates the gate because `and` short-circuits on a non-Local adapter. Two findings, neither in the mailbox work itself. `dev_mailbox_enabled?/0` reads a setting with no rescue, in the delivery path, while the sibling `Config.mailer_local?/0` added by the same PR is guarded. AGENTS.md gives the rule and the reason `rescue` alone is insufficient: settings reads are ETS-cached, so only a cache MISS touches the database, and an unreachable one raises on an unowned checkout but EXITS on a dead pool. This read is new to the path, so before it nothing here could fail that way — an auth flow could die inside deliver_email/2 on the one code path whose job is to not lose the token. Guarded, failing closed: suppress and log the token rather than hand single-use links to an unauthenticated mailbox because a pool blipped. The dialyzer-ignore entry said the storage controllers pass their scope to `can_access_admin_area?/1`. They pass it to `system_role?/1`, and both controllers' docs stress the difference in capitals — can_access_admin_area?/1 is true for any holder of one module permission, precisely the actor that must not read every other user's file metadata and signed URLs. That file is where someone auditing suppressed warnings looks; it should not be the one place reading as though the weaker check is in use. Also flagged, not a defect: the PR body describes four changes and the merge carries six. The two undescribed commits fix the unauthenticated POST /api/upload and GET /api/files/:uuid/info — the storage item at the top of CLAUDE.md's TODO list, and a bigger security fix than the mailbox work the PR is named for. Release notes written from the description would miss it, as would the second behaviour change hosts need: file-info now requires auth and owner-or-staff, and it is documented host API. Reviewed in an isolated worktree at origin/main: the main tree has another agent's uncommitted changes to rate_limiter.ex, which this PR also touches. compile --warnings-as-errors, format, credo --strict, dialyzer all clean; the PR's two unit test files pass (6 tests). Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Tim's call: the release ships as 2.0.0, with the feature-module pins widened the
same day so the ecosystem lands with it. That removes the cost I had weighted
most heavily when I bumped to 1.7.237 — the unsatisfiable-dependency window is
coordinated away rather than endured — and it restores what the patch route
could not offer: `{:phoenix_kit, "~> 1.7"}` does not resolve to 2.0, so no
below-floor host is dragged across the floor by a routine `mix deps.update`.
That last point is why one paragraph had to be rewritten rather than kept. The
upgrade-requirement section warned that a routine deps.update WOULD carry a
below-floor host across; true of 1.7.237, false of 2.0.0. It now explains that
the major is precisely what prevents it, and carries the other half hosts need:
`~> 2.0` is unsatisfiable alongside any phoenix_kit_* package still pinning
`~> 1.7.x`, so modules must be upgraded together with core, not after it.
CHANGELOG now covers #689 through #697. The #695/#696 entries existed only in
the main working tree, which is dirty with another agent's in-flight work, so
they are re-created here rather than committed from there. #697's entries name
the two behaviour changes hosts will actually notice — the dev mailbox going
quiet by default, and /api/files/:uuid/info now requiring auth — and the
unauthenticated upload fix its own PR description never mentioned.
Written in an isolated worktree at origin/main so the other agent's uncommitted
CHANGELOG and lib/ changes are untouched; their entries stay in their tree and
will merge as ordinary changelog text.
Release gate at v2.0.0: CHANGELOG heading and body PASS, tag collision PASS.
Only the stale chain_hash remains, which that same agent is fixing.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>…6 manifest Not my work — another agent's changes, uncommitted in the working tree while I was reviewing #695/#696/#697. Committing at the maintainer's request so nothing is stranded. Verified it compiles with warnings-as-errors before committing. Three pieces, all closing findings from my earlier review passes: - Mentions.Users.search/2 no longer calls Scope.for_user/1 per candidate; the admin-area rule became two SQL EXISTS subqueries with the limit applied in the database. This is the ~128-queries-per-keystroke item I reported in the #692 review and deliberately did not fix, because rewriting an authorization filter as hand-written SQL without a database to test against was not a change to make blind. It has a database now. - AccessRequests.request/4 validates the resource type against ResourceLinks.handlers/0, requires a castable uuid, and rate-limits per account — the unvalidated/unthrottled path I filed as a nitpick on the same PR. - ExpectedSchema hand-declares V165/V166 objects and restamps chain_hash, which is the release blocker. Their choice of the hand-declare route matches what the #692 review concluded was necessary: no generator run can emit V164+ objects, because regeneration replays a pre-squash checkout whose chain ends at V163. CHANGELOG.md is deliberately NOT in this commit. The copy in this tree predates the 2.0.0 retitle and the #695-#697 entries now on main, so committing it would have reverted them. Their three entries were extracted verbatim first and are re-applied on top of the current file in the next commit. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Addresses the actionable half of #687: items 1 and 4 as filed, plus the design decision the issue left open — instead of an install-time option, local-mailbox delivery becomes a runtime opt-in, which also covers the delegation-mode hosts the installer can never reach.
The four changes
Config.mailer_local?/0resolves the waydeliver_email/2actually sends (item 1). NewPhoenixKit.Mailer.resolved_send_path/0— default send integration → delegated host mailer (adapter read from the parent app's env) → built-in — andmailer_local?/0derives from it. Fixes the delegation-mode false negative from the issue and the false positive @ddon added (installer-written Local block + delegating real mailer). Guarded withrescue/catch :exit: it renders on public pages, a dead pool meansfalse, not a crash.Local mailbox is opt-in. New setting
dev_mailbox_enabled(default"false"). When the resolved adapter isSwoosh.Adapters.Localand the switch is off,deliver_email/2does not hand the message to the mailbox: the recipient, subject and body (where the one-time links live; falls back tohtml_bodyfor HTML-only mail) go to the server log with a one-line pointer at the switch, and the call returns{:ok, %{suppressed: true, ...}}so register/reset flows stay green. The gate sits before the tracking pipeline — a message never handed to an adapter is not recorded as sent. The integration path is untouched (no Local adapter exists among providers).Toggle + warning on the existing
/admin/settings/email-sendingpage. The section renders only when the resolved path is Local; switching the mailbox on shows the consequence spelled out ("tokens readable by anyone who can reach this server").DevNotice stops advertising the inbox (item 4, the "drop the anchor" option). Gate closed → "outgoing email is written to the server log"; gate open → the old copy without the
/dev/mailboxlink. Its render condition (mailer_local?/0) is now truthful, which fixes the backwards visibility ddon described — the notice no longer shows only on the installs that were configured the ordinary way while staying silent on the actually-blind delegation hosts.The cost, named
An upgraded dev install stops filling the mailbox until the operator flips the switch. Every suppressed send says exactly that in the log, the DevNotice copy changes visibly, and the admin page carries the banner — the failure mode is loud, and the tokens stay recoverable from the log.
Items 2 (router introspection in
install/doctor) and 3 stay open: 2 as the agreed follow-up, 3 superseded — the safe default no longer depends on what the installer asks.Evidence
chain_hashmanifest items (see the Close the remaining review findings from the #694 round #695 review), untouched by this branch.mix precommitclean (compile with warnings-as-errors, format, credo --strict, dialyzer).get_parent_app/0doc note) are all fixed in the last commit.