Skip to content

fix(blocklist): require admin for global block rule management - #234

Open
Waiel5 wants to merge 3 commits into
choyiny:mainfrom
Waiel5:fix/blocklist-admin-guard
Open

fix(blocklist): require admin for global block rule management#234
Waiel5 wants to merge 3 commits into
choyiny:mainfrom
Waiel5:fix/blocklist-admin-guard

Conversation

@Waiel5

@Waiel5 Waiel5 commented Aug 5, 2026

Copy link
Copy Markdown
Contributor

Summary

/api/blocklist is mounted alongside the ordinary member routes, without the requireAdmin guard that /api/suppressions, /api/webhook and /api/oauth-apps each carry. The blocklist is global instance security state in exactly the way those are, so this reads as an omission rather than a decision: any authenticated member can list, add and remove block rules for the whole deployment.

The sharpest edge is DELETE /api/blocklist/mail, which permanently deletes every currently-hidden message from blocked senders — database rows and the R2 objects behind them — and applies no inbox scoping at all. A member can therefore destroy mail belonging to inboxes they hold no permission to read, with no undo.

Changes

  • Guard /api/blocklist and /api/blocklist/* with requireAdmin, using the same two-line app.use pair the neighbouring admin routers use, so it applies to every authentication method rather than only one.
  • Moved the app.route down next to the other admin-guarded routers so the grouping matches the policy.

Release impact

  • Patch — bug fix or internal change, no new behaviour

  • Bug Fix

Test plan

  • yarn test worker/src/__tests__/blocklist-router.test.ts + blocklist-lib + purge-blocked — 18 passed
  • Verified the new tests fail against the previous mounting (4 failures)
  • prettier --check clean

Notes for reviewers

This is a behaviour change for any non-admin currently using these endpoints, so it is worth a conscious call rather than a rubber stamp — but given the purge route deletes mail deployment-wide, admin-only looks like the intended policy.

Same blind spot as the attachments PR: every existing test here authenticates as an admin, because createTestUser defaults to role: "admin", so an unguarded router passed the suite. The added cases run as a member across all four routes.

Checklist

  • Added or updated a migration (yarn db:generate) if the schema changed — n/a
  • Updated CHANGELOG.md under ## [Unreleased]
  • Updated docs — n/a

`/api/blocklist` was mounted alongside the ordinary member routes, without
the `requireAdmin` guard that `/api/suppressions`, `/api/webhook` and
`/api/oauth-apps` each carry. The blocklist is global instance security state
in exactly the way those are, so this reads as an omission rather than a
decision: any authenticated member could list, add and remove block rules for
the whole deployment.

The sharpest edge is `DELETE /api/blocklist/mail`, which permanently deletes
every currently-hidden message from blocked senders — database rows and the
R2 objects behind them — and applies no inbox scoping at all. A member could
therefore destroy mail belonging to inboxes they hold no permission to read,
with no undo.

Guarded with the same two-line `app.use` pair the neighbouring admin routers
use, so it applies to every authentication method rather than only one.

The existing tests in this file all authenticate as an admin, because
createTestUser defaults to role "admin" — which is why an unguarded router
passed a full suite. The added cases run as a member across all four routes
and fail against the previous mounting.
@Waiel5

Waiel5 commented Aug 5, 2026

Copy link
Copy Markdown
Contributor Author

Note on the red check: it is the major/minor/patch label gate, not the test suite.

Exactly one check-run exists on this commit. Test, e2e, Format and CodeQL are all at action_required, awaiting your approval of workflows from a first-time fork contributor — they have run nothing. The one check that did run is pr-labels.yml, whose job is confusingly named test; its actual failure is the missing version label. #232 fixes the labelling for fork PRs (and renames that job), but cannot apply to already-open PRs until it is on main.

I can't clear it myself — Waiel5 has no push access, so it cannot self-label.

Locally: 18 passed across blocklist-router, blocklist-lib and purge-blocked, and the 4 new member-denial cases fail against the previous mounting.

@choyiny choyiny added the patch Bug fix or small change; bumps the patch version label Aug 5, 2026
@choyiny

choyiny commented Aug 5, 2026

Copy link
Copy Markdown
Owner

Appreciate the contribution, and I think this is totally valid. saasmail is meant to be a collaborative inbox for SaaS teams to manage mail together mostly. Wondering what your use case is when using saasmail?

@Waiel5

Waiel5 commented Aug 6, 2026

Copy link
Copy Markdown
Contributor Author

Sure — I am running an instance on my own domain and building a native iOS client against it (https://github.com/Waiel5/saasmail-mobile). Everything I have opened here came out of that: building a client is a good way to walk the whole API surface, and it surfaces things the dashboard never exercises because the dashboard and the worker ship from the same commit and a third-party client does not.

The collaborative-inbox model is exactly why this one bothered me. On a single-operator instance a member-vs-admin distinction is academic, but the moment there is a team, /api/blocklist was the one global-state router mounted without requireAdmin/api/suppressions, /api/webhook and /api/oauth-apps all had it. And DELETE /api/blocklist/mail is not inbox-scoped: it hard-deletes messages and their R2 objects across every inbox on the deployment, so a member could destroy mail in inboxes they cannot even read.

So it is less about my use case and more that the guard looked like an oversight against its three neighbours rather than a decision.

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Restricts global blocklist management and destructive mail purging to administrators.

Changes:

  • Adds requireAdmin guards to all blocklist routes.
  • Adds member-denial and admin-access tests.
  • Documents the security fix.

Reviewed changes

Copilot reviewed 3 out of 3 changed files in this pull request and generated no comments.

File Description
worker/src/index.ts Applies admin middleware to blocklist routes.
worker/src/__tests__/blocklist-router.test.ts Tests authorization across all blocklist operations.
CHANGELOG.md Records the security fix.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

patch Bug fix or small change; bumps the patch version

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants