fix(auth): enforce role ceiling on user update and fix OAuth callback XSS #426
Loading…
Reference in a new issue
No description provided.
Delete branch "fix/PMS-625-auth-audit-privesc-xss"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Auth/login/2FA/session security audit (PMS-625). Two exploitable gaps found and fixed; the full findings table (including follow-ups to file) lives in docs/dev-docs/security/auth-audit-PMS-625.md.
F1 (privilege escalation): the admin PUT /api/v1/auth/users/{id} path checked only is_admin(), not the PMS-503 can_grant ceiling that POST /users already enforces. A tenant admin could set any user's role - including their own via PUT /users/{self}, which is not the role-sanitizing /me handler - to super_admin, a platform-level cross-tenant account. Mirror the create-side ceiling on the update path so an above-ceiling grant returns 403.
F2 (reflected XSS): google_login::callback_html embedded serde_json output into an inline
Auth/login/2FA/session security audit (PMS-625). Two exploitable gaps found and fixed; the full findings table (including follow-ups to file) lives in docs/dev-docs/security/auth-audit-PMS-625.md. F1 (privilege escalation): the admin PUT /api/v1/auth/users/{id} path checked only is_admin(), not the PMS-503 can_grant ceiling that POST /users already enforces. A tenant admin could set any user's role - including their own via PUT /users/{self}, which is not the role-sanitizing /me handler - to super_admin, a platform-level cross-tenant account. Mirror the create-side ceiling on the update path so an above-ceiling grant returns 403. F2 (reflected XSS): google_login::callback_html embedded serde_json output into an inline <script> and wrongly assumed serde_json HTML-escapes <, >, and &. The OAuth error branch reflects the fully attacker-controlled error_description query param into the payload, so /auth/google/callback?error=x&error_description=</script><script>... executes as markup on the API origin. Route both the payload and origin JSON through escape_json_for_script (<, >, &, U+2028, U+2029 -> \uXXXX); the output is byte-for-byte equivalent JSON but cannot close the script element. Tests: tests/auth.rs::update_user_enforces_role_ceiling (admin->super_admin 403, admin->manager 200); google_login unit tests callback_html_neutralizes_script_breakout and escape_json_for_script_is_reversible_json. #PMS-625