You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
fix(security): close the wave-2 advisories (RLS, XSS sink, token exposure, KDFs) (#252)
Addresses the ten advisories left open after wave 1 (#244-#251).
Application:
- MessageItem no longer passes raw message content to dangerouslySetInnerHTML.
The sink is currently unreachable because the format detection reads fields the
detection library does not return, but any fix to that naming would have turned
it into stored XSS against every recipient. (GHSA-9jq8-g7m8-wg4v)
- /admin no longer serialises autoblog bearer tokens into the RSC payload; the
page ships a non-reversible hint and the token is shown once, at creation.
(GHSA-75px-j56m-6cpr)
- /api/conversations/delete now lets a participant delete only rows they own,
whatever the conversation type. The shared conversation is garbage-collected
once the last participant leaves, so a single member of a direct conversation
can no longer destroy the other party's ciphertext and files.
(GHSA-xm8x-rpr6-4j7h)
- The CoinPay OAuth callback requires email_verified before linking an identity
to an existing account. A missing claim does not count as verified.
(GHSA-j2m4-m3w7-w2cq)
- getClientIp only reads forwarding headers when a trusted proxy is configured,
and the count now defaults to 1 rather than 0. Previously an unset count meant
X-Real-IP was honoured unconditionally, so rotating that header minted a fresh
rate-limit bucket per request and defeated every limiter including the SMS
ones. (GHSA-64m7-3h2w-2qr6)
- PBKDF2 work factor raised from 100k to OWASP's 600k, and the backup-PIN scrypt
from N=2^17. Key exports record the iteration count they were written with so
existing files still import. (GHSA-29hv-86qw-3vx5)
- /api/users/search runs as the service role after verifying the session, which
is what lets the users policy below be narrowed, and only matches phone numbers
once the query is long enough to not be an existence oracle.
Database (20260816120000):
- users_select_authenticated was USING (true): any authenticated account,
including a throwaway anonymous sign-in, could read every user's phone_number
and salt. Narrowed to the caller's own row plus users they share a conversation
with. (GHSA-7w99-6w89-2926)
- conversation_participants INSERT required only a non-NULL auth.uid(), so anyone
could inject themselves into any conversation. Restricted to the conversation's
creator or an existing participant. (GHSA-vxcr-4mm8-jfm3)
- conversations carried a FOR SELECT USING (true) policy with no role clause,
readable by anon on any fresh deploy. (GHSA-9jgr-3h36-9748)
- conversation_participants was likewise USING (true), exposing the whole social
graph. Scoped to conversations the caller is in. (NEW-02)
- get_inactive_participants returns full phone numbers; EXECUTE revoked from
authenticated, leaving the one server-side service-role caller.
(GHSA-ffpr-xfm2-pp84)
The policy helpers are SECURITY DEFINER so that policies referencing each other's
tables do not re-enter one another, which is what this schema's earlier "fix
recursion" migrations were fighting.
497 tests pass; the build is clean.
Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
0 commit comments