Skip to content

fix(admin-edit-comms): run not-found guard before permission check (#1410) - #1415

Merged
rumblefrog merged 1 commit into
mainfrom
fix/1410-edit-comms-check-order
May 18, 2026
Merged

rumblefrog merged 1 commit into
mainfrom
fix/1410-edit-comms-check-order

Conversation

@rumblefrog

Copy link
Copy Markdown
Member

Description

Swaps the order of two guards in web/pages/admin.edit.comms.php. The not-found check now runs before the permission check, so a request for a non-existent comm-block cid surfaces the correct "There was an error getting details. Maybe the block has been deleted?" toast instead of the misleading "You don't have access to this!" toast.

Net change is a 34-line relocation of the existing if (!$res) { Toast::emit(...); PageDie(); } block (plus the $pagelink it depends on) from its previous spot ~130 lines below the SELECT to immediately after the SELECT, ahead of the perm check. Same shape, just earlier in the file. The comment above the moved block documents the new ordering rationale.

Motivation and Context

Closes #1410. Pre-fix the perm check at line ~42 read $res['aid'] / $res['gid'] BEFORE the if (!$res) not-found guard 130 lines down. On a bad cid, $res was false and $res['aid'] evaluated to null (PHP 8.x emits E_WARNING: Trying to access array offset on value of type bool; PHP 9 will turn the warning into a TypeError). The null != $userbank->GetAid() comparison then tripped the deny-access branch, so the user got the wrong toast AND the perm-flavored body worked as a side-channel for "does this cid exist?" enumeration.

Surfaced by the #1403 toast-lift adversarial review.

Sister admin.edit.ban.php was already correctly ordered (its if (!$res) at line 82 runs before $canEditBan = HasAccess(...) || ... at line 87) — the bug was localised to admin.edit.comms.php. Neither branch of the comms file logged to :prefix_log before the swap; that contract is unchanged (the issue body's "both branches call Log::add(LogType::Warning, ...)" claim was inaccurate, but the swap preserves whatever each branch did, which is nothing in this case).

How Has This Been Tested?

  • ./sbpp.sh phpstan — clean
  • ./sbpp.sh test --filter=AdminEditCommsCheckOrder — 4 new tests, 18 assertions, all pass
  • ./sbpp.sh test — full suite green (693 tests, 2764 assertions)
  • ./sbpp.sh ts-check — clean
  • ./sbpp.sh composer api-contract — no diff

The new AdminEditCommsCheckOrderTest.php pins four static-shape contracts against the source file:

  1. Order of guards — the if (!$res) block must precede the HasAccess(WebPermission::mask(WebPermission::Owner, WebPermission::EditAllBans)) check. Directly catches the admin.edit.comms.php runs the permission check before the not-found guard, surfacing the wrong error #1410 bug condition.
  2. Not-found toast body — pins the user-visible "Maybe the block has been deleted?" phrasing so a future refactor can't genericise it away.
  3. Perm check is not weakened — pins both the "no access" toast body AND the HasAccess(WebPermission::EditOwnBans) / HasAccess(WebPermission::EditGroupBans) arms so a future cleanup can't accidentally drop the perm check entirely (the task description called this regression out explicitly).
  4. Not-found branch still PageDies — pins that PageDie() runs after the Toast::emit so the buggy $res['aid'] deref in the perm check (and the inline <script> further down) never runs on the bad-cid path.

Static-shape tests are the right tool here because every runtime path that exercises the bug calls PageDie() (exit;), which breaks PHPUnit's RunInSeparateProcess serializer — same constraint sister ToastEmitRegressionTest.php documents at its file docblock. The runtime side is already covered by the existing web/tests/e2e/specs/flows/admin-edit-comms-toast.spec.ts E2E spec, which doesn't have the serializer constraint.

Screenshots (if appropriate):

User-visible behavior change only on the bad-cid path:

Path Pre-fix toast Post-fix toast
Bad cid + any admin "You don't have access to this!" + PHP E_WARNING in logs "There was an error getting details. Maybe the block has been deleted?"
Good cid + insufficient perm "You don't have access to this!" "You don't have access to this!" (unchanged)
Good cid + Owner (page renders) (page renders, unchanged)

Types of changes

  • Bug fix (non-breaking change which fixes an issue)
  • New feature (non-breaking change which adds functionality)
  • Breaking change (fix or feature that would cause existing functionality to change)

Checklist:

  • My code follows the code style of this project.
  • My change requires a change to the documentation.
  • I have updated the documentation accordingly.
  • I have read the CONTRIBUTING document.

Refs:

I have read the CLA Document and I hereby sign the CLA

…1410)

Closes #1410. The page handler at admin.edit.comms.php was checking
`HasAccess(...)` (which dereferences `$res['aid']` / `$res['gid']`)
BEFORE the `if (!$res)` not-found guard 130 lines further down. On a
request for a non-existent cid, `$res` was false and `$res['aid']`
evaluated to null under PHP 8.x (with an `E_WARNING: Trying to access
array offset on value of type bool`; PHP 9 will turn the warning into
a TypeError). The perm check then read "you can't edit a block owned
by null" and surfaced the misleading "You don't have access to this!"
toast instead of the correct "There was an error getting details.
Maybe the block has been deleted?" toast.

Moves the `if (!$res)` block + the `$pagelink` computation it depends
on to immediately after the SELECT, before the perm check. Result is
a 34-line relocation: same `Toast::emit` + `PageDie` shape, just
earlier in the file so the bug condition can't fire. The comment
above the moved block documents the new ordering rationale.

Sister `admin.edit.ban.php` was already correctly ordered (its
`if (!$res)` runs before `$canEditBan = HasAccess(...) || …`), so
the bug was localised to admin.edit.comms.php. No leakage to
non-Owner callers (the perm check still rejects real cids the caller
doesn't own); the audit-log trail is unchanged (neither branch logs
to `:prefix_log` — the issue body's "both branches call
Log::add(LogType::Warning, ...)" claim was inaccurate, but the swap
preserves whatever each branch did, which is nothing in this case).

Regression guard: `AdminEditCommsCheckOrderTest.php` pins the
order-of-guards contract directly against the file source. Static-
shape tests are the right tool here because every code path that
would exercise the bug runtime-wise calls `PageDie()` (`exit;`),
which breaks PHPUnit's `RunInSeparateProcess` serializer — same
constraint sister `ToastEmitRegressionTest.php` documents. Four
tests pin: (1) the not-found guard runs before the perm check,
(2) the not-found body stays correct, (3) the perm check is not
weakened (both `EditOwnBans` + `EditGroupBans` arms preserved), and
(4) the not-found branch still terminates via `PageDie()` after the
toast emit so the buggy `$res['aid']` read never runs on bad-cid.

Surfaced by the #1403 toast-lift adversarial review.
@rumblefrog
rumblefrog added this pull request to the merge queue May 18, 2026
Merged via the queue into main with commit aab4548 May 18, 2026
5 checks passed
@rumblefrog
rumblefrog deleted the fix/1410-edit-comms-check-order branch May 18, 2026 03:51
@github-actions github-actions Bot locked and limited conversation to collaborators May 18, 2026
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

admin.edit.comms.php runs the permission check before the not-found guard, surfacing the wrong error

1 participant