Repository navigation
[Tracking] Security review 2026-10-06 fixes #416
Description
Goal
Fix the 34 findings from the 2026-10-06 security review. The first target was Friday 2026-10-09. Every issue below is on the milestone Security review 2026-10-06 and is a sub-issue of this one.
Status (2026-10-09)
All code is deployed (bbdaae66c, production run 37976785981, green). All migrations are applied, including the late ones for #434 and #402, and verified read-only on production. 22 sub-issues in the list below are closed with evidence.
Still open, and why:
- Confirm the password-token hook is on, and correct the connected-app docs #441, Run only reviewed main-branch workflows on the production runner #437, Give the observability containers least privilege #443: the code or docs are live. Each needs a maintainer step: the Supabase hook check and Secure password change (Confirm the password-token hook is on, and correct the connected-app docs #441), GitHub environment and runner settings (Run only reviewed main-branch workflows on the production runner #437), a
pg_monitorexporter role (Give the observability containers least privilege #443). - Stop serving editor media after a document moves to Trash #408: the code is live. Old objects must be made private with
s3cmd setaclfrom a workstation. - Require a password on production Redis #438, Serve editor media of a Private document only to its owner #439: built but held as a patch. Serve editor media of a Private document only to its owner #439 needs a decision on how the media gate behaves while
MEDIA_COOKIE_SECRETis unset. - Keep server secrets out of the webapp and admin-dashboard containers #403, Bring back push on Chrome and Android devices the server switched off #417: a production check and a phone push test are owed.
The chatroom Cypress suite passes except attachments.cy.ts "lists the hover menu rows". That test has failed since it was added (4f2c460d1). It hovers inside the HoverMenu scroll lock, and it needs a ruling. The #427 follow-up is #471.
Status (2026-10-06)
14 issues are fixed on branch claude/youthful-lovelace-3nvsc6. The branch is not merged or deployed yet. Each issue closes when the branch merges into main.
- Carry the production function revokes in the Supabase migrations #397 Carry the production function revokes in the Supabase migrations (
335138d) - Stop the SSRF filter from treating fcm.googleapis.com as a private address #398 Stop the SSRF filter from treating fcm.googleapis.com as a private address (
0f6fc54) - Stop document content from injecting CSS through Highlight and link marks #399 Stop document content from injecting CSS through Highlight and link marks (
b65b33e) - Stop channel members from muting or renaming a channel for everyone #401 Stop channel members from muting or renaming a channel for everyone (
335138d) - Keep server secrets out of the webapp and admin-dashboard containers #403 Keep server secrets out of the webapp and admin-dashboard containers (
1db1bd3) - Record the actor of every admin write action #404 Record the actor of every admin write action (
fd96138, step 1 only) - Stop any viewer from pushing a fake Pad title to live viewers #406 Stop any viewer from pushing a fake Pad title to live viewers (
6e5a537) - Keep a purged document id gone after a PUT or a reconnect #407 Keep a purged document id gone after a PUT or a reconnect (
02f7530) - Stop signed-in users from squatting a workspace slug to block chat #409 Stop signed-in users from squatting a workspace slug to block chat (
335138d) - Stop bookmarks and replies from copying a message the user cannot read #410 Stop bookmarks and replies from copying a message the user cannot read (
335138d) - Stop the sign-in error page from showing text from the URL #411 Stop the sign-in error page from showing text from the URL (
c6fdc2d, needs a maintainer decision, see below) - Keep raw database error text out of production 500 responses #413 Keep raw database error text out of production 500 responses (
355502b) - Bind dev and local Postgres, Redis and app ports to loopback only #414 Bind dev and local Postgres, Redis and app ports to loopback only (
0a09b5d) - Bring back push on Chrome and Android devices the server switched off #417 Bring back push on Chrome and Android devices the server switched off (
0f6fc54,da0d832,ea384b6,04fa4c0)
After the merge, run the #417 runbook. Its recovery queries are in the #417 comment.
Order of work for the open issues
Highest risk first. Inside one severity, the smaller fix goes first.
Critical
- Require approval before fork pull requests run workflows #440 Require approval before fork pull requests run workflows. It is one repo setting and takes minutes. Do it first.
- Confirm the password-token hook is on, and correct the connected-app docs #441 Confirm the password-token hook is on, and correct the connected-app docs. A dashboard check plus a docs fix.
High
- Make a Private document's chat readable and writable only by its owner #396 Make a Private document's chat readable and writable only by its owner. Size L.
- Serve editor media of a Private document only to its owner #439 Serve editor media of a Private document only to its owner. It is Low, but it follows the Make a Private document's chat readable and writable only by its owner #396 design. Needs a decision first.
- Stop clients from writing server-owned columns on chat messages #400 Stop clients from writing server-owned columns on chat messages. Run its read-only production pre-check first.
Medium
- Scope heading chat channels to their own document #402 Scope heading chat channels to their own document. Size M. Do it with Make a Private document's chat readable and writable only by its owner #396 or right after it.
- A pasted node or a channel row can crash a document page for every reader #436 A pasted node or a channel row can crash a document page for every reader.
- Clear the text of a chat message when it is deleted #435 Clear the text of a chat message when it is deleted.
- Stop exposing every user's online status and last-active time #434 Stop exposing every user's online status and last-active time. Ship the webapp step before the migration.
- Require a password on production Redis #438 Require a password on production Redis. Two deploys, staging first.
- Stop REST /health from leaking error text or taking every /api route down #405 Stop REST /health from leaking error text or taking every /api route down. Needs sign-off on the compose healthcheck change.
- Clear the public profile when an account is soft-deleted #427 Clear the public profile when an account is soft-deleted. Needs the deletion-path decision below.
- Check that a document id is one path segment before local media is deleted #426 Check that a document id is one path segment before local media is deleted. It affects self-hosters on local storage.
- Run only reviewed main-branch workflows on the production runner #437 Run only reviewed main-branch workflows on the production runner. It needs org settings and a quiet deploy window.
Low
- Let non-members read only chat media that a live message uses #432 Let non-members read only chat media that a live message uses. It changes the same policies as Make a Private document's chat readable and writable only by its owner #396, so do it after Make a Private document's chat readable and writable only by its owner #396.
- Re-check ghost status before an admin delete, and lock the last-admin guard #412 Re-check ghost status before an admin delete, and lock the last-admin guard. Needs the deletion-path decision below.
- Stop serving editor media after a document moves to Trash #408 Stop serving editor media after a document moves to Trash. It can ship now.
- Stop the public avatar buckets from accepting SVG files #431 Stop the public avatar buckets from accepting SVG files.
- Move the email and push gateway reports behind admin auth #433 Move the email and push gateway reports behind admin auth. Ship the server and the dashboard in one release.
- Keep the access token out of document page props #429 Keep the access token out of document page props.
- Re-check a WebSocket's identity when its access token expires #430 Re-check a WebSocket's identity when its access token expires. Needs sign-off on the reconnect cost.
- A forged presence payload can change the avatar on live chat messages #428 A forged presence payload can change the avatar on live chat messages.
- Resolve chat mentions by user id, not by username text #415 Resolve chat mentions by user id, not by username text. The review lists it as "Info only". It may close as accepted risk.
- Use a cryptographic random source for new document slugs #442 Use a cryptographic random source for new document slugs. Needs approval for one line in
proxy.ts. - Give the observability containers least privilege #443 Give the observability containers least privilege.
- Clear chat drafts and stale document keys on sign-out #444 Clear chat drafts and stale document keys on sign-out.
- Unsubscribe only when the reader confirms, not when a link scanner opens the email link #445 Unsubscribe only when the reader confirms, not when a link scanner opens the email link.
- Remove dead code left after the security fixes #446 Remove dead code left after the security fixes.
Optional push follow-ups after #417 are listed in #447.
Decisions the maintainer must make
- Stop the sign-in error page from showing text from the URL #411:
c6fdc2deditsapps/webapp/src/proxy.ts. RootCLAUDE.md§Settled (Next product APIs) forbids that. Accept the edit, or revert only theproxy.tspart. - Re-check ghost status before an admin delete, and lock the last-admin guard #412 and Clear the public profile when an account is soft-deleted #427: after Re-check ghost status before an admin delete, and lock the last-admin guard #412, the ghost delete refuses any account that is not a ghost. A real user who asks for deletion on the privacy page is not a ghost. Choose the deletion path for such a request before either issue merges.
- Serve editor media of a Private document only to its owner #439: choose how a media request proves who asks. The issue gives the smallest option found.
- Stop REST /health from leaking error text or taking every /api route down #405: approve pointing the
rest-apiDocker healthcheck at/health/database. - Re-check a WebSocket's identity when its access token expires #430: approve one reconnect per token lifetime for every signed-in tab.
- Require approval before fork pull requests run workflows #440: the fork pull request approval setting. Only the maintainer can change it.
- Confirm the password-token hook is on, and correct the connected-app docs #441: the Supabase dashboard hook and the secure password change setting. Only the maintainer can check them.
- Use a cryptographic random source for new document slugs #442: approve one line in
apps/webapp/src/proxy.ts, which rootCLAUDE.md§Settled protects. - Run only reviewed main-branch workflows on the production runner #437: the org runner group and
productionenvironment settings. Only the maintainer can change them.
Risk to the deadline
#396 is L and #402 is M. Together they may not fit in two days. If they slip, ship the Data API read gate of #396 first (anon and non-owner reads refused). That closes the High exposure. Then move the notification and media parts to a follow-up.
Done means
- Every acceptance box in each sub-issue is checked.
- SQL changes ship as migrations and mirror
packages/supabase/scripts/.seed.sqlis regenerated withbun run --filter @docs.plus/supabase_back seed. apps/webapp/src/types/supabase.tsis regenerated when a function signature or a column changes.- The chatroom Cypress suite passes.
- The production check from Carry the production function revokes in the Supabase migrations #397 shows no unexpected
truerows after deploy.
Source
Local report: Notes/local-docs/security-review-2026-10-06.md and its raw reports. They are not in the repo; the maintainer holds a copy sent on 2026-10-06. Every issue is self-contained, so the report is not needed to execute them.
Generated by Claude Code
Activity
18 remaining items
[-]Tracking: security review 2026-10-06 fixes (due Friday 2026-10-09)[/-][+][Tracking] Security review 2026-10-06 fixes[/+]HMarzban commentedon Oct 6, 2026
Handoff, 2026-10-06
The session that produced this review is closing. A new session starts here.
Branch
claude/youthful-lovelace-3nvsc6is 15 commits ahead ofmainand 2 behind; last commitd5117e7.mainshows no conflict.Fixed on the branch (each closes when the branch merges into
main): #397, #398, #399, #401, #403, #404, #406, #407, #409, #410, #411, #413, #414, #417. Changelogs and agent docs are updated in7560222andd5117e7.Next steps, in order
proxy.tsedits in Stop the sign-in error page from showing text from the URL #411 and Use a cryptographic random source for new document slugs #442.maininto the branch, open the pull request, and let CI run extension Cypress. Cloud sessions cannot download the Cypress binary, so that gate did not run locally (seeAGENTS.md§Learned Workspace Facts).20261006120000, then20261006130000;Environment notes for a cloud session
CYPRESS_INSTALL_BINARY=0 bun install --frozen-lockfile.LC_ALL=C.UTF-8.download.cypress.ioto the environment's allowed network domains.Generated by Claude Code
HMarzban commentedon Oct 9, 2026
Status, 2026-10-09
Deployed to prod today (run 37906965365, deploy job finished 09:28:41Z, after a migration push of
20261006120000–20261008140000):Decisions taken today:
proxy.tsedit is accepted, because it touches only the auth-error branch. It is deployed.internalschema helpers) and Remove anon default table privileges on production #470 (anon default table privileges).Now: every other open sub-issue is being re-validated against today's
main, then built in file-disjoint waves. Maintainer-only steps (#440, #437, and the dashboard half of #441) get their own checklist.