fix(coderd): prevent user-admin from resetting owner password#25709
Conversation
Add an owner-role guard to putUserPassword that refuses password-reset requests when the target user holds the owner role unless the caller is also an owner. This mirrors the existing guard in putUserStatus. Without this fix, a user-admin could reset any owner's password and authenticate as them, gaining full deployment control.
|
/coder-agents-review |
|
Review posted | Chat Review history
deep-review v0.5.0 | Round 1 | Last posted: Round 1, 6 findings (1 P2, 1 P3, 2 Nit, 2 Note), REQUEST_CHANGES. Review Finding inventoryFindings
Contested and acknowledged(none) Round logRound 1Panel (17 reviewers). 1 P2, 1 P3, 2 Nit, 2 Note new. 2 dropped. Reviewed against 20b50dd..fde041b. About deep-reviewCRF = Coder Review Finding (P0-P4, Nit, Note)
|
There was a problem hiding this comment.
Solid security fix. The guard is correctly placed, correctly scoped, and the privilege-escalation vector is closed. The existing test for the negative case is sharp: four assertions that pin the rejection to the exact guard. Pariston's differential diagnosis confirmed framings 2-4 (split RBAC action, remove old-password bypass, encode role hierarchy) are real systemic issues but disproportionate for a targeted fix that needs to ship.
One P2, one P3, two nits, two notes.
The P2: no test exercises the user-admin's legitimate password-reset path after the guard is added. AdminCanUpdateMemberPassword uses the owner client, not a user-admin client. If the guard condition is accidentally broadened in a future refactor, user-admin silently loses its ability to reset any password, and no test catches it. Four reviewers flagged this independently.
Five reviewers noted the PR description says this "mirrors the existing guard in putUserStatus." It doesn't. putUserStatus blocks ALL suspension of owners (including by other owners); this guard conditionally allows owner-to-owner resets. The design choice is correct, but "mirrors" will mislead anyone doing git-blame archaeology. Worth a description edit.
Four reviewers flagged deleteUser (line 654) as having the same class of gap: no owner guard, and user-admin holds ActionDelete on ResourceUser. The consequence is account destruction (DoS), not privilege escalation, and it's pre-existing. Noting it here as context for the broader attack surface.
"UserAdminCannotResetOwnerPassword is a real gem." -Bisky
coderd/users.go:654
Note [CRF-6] deleteUser has no owner guard, same vulnerability class.
user-admin holds ActionDelete on ResourceUser site-wide (coderd/rbac/roles.go:407). deleteUser checks self-deletion (line 668) and workspace count (line 688), but never checks whether the target is an owner. A user-admin can soft-delete an owner account. The consequence is DoS (account destruction), not privilege escalation, and the assignRoles map prevents chaining into role theft. Pre-existing and not this PR's scope, but four reviewers independently flagged it as the same class of ad-hoc guard gap. (Kurapika, Ryosuke P4, Razor P4, Zoro P4)
🤖
🤖 This review was automatically generated with Coder Agents.
- CRF-3: Improve error message to say 'acting user' for disambiguation - CRF-4: Use stdlib slices.Contains instead of custom slice.Contains - CRF-5: Add LoginWithPassword verification to OwnerCanResetOwnerPassword
Review response (2a54e17)CRF-1 (P2): Investigated. The user-admin role lacks CRF-2 (P3): Keeping HTTP 400 for consistency with the direct sibling CRF-3 (Nit): Fixed. Changed to CRF-4 (Nit): Fixed. Switched to stdlib CRF-5 (Note): Fixed. Added CRF-6 (Note): Acknowledged. CRF-7 (PR description): Fixed. Updated to clarify the guard is "modeled on"
|
| if !slices.Contains(actingUser.RBACRoles, rbac.RoleOwner().String()) { | ||
| httpapi.Write(ctx, rw, http.StatusBadRequest, codersdk.Response{ | ||
| Message: "Only owners can change the password of an owner.", | ||
| }) | ||
| return | ||
| } |
There was a problem hiding this comment.
Oof, I dislike specific role checks. I don't have another solution atm though
There was a problem hiding this comment.
I believe this matches the existing checks we do putUserStatus fwiw
PUT /api/v2/users/{user}/passwordwas protected only byActionUpdatePersonal, which the built-inuser-adminrole holds site-wide. No guard prevented targeting an owner. The old-password check is skipped for non-self resets, so a user-admin could reset any owner's password and authenticate as them, gaining full deployment control.Add an owner-role guard to
putUserPasswordthat refuses password-reset requests when the target holds the owner role unless the caller is also an owner. This is modeled on the guard inputUserStatus, but differs in that it conditionally allows owner-to-owner resets (whereasputUserStatusblocks all suspension of owners regardless of caller).Fixes https://linear.app/codercom/issue/PLAT-227
Implementation details
Authorizecheck, beforehttpapi.ReadapiKey.UserID != user.IDgates the check so self-password-change is unaffectedputUserStatus)UserAdminCannotResetOwnerPassword,OwnerCanResetOwnerPassword