# Principal type confusion in permission checks

Untyped GetID() shared by user and link-share principals, compared against owner/tenant columns without a type guard

_Area: Web access control and IDOR. Seen in 2 disclosed reports; latest 2026-10-09. Updated 2026-10-24._

## How it works
An auth layer exposes one identity accessor (e.g. `GetID()`) satisfied by several principal kinds — real users and link shares. The accessor returns a bare integer drawn from the same autoincrement space as `users.id`. Permission code then compares that integer to an owner/tenant column, or writes it into one, without asserting which principal type produced it. A link-share ID that happens to equal a victim's user ID is treated as that user. The broken assumption: "an ID identifies a principal unambiguously." It does not when IDs are shared across principal kinds.

## Where it shows up
- A one-method auth interface (`GetID() int64`) implemented by both user and share/link types.
- Share IDs returned as raw positive values in the same sequence as user IDs.
- Handlers assigning `auth.GetID()` directly to `owner_id`/`user_id` on create, list, or delete.
- Permission methods with a fast-path self-check that returns before any type assertion.
- A safe negated-ID helper that exists but is unused.
- Authorization keyed on a local DB row rather than the external resource identity [E2](https://bugbountylibrary.com/r/ghsa%3AGHSA-pxmc-2ffp-8j67.md).

## How to review
Grep every `GetID()`/ID comparison and every write into an owner/tenant column. The invariant: before an ID is compared to, or stored as, a user/tenant owner, the code must prove the principal is of the expected type (explicit type assertion or a typed accessor). Confirm permission sinks (create/list/delete, self-removal, roster reads) each carry that guard, and that no early return bypasses it [E1](https://bugbountylibrary.com/r/ghsa%3AGHSA-vvcv-vpph-h844.md)[E3](https://bugbountylibrary.com/r/ghsa%3AGHSA-32r8-5843-4qw2.md). For external resources, check that authorization binds to the resource identity, not a mutable local row, and that no import path can mint a second row aliasing the same external object [E2](https://bugbountylibrary.com/r/ghsa%3AGHSA-pxmc-2ffp-8j67.md).

## Variants seen
- Untyped `GetID()` written straight into `owner_id` on API-token create/list/delete, with `CanCreate` always true [E1](https://bugbountylibrary.com/r/ghsa%3AGHSA-vvcv-vpph-h844.md).
- Same confusion across three permission sinks: team-member self-removal, bot deletion, roster disclosure [E3](https://bugbountylibrary.com/r/ghsa%3AGHSA-32r8-5843-4qw2.md).
- Ownership check on a DB row while upload allows aliasing the victim's CA-side certificate [E2](https://bugbountylibrary.com/r/ghsa%3AGHSA-pxmc-2ffp-8j67.md).

## Why fixes fail
A safe negated-ID helper existed but was unused, and a fast-path self-check returned before the only type assertion — so the guard was present in the codebase yet not on the vulnerable path [E3](https://bugbountylibrary.com/r/ghsa%3AGHSA-32r8-5843-4qw2.md).

## Impact and severity
Cross-account token issuance and management [E1](https://bugbountylibrary.com/r/ghsa%3AGHSA-vvcv-vpph-h844.md), cross-account team removal, bot takeover and roster disclosure [E3](https://bugbountylibrary.com/r/ghsa%3AGHSA-32r8-5843-4qw2.md), and revocation of real production certificates at the CA [E2](https://bugbountylibrary.com/r/ghsa%3AGHSA-pxmc-2ffp-8j67.md). High severity: any ordinary authenticated user reaches other accounts. Worse where IDs are guessable sequential integers and where a single untyped accessor is reused across many handlers.

## Evidence
- E1: https://bugbountylibrary.com/r/ghsa%3AGHSA-vvcv-vpph-h844.md
- E2: https://bugbountylibrary.com/r/ghsa%3AGHSA-pxmc-2ffp-8j67.md
- E3: https://bugbountylibrary.com/r/ghsa%3AGHSA-32r8-5843-4qw2.md
