SOFTWARE / SYSTEMS / AIEngineering news. Technical depth.
Architecture / 4 MIN READ

Close the authorization race between permission checks and writes

How to prevent TOCTOU authorization races where ownership, membership, or resource moves invalidate a permission check before a write commits.

A permission check is only meaningful for the state it examined. If the application checks access, releases control, and later writes, a concurrent ownership transfer, group removal, or a resource move can invalidate the decision. The safe design is to bind the authorization facts and the mutation into one atomic commit, or make the write conditional on versions of every fact that mattered.

See the race as a state transition

The classic time-of-check/time-of-use weakness occurs when resource state changes between a check and its use, as CWE-367 explains. In application authorization, the shared resource is often several rows rather than a file.

Suppose Alice may edit document D because D belongs to workspace W and Alice is an editor in W. A request reads both facts and receives allow. Before its later update, another transaction moves D to workspace X or removes Alice from W. The update still succeeds if it only matches document_id. Nothing is wrong with the policy engine; the application used a correct answer after its premises expired.

Make this failure observable in tests. Pause a request after its decision, commit the ownership change in a second connection, then resume the first request. Repeat for group removal and resource movement. A test that merely launches concurrent operations without controlling their order may never exercise the dangerous interleaving.

Prefer one transactional predicate

When authorization data and the protected object share a transactional database, define a shared synchronization protocol for protected writes and permission changes. A join or EXISTS predicate can filter an update against its statement snapshot, but does not by itself lock membership against concurrent revocation. Serialize both paths on the relevant relationship rows or a shared authorization-version row, and hold that boundary through the protected commit. Check the affected-row count: zero means the object disappeared or a required predicate failed. Return a generic conflict or denial according to the product contract, then log a stable reason code internally.

A second pattern reads and locks the relevant object and relationship rows, evaluates the policy, then writes before committing. The lock must cover every mutable premise. Locking the document while membership remains independently mutable leaves a smaller but real race. Keep the transaction short and acquire locks in a fixed order to control contention and deadlocks.

Carry a consistency boundary across services

A separate authorization service cannot participate in every application database transaction. In that design, use a consistency mechanism so authorization decisions are evaluated against a snapshot fresh enough to avoid stale results after relationship changes. The Zanzibar paper describes authorization decisions that respect causal ordering of user actions and provide external consistency amid changes to access control lists and object contents.

A fresh decision still needs a commit boundary. For example, WHERE id='D' AND version=41 detects a resource move only if that move increments the version; it does not detect membership-only revocation. Every mutable permission premise needs a shared version or synchronization protocol that the mutation and revocation paths both enforce. A remote freshness token alone cannot atomically bind an unrelated database write. If the system cannot enforce that boundary, document the weaker revocation window instead of claiming the race is closed. For high-impact writes, treat missing preconditions as failure rather than falling back to a cached allow.

Use compensation only when atomicity is impossible

Some writes trigger external effects that cannot share a transaction. Split the operation into intent and execution. First store an authorized, version-bound intent. Before sending money, exporting a secret, or scheduling a destructive job, reauthorize against current state and mark the intent consumed atomically. Compensation can repair reversible side effects, but it cannot reliably retract disclosed data or an email already sent.

Review each sensitive write with four questions:

  • Which resource and relationship facts produced the allow?
  • Can each fact change before commit?
  • What transaction, lock, or version precondition binds the decision to the mutation?
  • What does the caller see when that precondition fails?

Start with one ownership-transfer path this week. Add the paused two-connection test, make the update conditional, and verify that the stale request changes zero rows. That small test exposes the architectural boundary more clearly than a broad policy review.

SOURCES & REVIEW

Sources are linked throughout this guide. Product capabilities can change; consult the linked documentation for your deployment.

Read our editorial approach ↗