Conversation
…ovider display names are sanitized
…ing org invites inside its transaction
…h and drop query strings
…ad the query-token flag from env
…, off-boarded operators lose report-chat powers
…on consumer domains, unique pending keys
…mous reports, posts, chat attachments)
…a validating upload
…tact suggestion is bounded
… unknown ids; claim nudge refuses foreign origins; resource budget checks the host first
…waitlist, guests cannot self-register on private events, roster search escapes wildcards
…-only invite email, generic suspension copy, escaped page search
…itical notices cannot be cancelled, unsubscribe failures answer 503, audits are logged or atomic
…an the sender never get an unprocessed upload; event pages serve processed media
…re consumer domains, one digest per message
…t claim under the event lock
…holds; erasure scrubs waitlist before registrations
This was referenced Sep 23, 2026
byteful
added this pull request to stack #122
September 24, 2026 17:53
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
What changed
Security fixes across the API, found by the campaign audit and hardened by an adversarial review pass. Sign-in never adopts an account whose email was never verified. Account deletion revokes every session before it runs. Uploads can only be attached by the person who uploaded them, and nobody but the uploader ever receives an unprocessed original. Private events answer like unknown ones to people without standing. Bans, org invites, host messaging, CSV exports, inbound city mail, websockets and logs each close their own gaps. Nothing is removed from any response a client relies on.
Before you start
media_assets.uploader, catalog-only) and 0182 (partial index on pending org invites by inviter, small table).Verify
[Web]
x;=1+1shows as text with an apostrophe before the=, never as a formula; plain numbers stay numbers.[Mobile]
[Admin]
Regression
Signing in — [Web] [Mobile]
Submitting reports and posts with photos — [Web] [Mobile]
Chat and DMs — [Web] [Mobile]
Events and RSVPs — [Web]
Organizations — [Web]
Host email links — [Web]
Unsubscribe — [Web]
Findings addressed
Grouped by theme; IDs are campaign ledger IDs (
.campaign/backend/ledger.json, not in this repo).Decisions for the reviewer
media_assets.uploader(0181). Every claim by uploadId (report, anonymous report, post, chat, DM, avatar, group avatar, event cover and gallery, page media, org logo, verification document) and finalize require the caller to be the uploader. Rows from before the deploy (NULL) are claimable only inside the existing 6-hour claim window, so they age out. Operator org logos must be uploaded through the operator's own console session, which is how the admin app uploads today. A browser that uploaded as a guest and then signed in can still use those uploads for its report or post (the verified guest cookie is carried alongside the session for this check only; quota, cookies and CSRF are unchanged). Callers with neither a session nor a guest cookie share one upload subject; closing that needs the guest cookie issued at upload time (cookie change, deferred).?token=works only whenWS_ALLOW_QUERY_TOKENis set (validated env, default off). Cookie upgrades check Origin against the allowlist; a missing Origin is allowed (native clients). Before authentication: 32 frames and 64 KiB are buffered, extra frames are dropped. After: 64 queued frames or 256 KiB, then one RATE_LIMITED frame and close 1008.kill_switch, because the DB constraint has no org value; the audit row carriesorg_suspended). Unsubscribe write failures answer 503 so one-click senders retry.;,,, tab, line break) even when spaces or double quotes sit between them, so a value likex;=1+1shows an apostrophe. Numbers are never prefixed. The roster export's audit row is written in the export's transaction; an audit failure now fails the request.--yesand re-resolves only the rows it touches.User-visible copy changes
Tests changed
Existing tests whose expectation changed because the fix changes the behavior they pinned (no test was skipped, deleted or loosened):
test/unit/delete-account-route.test.ts: push tokens and notifications are purged inside the erasure transaction, so the route no longer issues those deletes itself.test/unit/email-format.test.ts: a URL inside host-authored footer text is no longer auto-linked; the footer is built from typed segments.test/unit/media-routes.test.ts: an unsigned anon cookie is no longer a quota subject (0 charges instead of 2).test/integration/anon-pg.test.ts: a replayed anonymous submit returns a fresh claim code (plaintext codes are never stored).test/unit/export-csv-security.test.ts(rewritten) andtest/unit/host-export-csv.test.ts: separator-borne formulas are neutralized; every cell is quoted; numbers are not prefixed.test/unit/host-route-audit-security.test.ts: the roster export audit failure now fails the request (was 200 plus a warning).test/unit/socket-lifecycle-security.test.ts: with a 64-frame backlog the token bucket (60) answers the last frames with RATE_LIMITED while the socket stays open.test/unit/anon-repository-security.test.ts: the lost duplicate-submit race answers 409 instead of rotating the winner's code.test/integration/media-attach-claim-security-pg.test.ts: a chat send with an unclaimable upload is refused (422) instead of silently dropping it; fixtures now name their uploader.test/integration/registration-repository-security-pg.test.ts: a ban also reports the ticket type freed by the cancelled registration.test/unit/affiliation.test.ts,test/unit/chat-routes.test.ts,test/unit/chat-attachments-servable.test.ts,test/integration/avatar-media-pg.test.ts,test/integration/host-page-media-pg.test.ts, the chat integration tests: fixtures and call signatures only (new required arguments, the served key instead of the raw key, the production operator gate); no expectation loosened.Three source-text tests were made whitespace-insensitive in Backend formatting pass with the repo's Prettier config #91, not here.
Verification
pnpm typecheck,pnpm lint,pnpm build,pnpm check:sql, prettier check: pass.FFPROBE_PATH/FFMPEG_PATH): 281 passed, 2 failed. Both failures are the known ffprobe-helper environment issue fixed in Backend test safety net: SQL transcripts, CSRF pairing, authz and characterization tests #92, which this stack does not contain.*-pgfiles) are written but not run locally (no Docker by rule). CI runs only on PRs based on main, so it starts on this PR when Backend formatting pass with the repo's Prettier config #91 merges and GitHub retargets it to main; merge this one only after that run is green.Staging checks
https://api.civfix.dev/readyzanswers ready after the deploy (migrations 0181 and 0182 applied).SELECT DISTINCT u.id, u.created_at FROM users u JOIN oauth_identities o ON o.user_id = u.id WHERE u.email IS NOT NULL AND u.email_verified = false AND u.deleted_at IS NULL.Not covered
🤖 Generated with Claude Code