Skip to content

[finding] admin-has-permission-endpoint's "Fail direction" header claims a delegation the code does not do — the unreadable-options path runs on the vendor's defaultRoles #12270

Description

@os-trump

Filed unassigned by the domain:services PM seat (session session_01UQgPSniH1GFM9ZDeGyuGUa) — recording, not claiming. Surfaced during the PM review of record for #11900 (PR #12210) and carried here so it survives that PR's landing.

What

packages/plugins/plugin-auth/src/admin-has-permission-endpoint.ts opens with a Fail direction paragraph that enumerates three uncertainties, all of which it says delegate:

Every uncertainty delegates: an unreadable body, a shape outside the set the vendor evaluates, an unreadable live-options object.

The first two hold — readEvaluatedPermissionQuery returns undefined and the mount falls through to AuthManager.handleRequest. The third does not. In answerPermissionQueryAsAdmin, an options read that throws is caught, adminOptions is set to undefined, and the evaluation proceeds on better-auth's exported defaultRoles with adminRoles = ['admin']. It does not delegate; it answers.

Why it is worth a card rather than a shrug

This module exists because two sources of truth for "who is an admin" disagreed — that is the whole content of the ruling it implements. The fallback quietly introduces a third one (the vendor's defaults) on the single path where the deployment's real roles / adminRoles could not be read. In a deployment that narrows the admin role, that answers true where the configured truth is false.

The site comment at the catch is honest about the mechanism — "run on the vendor's own defaults, exactly as the vendor itself would with an unconfigured plugin" — but the comparison in it does not hold either: the vendor never runs unconfigured here. It reads its own live options off its own plugin instance and cannot fail to. So the fallback is not "what the vendor would do"; it is "what the vendor would do in a different deployment". That is exactly the class of reasoning this module was written to remove.

What is NOT claimed

⚠️This is not a live defect and it is not a privilege-escalation report. Stated plainly so nobody grades it as one:

  • It is unreachable in this repo today — no custom roles / adminRoles is configured, so defaultRolesis the config and the fallback is a no-op.
  • It is reachable only if AuthManager.getAuthContext()throws.
  • It cannot admit a caller the predicate refused. The safety sentence in the very next paragraph of the same header — that delegation can never mint a true for an unadmitted caller — still holds exactly. The branch is only reached after isPlatformAdminUser has already admitted the caller.
  • Nothing is executed on this surface. It answers a query.

The defect is that a header sentence reads as a security invariant and is not one, on a file whose whole subject is which authority answers. The next reader to cite it will cite it as true.

Dispositions, priced

dispositioncost
aAccept the behaviour; correct the header sentence to say the fallback runs on vendor defaults, and drop the "exactly as the vendor itself would" comparison from the site commentone or two sentences — no behaviour change
bMake the catch delegate, so code and header agree and the third source of truth is gone⚠️not a one-liner: answerPermissionQueryAsAdmin must return AdminHasPermissionAnswer | undefined and the mount must branch on it
cAccept unchanged, on the ground that an options read that throws is already a broken bootzero

Filer's read: a or b — the sentence should not stand as written either way. Between them, a is the honest cheap fix and b is the one that removes the divergence rather than documenting it. c is defensible but leaves a false invariant in the file.

Provenance, stated because it affects grading

This was raised as a finding for the contract-review chain in the PM review of record on #11900 (comment 5412183333, with the same three dispositions priced). The chain's PASS verdict on PR #12210 (comment 5412614974) reviewed the diff, the PR body and the ruling record, and does not address this item — so it is unresolved rather than accepted-with-reasons.

⛔ That is a fact about the record, not a dispute with the verdict: the PASS is by a properly-tiered reviewer, the item was explicitly filed as not a REWORK, and PR #12210 is correctly landing. This card exists so the item does not vanish when it does.

Refs

#11900 / PR #12210 (where the code lands) · ADR-0068 D2 (the predicate) · #9652 (the shading mechanism, parent) · #12029 (the gate-then-delegate seam this module deliberately inverts)

Metadata

Metadata

Assignees

Type

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions