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
This commit is contained in:
@@ -4,6 +4,7 @@ namespace Dashboard\Unifi\Http\Controllers;
|
||||
|
||||
use App\Models\Setting;
|
||||
use App\Models\User;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Routing\Controller;
|
||||
|
||||
/**
|
||||
@@ -44,9 +45,35 @@ class GuestCardController extends Controller
|
||||
];
|
||||
}
|
||||
|
||||
/** Live poll for the card (any logged-in user who can see the card). */
|
||||
public function data()
|
||||
/** The dashboard card this endpoint feeds — must match UnifiServiceProvider. */
|
||||
private const CARD_KEY = 'unifi.guest_wifi';
|
||||
|
||||
/**
|
||||
* SECURITY: authorize against the card's own visibility.
|
||||
*
|
||||
* This was `web,auth` only, and the route comment claimed "card visibility is
|
||||
* grant-gated by the shell" — but that gate is PRESENTATION only
|
||||
* (DashboardController decides which cards to render). The endpoint itself was
|
||||
* unchecked, so any signed-in account — every student and parent included —
|
||||
* could fetch the live guest Wi-Fi password by requesting the URL directly.
|
||||
*
|
||||
* The endpoint now enforces what the comment always claimed: you may poll it iff
|
||||
* you may see the card. Super admins pass, as everywhere else. Kept as the card
|
||||
* gate rather than a permission so the widget stays grantable to non-unifi staff,
|
||||
* which is the point of a home-page card.
|
||||
*/
|
||||
public function data(Request $request)
|
||||
{
|
||||
$user = $request->user();
|
||||
abort_unless($user, 403);
|
||||
|
||||
if (! $user->is_super_admin) {
|
||||
$allowed = \App\Models\DashboardCardGrant::accessibleCardKeysFor($user)
|
||||
->contains(self::CARD_KEY);
|
||||
|
||||
abort_unless($allowed, 403);
|
||||
}
|
||||
|
||||
return response()->json(self::payload() ?? ['title' => null]);
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user