authentik wrapper: core/applications pagination block asserts count=28/total_pages=1 while delivering 7 — policy-suppressed results under a SUPERUSER token; app-list.sh affected #963

Open
opened 2026-07-30 20:25:00 +00:00 by Ghost · 0 comments

Found during a P0 login diagnosis. The worst member yet of the silently-incomplete-listing family: this one actively contradicts itself, and the false half is the half that looks authoritative.

Observed

GET core/applications/ returned seven results under a pagination block reading count: 28, total_pages: 1, end_index: 28. The block asserts a complete single page of 28 and delivers 7.

Mechanism

The suppression is policy-based: results are filtered to objects whose access policies admit the caller — so what is hidden is exactly the set of objects whose bindings exclude you, and it applied to a token resolving to is_superuser: true. Passing superuser_full_list=true returns all 28.

tools/authentik/app-list.sh does not pass that parameter and is affected.

Why this is worse than the siblings

Every prior family member either capped silently or kept its count honest. This one lies in both directions at once: the metadata says complete, the payload is filtered, and nothing marks the difference. During the incident diagnosis, a diff built on the default call would have reported three missing applications on a P0 login incident — a manufactured finding pointing at exactly the objects most likely to look suspicious (the ones with restrictive policies).

A count field that disagrees with its own payload is not metadata; it is testimony, and it testifies falsely.

Fix

  1. app-list.sh (and any sibling wrapper listing policy-gated objects) passes superuser_full_list=true when the token is superuser, or compares len(results) against pagination.count and FAILS LOUDLY on mismatch instead of returning the subset.
  2. The mismatch check is the general fix — it protects non-superuser callers too, for whom the parameter is not available: a wrapper must never return a result set that its own metadata says is incomplete without saying so.

Acceptance

  • Superuser call returns all objects, verified against pagination.count.
  • A deliberately policy-restricted caller produces a loud partial-result indication, not a silent subset.
  • Negative control: the mismatch detector must NOT fire when count == len(results).
Found during a P0 login diagnosis. **The worst member yet of the silently-incomplete-listing family: this one actively contradicts itself, and the false half is the half that looks authoritative.** ## Observed `GET core/applications/` returned **seven** results under a pagination block reading `count: 28, total_pages: 1, end_index: 28`. The block asserts a complete single page of 28 and delivers 7. ## Mechanism The suppression is **policy-based**: results are filtered to objects whose access policies admit the caller — so what is hidden is exactly the set of objects whose bindings exclude you, **and it applied to a token resolving to `is_superuser: true`**. Passing `superuser_full_list=true` returns all 28. `tools/authentik/app-list.sh` does **not** pass that parameter and is affected. ## Why this is worse than the siblings Every prior family member either capped silently or kept its count honest. This one **lies in both directions at once**: the metadata says complete, the payload is filtered, and nothing marks the difference. During the incident diagnosis, a diff built on the default call would have reported **three missing applications on a P0 login incident** — a manufactured finding pointing at exactly the objects most likely to look suspicious (the ones with restrictive policies). > A count field that disagrees with its own payload is not metadata; it is testimony, and it testifies falsely. ## Fix 1. `app-list.sh` (and any sibling wrapper listing policy-gated objects) passes `superuser_full_list=true` when the token is superuser, **or** compares `len(results)` against `pagination.count` and FAILS LOUDLY on mismatch instead of returning the subset. 2. The mismatch check is the general fix — it protects non-superuser callers too, for whom the parameter is not available: *a wrapper must never return a result set that its own metadata says is incomplete without saying so.* ## Acceptance - Superuser call returns all objects, verified against `pagination.count`. - A deliberately policy-restricted caller produces a **loud** partial-result indication, not a silent subset. - Negative control: the mismatch detector must NOT fire when count == len(results).
Sign in to join this conversation.
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: mosaicstack/stack#963