WPMgr v0.61.199, invite accept after revoke
WPMgr v0.61.199 MarkInvitationAccepted updates WHERE id AND accepted_at IS NULL with no revoked_at or token_hash bind. A client invite revoke returns 204 and a later accept still returns 200, membership, and a session. Sequential, not a tight race. Labbed. No CVE yet.
- Name
- WPMgr v0.61.199, invite accept after revoke
- Type
- 0-day analysis
- CVE
- n/a
- CVE Risk
- high
- Disclosure Status
- public
- Vendor
- mosamlife
- Affected
- WPMgr v0.61.199 (e1b3a163). Anonymous holder of a client invitation token. Org invites have no revoke on this pin.
- Published
- 09 Nov 2026
- Updated
- 09 Nov 2026
- Tags
- 0-day, wpmgr, broken-revoke, cwe-284, cwe-367, remote
Cancel returned 204. The guest still walked in.
I went looking for a race on WPMgr invitations. Lookup, revoke, and the argon2 password work all sit outside the claim transaction. MarkInvitationAccepted is UPDATE … WHERE id = $1 AND accepted_at IS NULL. No revoked_at IS NULL. No token_hash bind. That is the textbook TOCTOU shape.
The lab did not need a tight race. Revoke first, 204. Accept second, 200. Both timestamps set, about sixteen milliseconds apart. client_members went to 2. Session cookie issued. Comments in Service.Accept say a revoked invite is dead even to the original link. The SQL does not agree.
If you sent a client invite and then revoked it, anyone who still has the token can join anyway. They do not need a session of their own.
This is WPMgr v0.61.199 (e1b3a163988c9f1c31bc93922ea34322fa188792). No CVE yet. CWE-284 / CWE-367. 7.5 High (CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:H/A:N). Remote broken revoke. Pack: abraxas/wpmgr-invite-accept-after-revoke.
Why client invites
Org invitations have no revoke on this pin. Site-share revoke exists. Client invitations have DELETE /api/v1/clients/{id}/invitations/{invitation}. The control plane will issue a client invite without a WordPress agent attached. I almost blocked the lab on "need a site." I did not.
FIX 5 already made double-accept atomic. Second claim returns invitation_already_used. That is why trial 1 in the lab is a miss-late: accept won, revoke 409. Trial 2 flipped the order enough that revoke committed and accept still stamped accepted_at.
Enrollment pairing, autologin consume, and CP recovery codes are compare-and-set. I left them closed.
The lab
Compose wpmgrh12, loopback 18200-18204. Twenty trials, delay starting at 50ms. Trial 2 at 30ms: revoke 204 (revoked_at=22:05:33.944943Z), accept 200 (accepted_at=22:05:33.960446Z). Members 2. Session cookie 1.
IOC trial=2 revoke=204 accept=200
accepted_at=2026-11-09 22:05:33.960446+00
revoked_at=2026-11-09 22:05:33.944943+00
members=2 session_cookie=1
SUCCESS WPMGR-H12-INVITE-RACE trial=2 delay_ms=30 ... WPMGR-H12-ACCEPT-AFTER-REVOKEThe Go checks for revoked_at happen after a plain SELECT with no FOR UPDATE, then argon2 runs, then a different transaction claims by id. Even a slow, sequential revoke loses.
What an attacker can do
Needs a client invitation token that was issued and then revoked. In ordinary words: you took the invitation back. They used it anyway and they are on the client now.
| Privilege | Method / sink | Recon / integrity / availability | Lab-proven or inferred |
|---|---|---|---|
| Accept after a committed revoke | DELETE …/invitations/{id} 204 then POST /api/v1/invitations/accept 200 |
Integrity (membership) | Lab-proven |
| Session as the new member | accept Set-Cookie: wpmgr_session |
Integrity (authn) | Lab-proven |
| Client membership row | client_members count 2 |
Integrity | Lab-proven |
| Negative control: revoke after a won accept | trial 1 accept 200, revoke 409 | n/a | Lab-proven |
Stays closed on this leftover
| Axis | Method / sink | Why it is closed |
|---|---|---|
| Integrity | Double-accept | FIX 5 invitation_already_used |
| Integrity | Enrollment pairing consume | compare-and-set |
| Integrity | Autologin consume | compare-and-set |
| Integrity | CP recovery codes | compare-and-set |
| Integrity | Org-invite revoke | No revoke API on this pin |
The fix
Put revoked_at IS NULL and the presented token_hash in the same UPDATE that stamps accepted_at. Look the row up FOR UPDATE inside that transaction. Treat a row with both timestamps as a failed revoke, not a member.
I am @abraxas_null. Site abraxaslabs.tech. GitHub abraxas. Mail abraxas.null@proton.me.