Skip to main content

Access Control

Authentication answers who is calling. Authorization answers what they may touch. EDH's zero-trust claim is an authorization claim: it says no party — not even the operator — can reach data that is not theirs. That holds only if every endpoint, contract function, and storage path enforces it. A single resource that authorizes by an unchecked identifier is enough to expose the whole dataset behind it.

The governing principle for this entire page: a resource identifier supplied by the caller (a path parameter, a query field, a request body key, a storage prefix) is an input to an authorization decision, never a substitute for one.

API endpoints must authorize per resource, not merely authenticate

AC-1 (MUST). Every endpoint that reads or mutates account-scoped data MUST both authenticate the caller and authorize the specific resource against the caller's identity. When a resource is addressed by a caller-supplied identifier (e.g. a user id, record id, or booking id in the path or query), the implementation MUST verify that the resource belongs to — or is shared with — the authenticated caller before acting. Deriving the owner from the session (e.g. "current user") is preferred; where an identifier must be accepted, an explicit ownership/role check is required.

Rationale. This is the IDOR (insecure direct object reference) class. With authentication but no per-resource check, any holder of a valid session reads or overwrites any other user's records by changing an integer in the path. For health data, that is mass PHI disclosure and silent tampering with one request. Sequential or guessable identifiers make it trivial; non-guessable identifiers reduce but do not remove the exposure and MUST NOT be relied on as the control.

Conformance check. As user A, request and modify a resource owned by user B by its identifier. Both MUST be denied.

Authorization must be enforced by default (deny-by-default)

AC-2 (MUST). Endpoints MUST be access-controlled by default. An endpoint becomes public only through an explicit, reviewable opt-out, never through the absence of a guard. Intentionally public endpoints MUST be documented as such and MUST NOT return account-scoped or PHI data, nor act as an unauthenticated oracle that confirms secret-derived values.

Rationale. When "no guard" means "open", a forgotten annotation silently publishes an endpoint. A global deny-by-default posture with explicit allowances converts that silent failure into a visible decision. Endpoints that confirm whether a supplied hash/value matches an active record leak one bit per query and SHOULD be authenticated or rate-limited even when they return "no data".

Encrypted-relay endpoints still require authorization

AC-3 (MUST). Storage endpoints that act as a "blind relay" of client-encrypted content MUST still authenticate and authorize writes, deletes, and listings. Client-side encryption protects confidentiality of content; it does not protect integrity, availability, or metadata.

Rationale. "The server only sees ciphertext, so the endpoint can be open" is a false inference. An unauthenticated write/overwrite lets an attacker corrupt or replace a user's documents (integrity) or exhaust storage (availability); an unauthenticated listing enumerates which records exist for an address, and headers/metadata (categories, sizes, timestamps) leak structure even when the bytes are opaque. Confidentiality is one of several properties; the others need access control regardless.

Storage namespaces must be server-enforced and non-overridable

AC-4 (MUST). Tenant/namespace isolation MUST be enforced on the server from the authenticated principal and MUST NOT be derived from, or overridable by, a client-supplied value. Where a client may supply a target path, the server MUST constrain it to the principal's namespace (prefix-enforce), not fall back to it only when the client omits one.

Rationale. A common defeat is target = clientSupplied OR derivedNamespace: because the client can always supply a value, the derived namespace never applies and the client writes anywhere. The same applies to issuing pre-signed URLs — a pre-signed URL minted for an arbitrary object key grants cross-tenant read/write outside any namespace check. Namespace scoping that is set in one code path but ignored in the service it calls is equivalent to no scoping; the constraint MUST be applied at the point of effect.

Conformance check. As a principal scoped to namespace A, attempt to write to, list, or mint a pre-signed URL for a key under namespace B, both by supplying an explicit out-of-namespace path and by relying on a defaulted one. All MUST be denied or rewritten into A.

Smart contracts must bind state changes to an authorized caller

AC-5 (MUST). A contract function that writes per-account state MUST establish that the caller is authorized for that account — by requiring msg.sender to equal the account, by verifying a signature/authorization for it, or via a trusted forwarder context. An externally callable function MUST NOT accept an arbitrary account argument from an unauthenticated caller and write under it.

Rationale. On-chain calls do not arrive through a trusted UI; any address can call any external function directly. A seed/key-storage or registry function that takes account as a parameter without binding it to the caller lets anyone append or rotate entries under another user's account — polluting recovery state or injecting attacker-controlled key references into a flow another device later trusts. Ownable on the contract does not help if the sensitive function is not actually restricted to an authorized party.

Meta-transactions must resolve the real sender

AC-6 (MUST). When using meta-transactions (e.g. EIP-2771), every target contract that derives authorization from the sender MUST implement the corresponding context (ERC2771Context) so that _msgSender() resolves to the original signer rather than the relayer. A forwarder that appends the signer MUST NOT be paired with targets that read msg.sender directly for authorization.

Rationale. If the forwarder appends the signer but the target reads msg.sender, every authorized action is attributed to the relayer, not the user — silently breaking the authorization model (and, depending on the contract, granting the relayer the user's rights or rejecting legitimate users).

AC-7 (MUST). A wallet/extension that exposes privileged actions (key storage, signing, connection or permission approval, vault deletion) MUST authenticate the origin/sender of each message and MUST NOT route page-originated messages into privileged internal handlers. Privileged actions MUST require that the message originated from the extension's own trusted UI (verified sender identity), and consent-granting actions MUST reflect a real user decision.

Rationale. Content scripts relay messages between an untrusted page and the trusted background. If the background dispatches any received message to its internal handler without checking the sender, a malicious page the user merely visits can call privileged actions directly — self-approving its own connection, reading or overwriting the vault, or requesting signatures — bypassing the consent UI the security model depends on. Allowing externally-connectable origins (including loopback) without validating the sender extends the same exposure to any matched origin. A consent step that resolves without a real user action (or a signing path that returns a placeholder instead of a real signature) provides the appearance of approval without its substance.

Conformance check. From an unaffiliated origin (and from a page via the content-script relay), invoke each privileged action. All MUST be rejected as coming from an untrusted sender.

Summary

IDRequirementPrevents
AC-1Per-resource ownership check, not just authenticationIDOR / cross-account PHI read & tamper
AC-2Deny-by-default; public is an explicit, documented opt-outSilently exposed endpoints; oracle leaks
AC-3Authorize writes/lists even on encrypted relaysIntegrity, availability, metadata exposure
AC-4Server-enforced, non-overridable namespacesCross-tenant read/write; pre-signed-URL escape
AC-5Bind contract state changes to an authorized callerCross-account on-chain writes
AC-6Targets implement ERC2771ContextSender misattribution under meta-tx
AC-7Authenticate sender; require real consentWeb-page-driven wallet takeover