Confine every Support Ticket query to the authenticated user's tenant - #1
Merged
Conversation
Same cross-tenant IDOR as the other modules: 22 update/destroy actions guarded by a capability check alone, which every tenant's staff passes, so another company's tickets, contacts and knowledge base resolved fine and were then read, edited or deleted. 12 models are scoped via App\Models\Concerns\TenantScoped. ticket_field_values has no created_by column and inherits the boundary through its parent ticket. The public help centre is addressed by a slug in the URL and must remain readable by a visitor signed in to another company, so its middleware stands the scope down for the request (see zerp-pk/zerp#6). The public ticket form is validated against the PORTAL's company, resolved from the slug, not against creatorId() — the submitter is usually a guest, for whom creatorId() means nothing. SupporUtility, TicketField and SupportTicketSetting::getAllByCompany() all read a company other than the caller's, so they opt out explicitly.
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.
Closes the cross-tenant IDOR in this module. Depends on zerp-pk/zerp#6.
22
update/destroyactions were guarded by a capability check alone, which every tenant's staff passes. Another company's tickets, contacts and knowledge base articles resolved fine and were then read, edited or deleted.The fix
12 models scoped via the shared
App\Models\Concerns\TenantScoped.ticket_field_valueshas nocreated_bycolumn of its own, so it declares$tenantParent = "ticket"and inherits the boundary through its parent.The public help centre
It serves one company's FAQ, knowledge base and ticket lookup to the world, addressed by the slug in the URL — a visitor signed in to a different company must still see it. Its middleware stands the scope down for that request (zerp-pk/zerp#6).
The public ticket form is validated against the portal's company, not
creatorId(). The submitter is usually a guest, for whomcreatorId()means nothing — soStoreFrontendTicketRequestresolves the company from the slug and validates the category against that. Left as it was, a submitter could file a ticket under another company's category; scoped tocreatorId()instead, no guest could ever file one at all.Provisioning opt-outs
SupporUtility(default ticket categories),TicketField(default fields) andSupportTicketSetting::getAllByCompany()all read a company other than the caller's — the first two when provisioning a new company, the third from the public portal. All three opt out withwithoutGlobalScope("tenant"). Without that, signup would silently re-seed defaults on every run and the portal would render with no settings.Verification
Against real MySQL: in admin context each tenant is blocked from the other's ticket categories; in portal context an acme-authenticated visitor still reads beta's help centre. Covered by
tests/Unit/TenantScopeTest.php.