jwed 9cbc8e682a SECURITY: the guest Wi-Fi card poll had no authorization at all
GET /app/unifi/guest-card/data carried `web,auth` and nothing else, and
GuestCardController::data() checked nobody. So ANY signed-in account could fetch the
live guest Wi-Fi password by requesting the URL — on this dev database that is 663
students and 418 parents, none of whom can see the card in the UI.

The route comment asserted the protection that did not exist:

    // ── Guest Wi-Fi home-card poll (any logged-in user; card visibility is
    // grant-gated by the shell). …

Card visibility IS grant-gated, but that gate is PRESENTATION only — DashboardController
decides which cards to render for a user; it has never constrained the endpoints those
cards poll. So the comment described the card, and the endpoint was open.

Now enforced server-side: you may poll it iff you may see the card
(DashboardCardGrant::accessibleCardKeysFor), with the usual super-admin pass. Kept as
the card gate rather than a permission so the widget stays grantable to non-unifi staff,
which is the whole point of a home-page card.

Verified: a non-super user holding only the MyDocuments card is refused for
unifi.guest_wifi, and an explicit grant admits the same user.

Found while auditing whether the bespoke page-grant middleware could be deleted. This
route sits inside that middleware's prefix match, so deleting it would have removed the
last non-auth check on the endpoint — it needed closing first either way.

Two things nearby that are NOT fixed here and want their own look:
  • unifi.portal.callback (`auth` only, no permission) checks only that somebody is
    logged in.
  • unifi.api.wifi.current-password is token-protected via hash_equals, which is fine,
    but it is the same secret behind a different door — worth confirming that token's
    rotation story.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RDA3fQbsa3gaHCFoV1Q8Gg
2026-07-28 15:03:53 -04:00
Description
UniFi snap-in package for the Dashboard shell
578 KiB
Languages
PHP 100%