//nefariousplan

Auth Pins The Slot, Not The Value

Authorization is granted for a request-handle whose contents the attacker can replace before the grant is consumed. Polkit, OAuth, transaction caches, any framework where auth is requested for a slot and the slot's value is mutable post-request.

An authorization framework grants for a request the caller has named. Polkit, OAuth consent screens, signed approval tickets, every system that lets a user say yes to an action does it by binding the answer to the request's identity. The bug is what happens between when the question is asked and when the answer is bound: in a class of designs, the parameters the user thought they were approving are still in motion, sitting in a mutable cache the same caller can rewrite. The grant survives the rewrite because the grant attached to the slot, not to the value in it. The yes the user clicked is now a yes to whatever the slot contains at consumption time.

This is not a TOCTOU on a filesystem path. The slot is a server-side object, the writer is the same authenticated caller, and the auth framework is the third party whose answer the bug exploits.

Mechanism

A server holds caller-supplied parameters in a cache while it asks an external authorizer whether the action is permitted. The cache is keyed on a slot identifier (a transaction id, a request handle, a session, a ticket). The authorizer's answer is also keyed on the slot identifier. When the answer arrives, the server reads the cache by slot id and acts on whatever value is there.

The race window is between the auth request and the auth answer. During that window the cache is writable by the same caller who created the slot. If the caller writes a new parameter set into the slot before the auth answer arrives, the auth result still binds to the slot, but the value the auth result now applies to is the attacker's. The user who answered yes was answering yes to one thing; the action the system runs is on a different thing.

Three properties together produce the bug. First, parameters live in a mutable server-side cache rather than being captured into the auth request itself. Second, the auth answer is bound to slot identity rather than to a snapshot or hash of the parameters. Third, the caller who can request auth is the same actor who can rewrite the slot before the answer is consumed. Remove any one and the pattern collapses: a stateless auth call that takes the parameters as part of the call has nothing to swap; a content-addressable auth grant cannot be applied to different content; a slot only the authorizer can write cannot be raced.

The pattern shows up wherever a server treats authorization as a permission to act on a slot rather than a permission to act on a value. PackageKit's pre-1.3.5 transaction cache was the canonical exhibit, but the shape is widely repeated: D-Bus services with cached method parameters, OAuth flows with mutable scope or claim payloads between consent and token exchange, REST APIs that accept a PATCH on a request-handle while the request is "pending review", any signed-approval system where the claim signs a request-id rather than the request body. Every fix lands in the same place: capture the value into the auth request at the moment the question is asked, lock the slot until the answer is bound, or sign the grant over the content rather than the slot.

Exhibits

CVE-2026-41651: Polkit Authorized the Slot, Not the Value. The canonical exhibit. PackageKit asked polkit to authorize an InstallFiles call whose parameters lived in a mutable cache that the same caller could rewrite before polkit answered. Polkit's authorization survived the mutation. The post-mutation parameters consumed the authorization the pre-mutation parameters had earned.

Boundaries

Not every async-auth flow has this bug. Flows where the auth request itself carries the parameters (the authorizer answers yes-or-no based on what is being asked, not on a slot id pointing somewhere else) are safe. The failure mode is specifically the design where the question is asked by reference and the reference resolves at consumption time. If your auth call is permit(action, value) and the answer rides over the value, you are not in this pattern. If your auth call is permit(slot) and the answer comes back later to read whatever the slot now contains, you are.

Not solved by faster auth. Making polkit auto-grant or skipping the prompt narrows the race window to milliseconds, but the structural issue (the grant binds to the slot, not to the value) remains. A genuinely synchronous auth that reads the slot at decision time and re-reads at consumption time would race against itself in the same way. The fix is at the slot, not at the auth latency. Speed up the wrong layer and the pattern still holds; nothing the auth framework can do alone closes it.

Not the same as filesystem TOCTOU. Filesystem TOCTOU is about a path whose target the attacker controls between check and use; the privileged process trusts the path and the path lies. Here the path is a server-side slot the privileged process owns, and the question being raced is not "what does this name resolve to" but "did the caller ask for this exact thing". Different layer, different mitigations: file-descriptor handles and O_NOFOLLOW solve filesystem races and do nothing for this pattern, because the slot here is an in-memory object and the swap is an authenticated D-Bus call.

Defender playbook

Snapshot the parameters at auth-request time and bind the auth result to the snapshot. The auth response should answer "you may install /tmp/dummy.deb" not "you may complete transaction /2_cdaadeab". When consuming the result, re-validate that the current values match the snapshot the auth was taken against. If they have drifted, refuse to act and return an INVALID_STATE to the caller. The fix in PackageKit 1.3.5 is the cheap version of this: refuse all subsequent writes to the cache once auth has been requested.

Lock the slot from the moment auth is requested until the answer is bound. Refuse subsequent writes to the cached parameters while auth is in flight. The state machine should treat "auth pending" as a write-locked region; any second call from the same caller to mutate the slot returns an explicit error rather than silently overwriting. PackageKit's commit 76cfb675 does exactly this with one if-statement: any action method on a transaction past STATE_NEW returns PK_TRANSACTION_ERROR_INVALID_STATE.

Treat the auth grant as a token over a hash of the values, not over a slot id. If your auth framework supports it, sign the grant against a content-addressable identifier of the parameters. The downstream consumer verifies the hash matches what it is about to act on. OAuth's scope-locked token applies the same idea at the API layer: the grant says "this token may do these specific operations" and the resource server checks that what it is being asked to do matches the scope claim. Without content binding, the only thing keeping the grant honest is whatever lock you put on the slot.

Audit any framework that maintains a "pending" or "cached" parameter region during an external auth call. The pattern shows up anywhere a server holds caller-supplied state while it asks somebody else whether to act on it. List every such region in your service: D-Bus method handlers with cached args, REST endpoints that accept a PATCH while a request is "in review", multi-step workflows where step N writes to a slot that step N+1 will execute, signed-approval queues where the approver sees a summary and the executor reads the row. For each, ask: between the question and the answer, who can write?

Kinship

Trust Inversion. Trust Inversion names the case where the security artifact you trust becomes the attack surface. This pattern is what trust inversion looks like at the authorization layer specifically: polkit (or any auth framework) does its job correctly, and the attacker turns its grant into their primitive by ensuring the parameters polkit answered yes to are not the parameters the system ends up acting on. Same shape, different artifact: trust inversion at the credential layer; auth-pins-the-slot at the consent layer.

Nonce Is Not Auth. Both name a confusion about what an auth check actually proves. Nonce Is Not Auth says a valid CSRF nonce does not equal an authenticated caller; the developer mistook session continuity for authorization. Auth Pins The Slot Not The Value says a valid authorization grant on a slot does not equal authorization for the value in it now; the developer mistook auth result identity for value identity. Different confusions, same shape: the auth artifact answered a narrower question than the code is treating it as having answered.

TOCTOU That Isn't. TOCTOU That Isn't names the cases where "race condition" is the wrong label and the bug is actually a deterministic preemption. This pattern is what those cases sometimes get mislabeled as. CVE-2026-41651 IS a real race, but it is a race at the authorization layer (between an async auth call and a synchronous parameter write), not at the filesystem layer. The TOCTOU label drives operators toward atomic-filesystem-primitive fixes that do nothing here. The boundary between the two patterns is what kind of race is real, and how the fix has to be shaped: filesystem TOCTOU narrows windows; auth-pins-the-slot locks slots.

An authorization that survives a parameter swap was never an authorization for those parameters.