Skip to content

fix(platform): audit successful 2FA and passkey lifecycle events - #3181

Open
Israeltheminer wants to merge 1 commit into
mainfrom
fix/2fa-lifecycle-audit
Open

fix(platform): audit successful 2FA and passkey lifecycle events#3181
Israeltheminer wants to merge 1 commit into
mainfrom
fix/2fa-lifecycle-audit

Conversation

@Israeltheminer

Copy link
Copy Markdown
Collaborator

An attacker who registered their own passkey and turned 2FA off left no successful-action trail. 0.4 audited every successful second-factor lifecycle event (#1508); the port kept only the failure half.

Split out of #3149. Refs #3142.

Why

The failure half audits, so a wrong code is recorded — but enabling 2FA, disabling it, and adding, removing or signing in with a passkey were silent. Those are the events that matter most in an account takeover.

All five 0.4 action names are reused verbatim: 2fa_enrolled, 2fa_disabled, passkey_added, passkey_removed, passkey_sign_in. A renamed action silently breaks any query or export that groups on it. Checked against 0.4's full vocabulary — passkey_removed and passkey_revoked_by_admin were distinct events there and stay distinct here.

Rows are written in the same transaction as the state change, carrying the 0.4 security/success shape.

What is NOT here, and why

#3149 also restored the conversation write-role gate, so a read-only member could no longer reply, send outbound mail, close, bulk-act or delete. That half is held back. It is faithful — 0.4's rule is literally authorizeRls(role, 'conversations', 'write'), and this repo's own matrix gives member read only — but it breaks the harness:

mine (both halves):  aborts at 178 checks, 9 failures
main:                373/379, 6 known failures
2FA half alone:      373/379, identical 6

The first observable failure is real, not incidental:

FAIL  blob-ref authority: outbound mail attaches only the sender's uploads
      compose → 403 FORBIDDEN (want 403 attachment_not_owned)

The gate fires before the attachment-ownership check, so that assertion gets the wrong 403 — and several later chat checks then cascade. Either the harness fixtures compose as under-privileged actors and need their roles raised, or the gate's placement relative to the ownership check needs rethinking. That is real work, not a rebase, so it is not being smuggled in behind a verified fix.

Tests

integration-check.ts asserts the whole trail and its shape, on a dedicated throwaway user rather than the harness's own:

trail=2fa_enrolled,2fa_disabled,passkey_added,passkey_sign_in,passkey_removed
shape=security/success   enabledLeftOn=false

That isolation is load-bearing, and finding out cost three runs. Running the lifecycle on the shared session left that user's second factor changed and their session invalid, which failed five unrelated checks — the 2FA grace check plus four chat checks. The throwaway user also needs an org membership, because audit rows are org-scoped and without one the trail reads empty.

WebAuthn cannot be driven from a script, so the passkey trio is asserted at the writer: the action names land as rows with the security shape.

374/380, and the six failures are byte-identical to a baseline run of unmodified main on the same instance — the warm-MinIO bucket collision, two yt-dlp probes and three agent-lane probes.

Gate: typecheck, oxlint --type-aware, oxfmt --check, lint:sast green; branched from origin/main at 899fcc08a.

0.4 audited every SUCCESSFUL second-factor lifecycle event (#1508). The
port kept only the failure half, so an attacker who registered their own
passkey and turned 2FA off left no successful-action trail — the events that
matter most in an account takeover.

All five 0.4 action names are reused verbatim: 2fa_enrolled, 2fa_disabled,
passkey_added, passkey_removed, passkey_sign_in. A renamed action silently
breaks any existing query or export that groups on it. Checked against 0.4's
full vocabulary — passkey_removed and passkey_revoked_by_admin were distinct
events there and stay distinct.

Rows are written in the same transaction as the state change, with the 0.4
security/success shape.

Split out of a wider branch that also gated conversation writes; that half
is held back because it breaks the harness (see the PR).

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

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant