Skip to content

Fix stale session permissions after wh_Auth_UserSetPermissions#481

Open
yosuke-wolfssl wants to merge 1 commit into
wolfSSL:mainfrom
yosuke-wolfssl:fix/f_4239
Open

Fix stale session permissions after wh_Auth_UserSetPermissions#481
yosuke-wolfssl wants to merge 1 commit into
wolfSSL:mainfrom
yosuke-wolfssl:fix/f_4239

Conversation

@yosuke-wolfssl

Copy link
Copy Markdown

Problem

wh_Auth_UserSetPermissions forwarded a permission change to the backend user
table but never refreshed the per-session cache context->user.permissions.
Authorization reads only that cache (wh_Auth_CheckRequestAuthorization), so a
live session that revoked its own privileges kept them until logout — revocation
did not bind an active session. wh_Auth_Logout already self-synchronised; the
asymmetry 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:

if ((rc == WH_ERROR_OK) && (user_id == context->user.user_id) &&
    (context->user.user_id != WH_USER_ID_INVALID)) {
    context->user.permissions = permissions;
}
  • rc == WH_ERROR_OK leaves the cache untouched when the backend fails.
  • != WH_USER_ID_INVALID stops a logged-out session (id 0) from matching a
    WH_USER_ID_INVALID target.
  • Admin guard — a non-admin session granting the admin flag is refused with
    WH_AUTH_PERMISSION_ERROR before the backend is consulted, so an unvalidated
    set 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 whAuthCb implementing delegated-admin now gets
WH_AUTH_PERMISSION_ERROR where it previously succeeded. Unobservable with the
shipped base backend, which is already admin-only for this call.

Tests (test-refactor/client-server/wh_test_auth.c)

Test Coverage
whTest_AuthSetPermsCacheSync (new entry point) Core-only, mock 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_AuthSetPermissions End-to-end: admin self-demotes, its next UserAdd is denied, perms restored unconditionally before asserting.

Verification

  • make AUTH=1 check: 52 passed, 18 skipped, 0 failed of 70.
  • Negative controls: disabling the cache write fails the integration test;
    disabling the admin guard, or making it target-dependent, fails the unit test.
  • Clean under -std=c90 -Werror -Wall -Wextra; changed lines within 80 columns.

@yosuke-wolfssl yosuke-wolfssl self-assigned this Jul 23, 2026
Copilot AI review requested due to automatic review settings July 23, 2026 01:58

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 refresh context->user.permissions when 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.

Comment thread wolfhsm/wh_auth.h Outdated
Comment thread docs/src/5-Features.md Outdated
Comment thread test-refactor/client-server/wh_test_auth.c

@wolfSSL-Fenrir-bot wolfSSL-Fenrir-bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 Frauschi self-assigned this Jul 23, 2026

@Frauschi Frauschi left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🐺 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 statesrc/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

Comment thread src/wh_auth.c
rc = WH_AUTH_PERMISSION_ERROR;
}
else {
rc = context->cb->UserSetPermissions(

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚪ [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.

@Frauschi Frauschi removed their assignment Jul 23, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

5 participants