Fix stale session permissions after wh_Auth_UserSetPermissions#481
Fix stale session permissions after wh_Auth_UserSetPermissions#481yosuke-wolfssl wants to merge 1 commit into
Conversation
There was a problem hiding this comment.
Pull request overview
This PR fixes an authorization correctness issue where wh_Auth_UserSetPermissions() updated the backend user record but did not refresh the calling session’s cached permissions, allowing revoked privileges to persist until logout. It also documents and tests the new “bind immediately” behavior and introduces a core-level guard preventing non-admin sessions from setting the admin flag.
Changes:
- Update
wh_Auth_UserSetPermissions()to refreshcontext->user.permissionswhen a logged-in user changes its own permissions successfully. - Add a core-enforced policy: non-admin sessions cannot set the admin flag via
wh_Auth_UserSetPermissions(), independent of backend policy. - Add unit + integration tests to verify immediate self-demotion effects and cache-sync behavior.
Reviewed changes
Copilot reviewed 6 out of 6 changed files in this pull request and generated 3 comments.
Show a summary per file
| File | Description |
|---|---|
| wolfhsm/wh_auth.h | Documents session-cache refresh behavior and the new admin-flag refusal semantics for wh_Auth_UserSetPermissions(). |
| src/wh_auth.c | Implements admin-flag refusal for non-admin sessions and refreshes the live session permission cache on successful self-targeted updates. |
| test-refactor/client-server/wh_test_auth.c | Adds an end-to-end self-demotion test and a core-only mock-backend unit test for cache synchronization behavior. |
| test-refactor/wh_test_list.c | Registers the new whTest_AuthSetPermsCacheSync test. |
| test-refactor/README.md | Updates test inventory documentation to include the new auth cache-sync test. |
| docs/src/5-Features.md | Documents core vs backend responsibility for permission policy and explains live-session binding semantics and consequences. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
b0fbc8c to
99d30d3
Compare
wolfSSL-Fenrir-bot
left a comment
There was a problem hiding this comment.
Fenrir Automated Review — PR #481
Scan targets checked: wolfhsm-core-bugs, wolfhsm-crypto-bugs, wolfhsm-src
No new issues found in the changed files. ✅
Frauschi
left a comment
There was a problem hiding this comment.
🐺 Skoll Code Review
Overall recommendation: APPROVE
Findings: 2 total — 1 posted, 1 skipped
Posted findings
- [Info] Session permission cache mirrors caller-supplied values, not backend-persisted state —
src/wh_auth.c:483-491
Skipped findings
- [Low] Session cache mirrors supplied permissions verbatim; diverges if a custom backend transforms them
Review generated by Skoll via Claude/Codex
| rc = WH_AUTH_PERMISSION_ERROR; | ||
| } | ||
| else { | ||
| rc = context->cb->UserSetPermissions( |
There was a problem hiding this comment.
⚪ [Info] Session permission cache mirrors caller-supplied values, not backend-persisted state
💡 SUGGEST
The new cache-sync writes context->user.permissions = permissions; — the raw values the caller supplied — after the backend returns WH_ERROR_OK, rather than re-reading what the backend actually stored. The core's only escalation check here is the absolute admin-flag guard (!WH_AUTH_IS_ADMIN(caller) && WH_AUTH_IS_ADMIN(permissions)); there is no subset check on group/action/key-id bits (unlike wh_Auth_UserAdd). Data flow: caller-supplied permissions -> admin-flag guard -> cb->UserSetPermissions (may clamp/reduce and still return OK) -> context->user.permissions = permissions (verbatim) -> read by wh_Auth_CheckRequestAuthorization. A backend that permits a self-targeted permission edit but persists a narrower set than it was handed (e.g. clamps keyIdCount, strips bits) leaves the live session cache holding MORE authority than the persisted record, and that surplus binds to the running session immediately. The shipped admin-only base backend does not exhibit this (an admin already may set anything), so there is no in-tree exploit; the risk is a sharp edge for custom whAuthCb backends. This is documented in docs/src/5-Features.md and exercised intentionally by Test Case 4 in wh_test_auth.c, so it is a design decision, reported here only as a hardening note. The verbatim over-large keyIdCount is safe against OOB reads because wh_Auth_CheckKeyAuthorization bounds its loop with && i < WH_AUTH_MAX_KEY_IDS.
Recommendation: Follow-up (non-blocking, not required for this PR). The divergence is not purely hypothetical: the shipped base backend already normalizes on write -- wh_Auth_BaseUserSetPermissions clamps keyIdCount and zeroes unused keyIds (src/wh_auth_base.c:423-434) -- so the verbatim cache can differ from the stored record even in-tree. It is benign today only because wh_Auth_CheckKeyAuthorization bounds its loop with i < WH_AUTH_MAX_KEY_IDS, and the base backend is admin-only for this call. To close the gap for custom whAuthCb backends without breaking the existing callback ABI, add an optional read-back callback and refresh the cache from it after a successful self-targeted set, falling back to the supplied struct when the backend does not implement it:
/* whAuthCb: new OPTIONAL member, appended at the end of the struct */
int (*UserGetPermissions)(void* context, whUserId user_id,
whAuthPermissions* out_permissions);/* wh_Auth_UserSetPermissions: replace the verbatim mirror */
if ((rc == WH_ERROR_OK) && (user_id == context->user.user_id) &&
(context->user.user_id != WH_USER_ID_INVALID)) {
whAuthPermissions effective = permissions;
if (context->cb->UserGetPermissions != NULL) {
rc = context->cb->UserGetPermissions(context->context, user_id,
&effective);
}
if (rc == WH_ERROR_OK) {
context->user.permissions = effective;
}
}Backends that leave UserGetPermissions NULL keep today's behavior, so no existing backend breaks.
Problem
wh_Auth_UserSetPermissionsforwarded a permission change to the backend usertable but never refreshed the per-session cache
context->user.permissions.Authorization reads only that cache (
wh_Auth_CheckRequestAuthorization), so alive session that revoked its own privileges kept them until logout — revocation
did not bind an active session.
wh_Auth_Logoutalready self-synchronised; theasymmetry was the bug. Addressed by F-4239.
Fix (
src/wh_auth.c)Refresh the session cache when a logged-in user changes its own permissions:
rc == WH_ERROR_OKleaves the cache untouched when the backend fails.!= WH_USER_ID_INVALIDstops a logged-out session (id0) from matching aWH_USER_ID_INVALIDtarget.WH_AUTH_PERMISSION_ERRORbefore the backend is consulted, so an unvalidatedset can never bind the live session. This makes the core deliver the guarantee
already claimed in
docs/src/5-Features.md.Behavior change: a custom
whAuthCbimplementing delegated-admin now getsWH_AUTH_PERMISSION_ERRORwhere it previously succeeded. Unobservable with theshipped base backend, which is already admin-only for this call.
Tests (
test-refactor/client-server/wh_test_auth.c)whTest_AuthSetPermsCacheSync(new entry point)whAuthCb. Cache refresh on self-target; unchanged on backend failure, different target, and logged-out session. Admin branch, target-independent admin refusal, and the self-locking admin clear.whTest_AuthSetPermissionsUserAddis denied, perms restored unconditionally before asserting.Verification
make AUTH=1 check: 52 passed, 18 skipped, 0 failed of 70.disabling the admin guard, or making it target-dependent, fails the unit test.
-std=c90 -Werror -Wall -Wextra; changed lines within 80 columns.