IAM handlers skip their permission checks
Table of Contents
This page is a capture in the inbox bucket of the product backlog — a pre-sprint idea, not yet pulled into a sprint as a story.
1. What
Several IAM message handlers never call has_permission, even though the
permission code they should check exists and is seeded. The most serious is role
assignment: iam.v1.roles.assign and iam.v1.roles.assign-by-name grant any
role to any account with no permission check at all, while
iam.v1.roles.revoke directly beside it does check iam::roles:revoke. Any
authenticated account can make itself a TenantAdmin.
2. Why
Found on 2026-09-23 while documenting the IAM user journeys (User Journeys): the access, directory and tenancy groups each ran into it independently.
The sites, all verified in source:
- Role assignment is unguarded.
projects/ores.iam/core/include/ores.iam.core/messaging/authorization_handler.hpp:66isassign: decode, validate the caller, callsvc.assign_role(...), reply. Nohas_permission.assign_by_nameat line 218 is the same.revokeat line 94 checksperms::roles_revokeat line 113. The asymmetry is the bug: taking a role away is guarded, handing one out is not. Nothing needs to be added to the permission catalogue;iam::roles:assignis already seeded. - The account and login reads are unguarded, in a different file from the
writes. The six guarded writes live in
account_operations_handler.hpp, which checkshas_permissionfor create, delete, lock, unlock, reset_password and update at lines 250, 306, 336, 371, 406 and 494. The reads live in a separately named file,account_handler.hpp:list_accountsat line 75 andget_accountat line 116. That file declaresusing ores::service::messaging::has_permission;at line 51 and never calls it.login_info_handler.hpphas the same shape —list_login_infoat line 75 andget_login_infoat line 116, with no call. So the guard was not forgotten in one place: the read handlers were written separately from the write handlers and never got it.iam::accounts:readandiam::login_info:readexist inpermission_codes.hppand are seeded, and no handler consults them. Any signed-in account can read every account in the tenant and every account's sign-in state. - The two combine with a third defect. The account wire shape carries
password_hash,password_saltandtotp_secret(Account secrets reach the browser in the accounts wire shape). Because the reads are ungated, any authenticated account can list the tenant and receive every account's password hash and TOTP seed. The privilege escalation in item 1 turns that into full compromise of any account.
This is one root cause with several sites: a handler that forgets the guard its
neighbours have. Fixing only assign leaves the reads open, and fixing only the
reads leaves the escalation.
Suggested shape of the fix: make the guard structural rather than remembered —
either a shared wrapper every handler passes through, or a test that asserts each
declared read or write subject has a matching has_permission call. The repo
favours the second kind of check elsewhere, and it is the one that cannot be
forgotten twice.
3. References
projects/ores.iam/core/include/ores.iam.core/messaging/authorization_handler.hpp—assign(66),revoke(94),assign_by_name(218).projects/ores.iam/core/include/ores.iam.core/messaging/account_operations_handler.hpp— the six guarded writes.projects/ores.iam/core/include/ores.iam.core/messaging/account_handler.hpp—list_accounts(75) andget_account(116), the unguarded account reads.projects/ores.iam/core/include/ores.iam.core/messaging/login_info_handler.hpp—list_login_info(75) andget_login_info(116), the unguarded login reads.projects/ores.iam/api/include/ores.iam.api/domain/permission_codes.hpp— the codes that exist but go unchecked.