fix: requesting access does not name an account you could not list - #913
Merged
blaipr merged 1 commit intoSep 7, 2026
Merged
Conversation
?r=account/requestAccess/<id> rendered any account's name and client to any signed-in user. ACCOUNT_REQUEST sits in the unconditional arm of Acl::checkUserAccess() beside the notification actions; getByIdEnriched() is a bare WHERE id = :id, unfiltered on purpose because most callers pair it with an explicit ACL check; and AccountRequestHelper is the one helper in its directory that makes no such check, where AccountHelper and AccountHistoryHelper both do. Private accounts were reached too — buildFilterPrivate() withholds those from everybody, isAdminApp included. The obvious fix would break the feature: requesting a modification exists to ask about an account you can see listed and cannot open — isShowRequest() is literally !isShow(), and under global search that is an account you have no relationship with. So the bound is the search filter, which decides listability, not the ACL. An account the caller could not have listed is refused with the same 'doesn't exist' as one genuinely absent. The view is aliased to Account because that is what the filter qualifies its conditions with; account_data_v exposes every column they read, confirmed against the database.
blaipr
deleted the
fix/requesting-access-does-not-name-an-account-you-cannot-list
branch
September 7, 2026 22:20
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
?r=account/requestAccess/<id>rendered any account's name and client to any signed-in user,for any id they cared to try.
Three things line up to make that true:
ACCOUNT_REQUESTis in the unconditional arm ofAcl::checkUserAccess()— the samereturn truethat serves the notification actions — so the action-level gate refuses nobody.AccountRepository::getByIdEnriched()is a bareWHERE id = :idwith no filter. That isdeliberate: most of its callers pair it with an explicit per-account ACL check.
AccountRequestHelper::setViewForRequest()is the one helper inHelpers/Account/that makes nosuch check — it tests
$this->actionGranted(the action-level gate above) and then assigns theaccount straight to the template.
AccountHelper::checkAccess()andAccountHistoryHelper::checkAccess()both do the per-account check.Private accounts are reached too, which is the sharpest part:
buildFilterPrivate()withholds themfrom everybody,
isAdminAppincluded, and this path went around it.The companion
saveRequestaction reads the same way, so it is changed with it.Why the obvious fix would have been wrong
The natural move is to copy
AccountHelper::checkAccess(). That breaks the feature."Request Modification" exists to ask about an account you can see listed and cannot open:
AccountSearchItem::isShowRequest()is literally!$this->accountAcl->isShow(), and under globalsearch (
AccountFilter::isFilterWithoutGlobalSearch()) a user withisAccGlobalSearch()listsaccounts they have no relationship with at all. Requiring view access would refuse exactly the case
the button is offered for.
So the bound is the search filter — the thing that decides what a user could have found — rather
than the ACL.
getByIdEnrichedForUser()isgetByIdEnriched()withAccountFilter::buildFilter()applied, and an account the caller could not have listed is refused with the same "The account
doesn't exist" as one that is genuinely absent, so the two cannot be told apart.
The view is aliased to
Accountbecause that is the name the filter qualifies its conditions with;account_data_vexposes every column they read —id,userId,userGroupId,isPrivate,isPrivateGroup— confirmed against the running database rather than assumed.Tests
requestAccessDoesNotNameAnAccountTheUserCannotListasserts the account's name is nowhere in theresponse. The two reads are told apart by their statement, not their mapper, because both map to
AccountViewand only the filtered one joinsAccountToUser.Mutation-verified in both directions. Worth recording that the first attempt passed against the
unfixed code for the wrong reason: a blanket query resolver answered every query with an
AccountView, so the request died onInvalid data's type. Expected: …ItemPresetbefore it everrendered, and "the name is absent" was true because nothing was. Scoping the resolver to the account
read — and letting everything else answer as the harness default does — makes the old code render
the page and fail the assertion, which is the proof that was wanted.