Access control becomes unreliable when the application authorizes one representation of a resource but later operates on another. A file may be reachable through aliases, an object may have both a public name and an internal ID, or a path may have several textual forms that resolve to the same target. If different parts of the request pipeline disagree about identity, an authorization check can answer the wrong question.
The defensive rule is to establish a canonical resource identity before making the security decision. Canonical means the single representation that the application treats as authoritative for identifying that resource. Resolve untrusted names or aliases to that identity, authorize the identity, and make the protected operation use the same resolved object.
This article explains why that ordering matters, what it protects against, and where canonicalization itself can create mistakes.
Authorization is a relationship between a principal and a resource
A useful authorization question is concrete:
May principal 42 read document 731?The decision concerns a principal, an action, and one resource. The URL or user-supplied name used to locate that resource is only an input to the decision.
Problems begin when an application treats the locator as if it were the resource identity:
request path -> authorize path -> resolve path -> open objectSuppose two accepted locators resolve to the same object. A rule attached to one textual form may not apply to the other. The application can then produce different authorization results for the same underlying resource.
A safer model is:
request locator
|
v
resolve under defined rules
|
v
canonical resource identity
|
+----> authorize
|
+----> operate on the same resourceThe important property is agreement. The authorization layer and the operation layer must refer to the same object.
See the problem with a small example
Imagine an application stores reports with immutable internal IDs but also supports human-friendly aliases:
/teams/finance/reports/quarterly -> report_id 731
/reports/731 -> report_id 731Assume access policy is stored against report_id. Checking whether a user may access the literal string /teams/finance/reports/quarterly would create a second identity system that policy does not understand.
Instead, route handling can conceptually do this:
alias or ID -> resolve report -> report_id 731
|
v
authorize(user, "read", report_id 731)
|
allow
|
v
read the already-resolved reportThis example is intentionally framework-neutral. A production implementation should use the framework and storage layer’s normal object-loading and authorization mechanisms rather than copying this pseudocode literally.
The same mental model applies to many resources: account aliases, case-insensitive names, storage keys, hierarchical paths, renamed objects, and compatibility identifiers.
Resolve first, but do not trust the resolver blindly
Canonicalization is part of the security boundary. Resolving an identifier is not permission to use the resulting object.
The resolver should answer a narrow question: which resource does this accepted identifier denote under the application’s rules? Authorization then answers whether the authenticated principal may perform the requested action on that resource.
Keep those questions separate:
resolution: "What object is this?"
authorization: "May this principal do this to that object?"A successful lookup must not imply access. Conversely, authorization should not guess resource identity from an untrusted string when the operation will later perform a more authoritative lookup.
Resolution rules also need to match the component that ultimately uses the resource. If one layer treats names as case-sensitive while another does not, or one normalizes a path differently from another, the application can reintroduce the identity disagreement it was trying to remove.
Avoid check-then-resolve gaps
A subtle failure occurs when the application authorizes a resolved resource and then resolves the original locator again before use.
Conceptually:
resource = resolve(locator)
authorize(resource.id)
# later
resource = resolve(locator) # second lookup
perform(resource)If aliases or mappings can change between the two lookups, the second resolution may produce a different object. The check was correct for the first object but the operation acts on the second.
Prefer carrying forward the resolved stable identity or object reference that was authorized:
resource = resolve(locator)
authorize(resource.id)
perform(resource)Whether an object reference remains stable depends on the application and storage system. In systems with concurrent changes, the protected operation may need a transaction, version check, conditional update, or another mechanism that preserves the relevant identity and authorization assumptions until use.
Canonicalization does not by itself solve time-of-check/time-of-use races. It gives the application a precise identity on which stronger concurrency controls can operate.
Define what happens to aliases and renames
Aliases are convenient, but their lifecycle affects authorization reasoning.
Suppose project-alpha once refers to resource 100 and is later reassigned to resource 205. A bookmarked URL now identifies a different resource. That may be acceptable for a navigation feature, but code must not treat historical possession of the alias as evidence of authorization.
Policy should normally follow the stable resource identity, not the mutable alias. After a rename or reassignment, a request using the alias is resolved again and authorization is evaluated for whichever resource it currently denotes.
If an alias itself carries security meaning, model that meaning explicitly rather than relying on its spelling. For example, a reserved administrative namespace may need rules during resolution as well as object-level authorization. Do not assume canonical object authorization automatically enforces constraints on every way an object may be named.
Canonicalization is not string cleanup
It is tempting to treat canonicalization as a sequence of generic string operations such as lowercasing, trimming, decoding, or collapsing separators. That is risky because valid normalization rules depend on the identifier domain.
An email-like identifier, a filesystem path, a URL, a Unicode username, and a database object ID do not share one universal canonicalization algorithm. Applying the wrong transformation can merge identifiers that should remain distinct or leave equivalent identifiers separate.
Use the semantics of the authoritative subsystem. If the database owns immutable resource IDs, resolve accepted external names to those IDs. If a storage API defines path resolution, use its documented semantics and keep authorization aligned with the resolved storage object. Avoid inventing a parallel normalizer unless the application genuinely owns the identifier format and its equivalence rules are precisely defined.
This distinction matters because over-normalization can become an authorization bug too. If two legitimate resources are incorrectly collapsed into one canonical identity, policy for one may be applied to the other.
State the threat model precisely
Canonical resource authorization reduces risk when an attacker can choose among multiple accepted representations of a protected resource and different representations might otherwise receive inconsistent access-control decisions. It also reduces accidental policy gaps caused by aliases, renames, or duplicated identifier handling.
It does not protect against a policy that grants excessive access, a compromised privileged account, an insecure object resolver, or a protected operation that bypasses authorization entirely. It also does not replace validation of identifier syntax, tenant boundaries, or safe handling by downstream components.
For multi-tenant systems, canonical identity should include enough context to prevent ambiguity. A bare object number may not be globally meaningful if each tenant has its own numbering space. The security-relevant identity might therefore be (tenant_id, object_id) rather than object_id alone.
The correct identity is the one that uniquely denotes the authorization resource inside the trust boundary where policy is evaluated.
Keep error handling from becoming a side door
Resolution can fail because an identifier is malformed, missing, stale, or no longer maps to a resource. Authorization can fail because the principal lacks permission. Those outcomes may be handled differently internally, but external error behavior should be chosen deliberately.
For some applications, revealing that a resource exists is harmless. For others, returning noticeably different responses for “exists but forbidden” and “does not exist” can disclose sensitive resource membership. Canonicalization does not dictate one universal HTTP status or message. It simply makes the internal states clear enough for the application to choose an exposure policy consciously.
Do not respond to resolution uncertainty by skipping authorization. If the application cannot establish which protected resource a request denotes, it cannot establish permission for that resource either.
Verify the invariant with tests
Tests should use equivalent and near-equivalent identifiers, not only the preferred form.
Create a resource with two supported aliases and confirm both resolve to the same canonical identity and receive the same object-level authorization decision. Rename an alias and verify policy follows the intended stable object. Attempt access through stale or malformed aliases and confirm no fallback bypasses authorization.
Where concurrent alias changes are possible, test that an authorized lookup cannot be switched to a different target before execution. For multi-tenant identifiers, verify that the same local object number in two tenants remains two distinct authorization resources.
A particularly useful code-review question is: What exact resource identity was authorized, and can the operation later act on anything else? If the answer is unclear, the request path probably needs a tighter boundary.
Conclusion
Authorization is only as precise as the resource identity it evaluates. When a protected object has multiple names, paths, aliases, or textual representations, resolve them according to authoritative rules before making the access-control decision.
Then keep the decision and operation bound to that same canonical resource. Do not let a later lookup reinterpret the original input, do not confuse successful resolution with permission, and do not invent generic normalization rules for identifier domains you do not control.
The practical goal is simple: however a caller names a resource, every accepted name should converge on one security identity before authorization decides what the caller may do.