high · 8.1CVE-2026-81892Sep 2, 2026

CVE-2026-81892: EasyAdmin Custom-Action Dispatcher Authorization Bypass

Pranav Khune
Penetration Testing Team Lead, SecureLayer7

A low-privilege EasyAdmin user can reach any Symfony route by name simply by passing its route name in a query parameter, skipping path-based access_control rules that were supposed to protect it.

Packageeasycorp/easyadmin-bundle
Ecosystemcomposer
Affected>= 4.0.0, < 4.29.16
Fixed in4.29.16
CVE-2026-81892: EasyAdmin Custom-Action Dispatcher Authorization Bypass

The problem

EasyAdmin funnels all backend traffic through one dashboard URL and dispatches custom actions by reading a routeName query parameter on the kernel.controller event. Symfony's firewall evaluates access_control rules against the dashboard URL before this swap happens, so the target route's own path-based rules are never checked.

Any backend user who can reach the EasyAdmin dashboard and knows the Symfony route name of a more-privileged controller can invoke it directly. The attacker needs no special tooling, only a crafted GET request.

Proof of concept

A working proof-of-concept for CVE-2026-81892 in easycorp/easyadmin-bundle, with the exact payload below.

http
GET /admin?routeName=app_admin_superadmin_dashboard HTTP/1.1
Host: target.example.com
Cookie: PHPSESSID=<valid_low_privilege_session>

# General pattern (swap in any protected route name):
# GET /admin?routeName=<protected_route_name>[&routeParams[key]=value]
#
# Example using linkToRoute / MenuItem::linkToRoute target:
# GET /admin?routeName=app_restricted_report&routeParams[month]=08

The root cause is CWE-639 (Authorization Bypass Through User-Controlled Key): the routeName parameter is attacker-supplied and was passed unvalidated into the controller swap. Symfony's security firewall already ran against /admin, so the path rule protecting /restricted-area was never triggered for the swapped controller.

The patch (commit 6228dfe / 03be45c) adds an explicit access_control re-evaluation inside the custom-action event subscriber before the controller is swapped. If the current user does not pass the check for the target route's path, the request is denied with a 403.

Routes using controller-level #[IsGranted] or denyAccessUnlessGranted() were always safe because those are re-run against the new controller regardless.

The fix

Upgrade to **4.29.16** (4.x branch) or **5.5.1** (5.x branch). As a temporary workaround, add #[IsGranted] or a denyAccessUnlessGranted() call inside every sensitive controller action, since controller-level checks are re-evaluated against the swapped controller and remain effective even on unpatched versions.

Reported by TungNGo02.

References: [1][2][3][4][5][6]

Related research