Security Engineering · Authorization

Reference Monitors and Complete Mediation in Access Control

Why an access check only means something if every access passes through it, how the policy decision point is separated from the enforcement point, and where check-then-use gaps appear.

Every authorization mechanism, from a nine-bit Unix mode to a cloud policy language with condition blocks, answers one question at runtime: may this subject perform this operation on this object? The framing is old and it has not improved. A 1972 study for the US Air Force by James P. Anderson described the abstract component that answers it as a reference monitor, and the properties he required of that component are still the right test for whether an enforcement mechanism is real or merely decorative.

The three properties that make a check meaningful

Anderson required the monitor to mediate every access, to be tamper-proof against the subjects it constrains, and to be small enough to be subjected to analysis. Each property rules out a large family of designs that look reasonable on a diagram.

Complete mediation means no path to the object exists that skips the check. Tamper resistance means the subject cannot alter the monitor, its policy store, or the path by which the request reaches it. Verifiability means the monitor’s logic is compact and isolated enough that its behaviour can be argued about formally; a security check spread across a hundred thousand lines of application code cannot be.

The three properties interact, and the interaction is where most real systems fail. A monitor that is complete but not tamper-proof is a suggestion. A monitor that is tamper-proof but incomplete protects a subset of the object and leaves the rest reachable through an unconsidered interface. Verifiability is the property most often abandoned first, because it constrains how much code the check is allowed to touch.

The canonical location for such a monitor is the kernel, because the kernel already sits between every process and every resource and is protected by the processor’s privilege level. This is why file permission checks survive even when the application is compromised: the check is executed by code the compromised process cannot write to. It is also why authorization logic placed in a client library or a front-end template offers no guarantee at all, regardless of how carefully the rules were written.

Separating the decision from the enforcement

A single component that both intercepts requests and evaluates policy grows without bound, because policy evaluation wants to consult attributes from directories, databases, and tokens. The standard decomposition splits it. The policy enforcement point (PEP) intercepts the request and is the only component that can allow or deny it. The policy decision point (PDP) evaluates policy and returns a decision. A policy information point supplies attributes, and a context handler translates the native request into the form the PDP understands.

The split has a cost that is easy to underestimate. Every PDP call is a network hop or an inter-process call on the critical path of the operation being authorized, so latency is now a function of the decision service’s availability. Systems that centralize decisions usually add a decision cache in front of the PDP, and that cache immediately becomes part of the security boundary: a cached permit outlives the policy change that revoked it. The revocation delay is the design parameter, not an implementation detail.

The split also determines what the PEP can even describe. A file-open hook knows a path, an inode, a process identity, and a requested access mode. A gateway PEP knows an HTTP method, a route, and a bearer token. If the policy needs an attribute the PEP cannot observe, the decision degrades to a guess or the architecture needs another enforcement point closer to the data.

Check and use are two different moments

The gap between the decision and the effect is the most persistent failure mode in access control. A program asks whether a path is writable, receives yes, and then opens the path. Between those two operations the path may resolve to a different object, because a directory component was replaced with a symbolic link or the target was swapped. The check was correct when it ran and irrelevant when it mattered.

The fix is to make the check and the use operate on the same object identity rather than on the same name. On Unix-like systems this means resolving the name once, under restrictions that prevent substitution, and then performing all subsequent operations on the resulting file descriptor. Descriptor-relative operations with a directory file descriptor, refusal to follow a final symbolic link, and resolution flags that confine lookups beneath a directory all exist for this purpose. The name is consulted once; the object is what gets used.

/* Resolve the name once, refuse a symlink at the final component,
   then confirm the object is the one the policy decision covered. */
int fd = openat(dirfd, "report.csv", O_RDONLY | O_NOFOLLOW | O_CLOEXEC);
if (fd < 0) return -1;

struct stat st;
if (fstat(fd, &st) == -1) { close(fd); return -1; }
if (st.st_dev != expected_dev || st.st_ino != expected_ino) {
    close(fd);           /* name now refers to something else */
    return -1;
}
/* Every later read uses fd, never the path. */

The same pattern appears at higher layers as identifier confusion: a check on a numeric object id followed by a lookup that accepts a different id, or a token validated for one audience and then accepted by a service expecting another. Wherever the thing checked and the thing used can diverge, the window between them is exploitable.

What incomplete mediation looks like in practice

The classic demonstration is the memory-mapped file. Permission to read a file is checked when the mapping is established, and the page faults that follow are satisfied from the page cache without reconsulting the file’s mode. Revoking read permission afterwards does not unmap pages already resident, and does not prevent faults on pages the mapping has not yet touched in every implementation. Any design that expects revocation to take effect immediately must account for every interface that captured a reference to the object earlier: open descriptors, mappings, delegated handles, cached attribute copies.

Deletion has a related shape. Removing a directory entry requires write and execute permission on the containing directory, not on the file. A user with write access to a directory can therefore unlink a file they cannot read, which is why world-writable directories carry the sticky bit: it restricts deletion to the file’s owner, the directory’s owner, or a privileged process. Without it, a shared temporary directory is a denial-of-service surface rather than a sharing surface.

Default-allow is the other silent failure. A monitor that is asked about an object with no matching rule must deny; implementations that fall through to allow turn every policy omission into a hole. The mirror error is treating a failed policy fetch as a permit. Both mistakes are availability decisions smuggled into a security path.

Where the boundary is drawn

The monitor’s position decides what it can protect and what it must trust. Enforcement inside the kernel covers every process but knows nothing about application-level meaning: it can distinguish read from write, not “read this record because the requester is the patient’s physician”. Enforcement inside the service knows the semantics but covers only requests that arrive through that service, and only until the service itself is compromised.

Practical systems therefore layer the two, and the layering is only as strong as its weakest mediated path. A database that applies row filters for queries arriving through the application, while accepting direct connections from the same credentials, has not enforced anything. The enumeration of paths is the security argument; each additional path is an additional assumption that must be written down. Later articles in this cluster take up the specific mechanisms that occupy each layer, from POSIX mode bits and ACL masks at the bottom through object capabilities and the confused deputy at the delegation boundary.

The alternative to a shared monitor is per-application enforcement, and its appeal is real: each service knows its own semantics, so the checks can be precise and the policy can live next to the code it governs. What it loses is the completeness property. Six services that each check their own access control are six independent implementations of the same invariant, and the invariant holds only if all six are correct simultaneously. A shared monitor is one place to be correct and one place to audit, at the price of a shared failure: a bug in the monitor is a bug in every check. The choice between the two is a choice about where to concentrate risk, and centralized enforcement concentrates it in a component that can be reviewed and tested as one unit.

References

  1. Computer Security Technology Planning Study (ESD-TR-73-51) — James P. Anderson, US Air Force Electronic Systems Division, 1972
  2. The Protection of Information in Computer Systems — Saltzer & Schroeder, Proceedings of the IEEE, 1975
  3. Security Engineering: A Guide to Building Dependable Distributed Systems, 3rd edition — Ross Anderson, Wiley, 2020