Summary
Multiple admin controllers implement permission checks on form display methods
(create(), edit()) but omit equivalent checks on the corresponding write
action methods (store(), update()). Any authenticated user can bypass
role-based access control (RBAC) by sending direct POST/PATCH requests to
these endpoints, skipping the form entirely.
Details
The affected controllers follow a pattern where the GET methods that render
forms correctly verify the caller's permissions, but the POST/PATCH methods
that process submissions do not. Because the permission gate is only applied
to the UI entry point and not to the underlying action, it can be bypassed
by crafting a direct HTTP request.
Missing permission checks - store() and update():
| Controller |
Required permission |
ApplicationApiController |
admin.api.write |
CouponController |
admin.coupons.write |
PartnerController |
admin.partners.write |
ShopProductController |
admin.store.write |
UsefulLinkController |
admin.useful_links.write |
VoucherController |
admin.voucher.write |
Missing permission check - update() only:
| Controller |
Required permission(s) |
ProductController |
admin.products.edit |
ServerController |
Any of: write, change_owner, change_identifier |
UserController |
Any of: write, change_email, change_credits, change_username, change_password, change_role, change_referral, change_ptero, change_serverlimit |
Additionally, ActivityLogController exposed stub store() and update()
methods with empty bodies that accepted any request silently; these have been
patched to return 403 unconditionally.
A separate but related issue was identified in UserController.logBackIn():
the method that restores a previous admin session after impersonating a user
lacked a permission check for admin.users.login_as, allowing any
authenticated user to trigger session restoration.
PoC
- Authenticate as any user without admin write permissions.
- Send a direct POST request to any of the affected endpoints, bypassing
the form UI entirely:
POST /admin/coupons HTTP/1.1
Host: <panel_url>
Cookie: <valid_session_cookie>
Content-Type: application/x-www-form-urlencoded
_token=<csrf_token>&code=FREECREDITS&type=percentage&value=100&uses=9999
- Result: the coupon is created successfully despite the caller lacking
admin.coupons.write permission. The same technique applies to all
endpoints listed in the tables above.
Impact
An authenticated attacker without admin write privileges can:
- Create and modify API credentials - issue application API keys with
arbitrary scopes, enabling persistent unauthorized API access
- Create and modify discount coupons and vouchers - generate unlimited
discount codes or vouchers, causing direct financial loss
- Create and modify partner relationships - assign arbitrary commission
and discount rates to any user account
- Create and modify shop products and pricing - alter product prices,
resource limits, and billing periods
- Modify user accounts - update roles, credits, passwords, and linked
Pterodactyl IDs for any user, enabling full privilege escalation
- Modify server records - reassign server ownership or change server
identifiers
- Abuse session restoration - trigger
logBackIn() without the
login_as permission, potentially interfering with active admin
impersonation sessions
Remediation
Add permission checks at the beginning of each affected write action method,
mirroring the checks already present in the corresponding create() and
edit() methods. The fix has been applied as follows:
For controllers with a single write permission - add checkPermission()
at the top of store() and update():
public function store(Request $request)
{
+ $this->checkPermission(self::WRITE_PERMISSION);
+
$request->validate([...]);
For controllers where update requires any one of several permissions
(e.g. ServerController, UserController) - use checkAnyPermission():
public function update(Request $request, User $user)
{
+ $this->checkAnyPermission([
+ self::WRITE_PERMISSION,
+ self::CHANGE_ROLE_PERMISSION,
+ // ... other applicable permissions
+ ]);
+
$data = $request->validate([...]);
For ActivityLogController - stub methods now explicitly abort with 403:
public function store(Request $request)
{
- //
+ abort(403, __('User does not have the right permissions.'));
}
Ensure that every action method that modifies state has a server-side
permission check independent of the UI flow that leads to it.
Summary
Multiple admin controllers implement permission checks on form display methods
(
create(),edit()) but omit equivalent checks on the corresponding writeaction methods (
store(),update()). Any authenticated user can bypassrole-based access control (RBAC) by sending direct POST/PATCH requests to
these endpoints, skipping the form entirely.
Details
The affected controllers follow a pattern where the GET methods that render
forms correctly verify the caller's permissions, but the POST/PATCH methods
that process submissions do not. Because the permission gate is only applied
to the UI entry point and not to the underlying action, it can be bypassed
by crafting a direct HTTP request.
Missing permission checks -
store()andupdate():ApplicationApiControlleradmin.api.writeCouponControlleradmin.coupons.writePartnerControlleradmin.partners.writeShopProductControlleradmin.store.writeUsefulLinkControlleradmin.useful_links.writeVoucherControlleradmin.voucher.writeMissing permission check -
update()only:ProductControlleradmin.products.editServerControllerwrite,change_owner,change_identifierUserControllerwrite,change_email,change_credits,change_username,change_password,change_role,change_referral,change_ptero,change_serverlimitAdditionally,
ActivityLogControllerexposed stubstore()andupdate()methods with empty bodies that accepted any request silently; these have been
patched to return 403 unconditionally.
A separate but related issue was identified in
UserController.logBackIn():the method that restores a previous admin session after impersonating a user
lacked a permission check for
admin.users.login_as, allowing anyauthenticated user to trigger session restoration.
PoC
the form UI entirely:
admin.coupons.writepermission. The same technique applies to allendpoints listed in the tables above.
Impact
An authenticated attacker without admin write privileges can:
arbitrary scopes, enabling persistent unauthorized API access
discount codes or vouchers, causing direct financial loss
and discount rates to any user account
resource limits, and billing periods
Pterodactyl IDs for any user, enabling full privilege escalation
identifiers
logBackIn()without thelogin_aspermission, potentially interfering with active adminimpersonation sessions
Remediation
Add permission checks at the beginning of each affected write action method,
mirroring the checks already present in the corresponding
create()andedit()methods. The fix has been applied as follows:For controllers with a single write permission - add
checkPermission()at the top of
store()andupdate():public function store(Request $request) { + $this->checkPermission(self::WRITE_PERMISSION); + $request->validate([...]);For controllers where update requires any one of several permissions
(e.g.
ServerController,UserController) - usecheckAnyPermission():public function update(Request $request, User $user) { + $this->checkAnyPermission([ + self::WRITE_PERMISSION, + self::CHANGE_ROLE_PERMISSION, + // ... other applicable permissions + ]); + $data = $request->validate([...]);For
ActivityLogController- stub methods now explicitly abort with 403:public function store(Request $request) { - // + abort(403, __('User does not have the right permissions.')); }Ensure that every action method that modifies state has a server-side
permission check independent of the UI flow that leads to it.