CVE-2026-48489: Symfony Firewall Bypass via failure_forward — when an internal subrequest skips the access_control line
31 May 2026. On 27 May, Symfony fixed CVE-2026-48489, a firewall bypass in the Security HTTP component. When a form-login firewall runs with failure_forward: true, the DefaultAuthenticationFailureHandler takes the client-supplied _failure_path as the target of an internal subrequest — and because Symfony deliberately skips subrequests in Firewall::onKernelRequest, the AccessListener with the access_control rules never runs. An unauthenticated POST with _failure_path=/admin/whatever thus reads any GET route behind a broad ^/admin rule — with no misconfiguration, no debug mode, no state-changing handler. For any Symfony or Sylius stack that protects admin areas via access_control and exposes read-only exports or internal APIs there, the finding is operational; fix releases are 5.4.53, 6.4.41, 7.4.13 and 8.0.13.
TL;DR — the 90-second summary
- What was published?
CVE-2026-48489 (Symfony security advisory, 27 May 2026). Class: authorization bypass / local request forgery via an internal subrequest. Mechanism: with a firewall using
form-login(or any authenticator that uses theDefaultAuthenticationFailureHandler) andfailure_forward: true, the handler reads the client-supplied_failure_pathfrom the failing login request and dispatches it as anHttpKernelInterface::SUB_REQUEST. TheFirewalllistener deliberately skips subrequests, so theAccessListener(which evaluatesaccess_control) does not run.- How severe?
High (operational rating — Symfony does not assign a CVSS, NVD still RESERVED as of 31 May). Reach: unauthenticated read access to GET routes behind an
access_controlrule. Entry condition: (a) a firewall withform-login/DefaultAuthenticationFailureHandlerand (b)failure_forward: true(not the default — the default is the redirect variantfailure_forward: false, which is not affected).- Which Symfony versions are affected?
Security HTTP component
<5.4.53,>=6 <6.4.41,>=7 <7.4.13and>=8 <8.0.13. Fixed in 5.4.53, 6.4.41, 7.4.13 and 8.0.13.- Am I affected?
You are affected if one of your Symfony or Sylius platforms has a firewall with
form-loginandfailure_forward: trueand read-only GET endpoints sit behind a broadaccess_controlrule (e.g.^/admin). The most common configuration uses the redirect variant (failure_forward: false) and is not affected.- Immediate mitigation?
Two steps. First, check whether any firewall in
security.yamlsetsfailure_forward: true(grep -r "failure_forward" config/). If not: not affected. Second, if yes: upgrade to the fix release (5.4.53 / 6.4.41 / 7.4.13 / 8.0.13). Stopgap: setfailure_forwardtofalse— closes the path immediately.- Criticality?
Hero badge
high(operational rating, no official CVSS). No public reports of active exploitation as of 31 May, no CISA KEV listing. The exploit path is trivially reproducible (a POST with_failure_pathset) — if you meet the precondition, act promptly.
What happened
On 27 May 2026 Symfony published a security advisory for CVE-2026-48489 — a firewall bypass in the Security HTTP component. The bug sits in a path most developers never consciously touch: the forward branch of login failure handling.
Anyone configuring a firewall with form-login (or, more generally, an authenticator that uses the DefaultAuthenticationFailureHandler) can set the failure_forward: true option. With it, after a failed login the request is not redirected to the login path; instead an internal subrequest is dispatched to the failure path and its response returned directly. For this, the DefaultAuthenticationFailureHandler read the _failure_path parameter — from the failing login request, i.e. from client-controlled data. That path was used as the target of an HttpKernelInterface::SUB_REQUEST.
The actual security break comes from a second, individually correct, design decision: Symfony's Firewall::onKernelRequest listener deliberately skips subrequests, on the assumption that subrequests are internally generated and therefore trusted. That assumption breaks here: because the attacker controls the subrequest target via _failure_path, the AccessListener — the listener that evaluates the access_control rules — does not run for this subrequest. An unauthenticated POST to the check path with _failure_path=/admin/whatever thus performs a local request forgery: the target controller runs outside the firewall perimeter and its response is returned to the caller.
The Symfony advisory is unusually clear here: applications that follow the recommended best practice and protect admin areas with broad access_control rules (e.g. ^/admin requires ROLE_ADMIN) and expose read-only GET endpoints under that area (data exports, internal APIs, account views) are fully exposed: any such GET route is readable by an unauthenticated attacker — without any developer mistake, without debug mode and without a state-changing GET handler having to exist. The fix makes the DefaultAuthenticationFailureHandler no longer honor the request-supplied _failure_path when failure_forward is active; the subrequest always goes to the application-configured failure_path (default: login_path). The redirect branch (failure_forward: false) is unchanged.
Technical assessment
Structurally, CVE-2026-48489 is a lesson about trust boundaries at the subrequest edge. Symfony's HttpKernel subrequest concept is powerful and correct: ESI fragments, render() controller forwards and internal sub-renders should not run the full request-processing chain including the firewall every time, because they originate from trusted application code. The assumption “subrequest == internal == trusted” is therefore hard-wired into Firewall::onKernelRequest. It holds exactly as long as the subrequest target is determined solely by application code. The moment a single path exists in which a client-controlled value flows into the subrequest target, the assumption tips over — and with it the entire authorization layer for that request.
The second methodological point is the kinship to the local request forgery / “confused deputy” class. The DefaultAuthenticationFailureHandler acts here as a confused deputy: it has the privilege to dispatch internal subrequests, and on behalf of an unauthenticated caller performs an operation that caller could not perform itself. This is the same basic form as SSRF (only the target is internal rather than external) and as classic forward/include authorization bypasses in other frameworks. The lesson is generic: any component that triggers a privileged internal operation on a caller's behalf must take the operation target from a trusted source — not from the request it is currently processing.
Third, the entry threshold matters for triage. failure_forward: true is not the default. The usual configuration shown as standard in the Symfony documentation uses the redirect variant (failure_forward: false), which is not affected. The forward branch is typically set in two situations: first, in single-page-style login flows that want to render the error response in the same response cycle, and second, in grown setups where the option comes from an old tutorial or a copied security.yaml block and was never questioned. That second class is the dangerous one in maintenance drift, because nobody remembers it is set. A grep -r "failure_forward" config/ over the estate answers the question in seconds.
Fourth, the place in the Symfony May 2026 release wave. CVE-2026-48489 is part of the same coordinated release (5.4.53 / 6.4.41 / 7.4.13 / 8.0.13) that also ships CVE-2026-48736 (an SSRF bypass in NoPrivateNetworkHttpClient via IPv6 transition forms) and several other component fixes. The patch path is shared: upgrading to the fix release closes both Symfony core issues in one step. The parallel Twig sandbox wave (Twig 3.27.0) is a separate component and only affects stacks that process user-controlled Twig template source.
Who is affected
| Affected | Not affected | Condition |
|---|---|---|
Symfony apps with Security HTTP <5.4.53 / <6.4.41 / <7.4.13 / <8.0.13 | Apps on the fix releases 5.4.53 / 6.4.41 / 7.4.13 / 8.0.13 and higher | Version of the symfony/security-http component |
Firewalls with form-login / DefaultAuthenticationFailureHandler and failure_forward: true | Firewalls with failure_forward: false (default, redirect variant) | failure_forward value in security.yaml |
Apps with broad access_control rules (e.g. ^/admin) and read-only GET endpoints behind them | Apps with additional controller-level authorization (voters / #[IsGranted] in the controller) | Protection model: access_control alone vs. additional controller authorization |
| Sylius shops and Symfony-direct apps with a custom admin login | TYPO3 (its own auth layer, not the Symfony security firewall) | Auth architecture of the CMS/shop |
One important point for classification: routes that are additionally authorized at the controller level — e.g. with #[IsGranted('ROLE_ADMIN')] on the controller or via a security voter in controller code — are protected by that second layer, because it runs independently of the access_control listener. So anyone running defense-in-depth (firewall access_controland controller authorization) has a buffer here. Anyone relying on the access_control rule alone — the practice Symfony recommends and that is very widespread — is exposed. That is the uncomfortable punchline: the best-practice configuration is the vulnerable one.
What it means for mid-market companies
Symfony is the invisible infrastructure under a great many web applications — directly as a Symfony app, transitively under Sylius shops, under API platforms and under countless in-house tools. The concrete question for you as an operator is: does any of your platforms run a login firewall with failure_forward: true, and are there GET endpoints serving data behind a ^/admin-style rule?
In practice the exposed endpoints are almost always the inconspicuous ones: a CSV export of the customer list at /admin/export/customers, an internal status JSON at /admin/api/orders, an account detail view at /admin/users/{id}. These read-only routes are often overlooked in threat modeling because they “only read” and sit “behind the admin login anyway.” CVE-2026-48489 turns that “behind the login anyway” into “readable with a single POST.”
On the compliance side the issue touches several axes. GDPR Art. 32 requires “technical measures appropriate to the risk”; unauthenticated read access to an endpoint serving personal data (customer exports, account views) is a clear finding here, and under Art. 33/34 demonstrable exploitation can become reportable. NIS-2 Art. 21 requires concrete patch discipline and a reliable asset/dependency inventory for the affected mid-sized companies — a symfony/security-http version below the fix release that is not tracked in the SBOM is an audit finding at this point. I do not provide a legal assessment here (I am not a lawyer); the data-protection impact assessment belongs with your DPO.
Operationally, the good news is that triage is cheap. A grep -r "failure_forward" config/ across the platform inventory separates affected from unaffected stacks in minutes. The bad news is the usual one: anyone without a dependency inventory and a reproducible build does not reliably know which symfony/security-http version sits in which application — and that is where the actual work is.
What it means for technical development
Architecturally, CVE-2026-48489 is a reminder of three disciplines.
First, subrequest targets are trust-boundary decisions, not implementation details. Every place in your own code that triggers a subrequest, an internal forward, a sub-render or a forward() must answer the explicit question: where does the target come from? From application constants, routing configuration or hard-wired code — good. If even one component of the target comes from the request — a query parameter, a body field, a header — an allowlist or a fixed binding belongs in between. This is not Symfony-specific: the same class exists in every framework with internal dispatch.
Second, defense-in-depth is not optional here, it is the buffer. Anyone protecting admin routes solely via access_control has exactly one authorization layer — and if that one layer fails through a framework bug, the entire protection fails. Anyone additionally authorizing at the controller level (#[IsGranted], voters) has a second, independent layer that catches this specific bypass. This is not an argument against access_control — the firewall rule remains the correct first line — but one for the second line at sensitive endpoints.
Third, “internal == trusted” is an assumption with an expiry date. The subrequest trust assumption was correct for years and remains so in the normal case. The bug shows that such fundamental assumptions deserve an audit entry: once per major release, ask “which paths can fill a construct marked as internal with externally controlled content.” For your own codebase that means concretely: a grep over SUB_REQUEST, forward(, HttpKernelInterface:: and comparable internal dispatch sites, and at every hit check where the target comes from.
Concrete recommendation
Operational decision block
- Patch immediately if: a production firewall has
failure_forward: trueset and read-only GET endpoints with personal or business-critical data sit behindaccess_control. - A maintenance window is enough if:
failure_forward: trueis set but the protected routes are additionally authorized at the controller level (#[IsGranted]/ voters) — then the second layer catches the bypass; the patch is still mandatory. - No operational pressure if: no firewall uses
failure_forward: true(default redirect variant) — then the app is not affected; take the patch in the regular update cycle.
In this order. First, inventory today: identify every Symfony/Sylius stack via the platform inventory and run grep -r "failure_forward" config/; in parallel pull the symfony/security-http version from composer.lock (composer show symfony/security-http). Second, patch run for the affected hosts: upgrade to the fix release of the respective branch — composer update symfony/security-http to 5.4.53 / 6.4.41 / 7.4.13 / 8.0.13 (or the matching symfony/symfony meta-package), rebuild, deploy. For container images, rebuild the base/app image tag, do not just patch the running container. Third, stopgap for hosts that cannot patch immediately: set failure_forward: false (redirect variant, not affected) — this changes the login-failure UX but closes the path immediately. Fourth, logging sweep over the last 30 days: check access logs for POST requests to login check paths with a _failure_path parameter set, especially with values pointing at /admin paths. Fifth, medium term: add a second authorization layer at the controller level (#[IsGranted] / voter) on sensitive admin endpoints, so the next firewall-layer bug does not again take down the entire protection line.
If these steps cannot be run in-house, talk to me: I keep Symfony and Sylius platforms in a running SBOM and patch process, review firewall / access_control configurations in the architecture review, and validate mitigations against the documented exploit path — platform operations, not advice on paper.
This article reflects my technical and strategic assessment. It does not replace legal advice or a data-protection impact assessment.
Conclusion
CVE-2026-48489 is not an RCE and not a critical mass wave — and that is precisely what makes it operationally awkward, because it hits the recommended configuration: anyone dutifully protecting admin areas via access_control and exposing harmless read-only GET routes there is exposed with failure_forward: true, without having done anything wrong. Triage is cheap (grep for failure_forward), the fix is a normal Composer update, the stopgap (failure_forward: false) is a one-liner. The most important recommendation is the medium-term one: sensitive admin endpoints deserve a second, controller-level authorization layer, so the next firewall-layer bug does not again take down the whole line. Risk, soberly: high for the affected configuration, irrelevant for everyone else — the craft lies in cleanly separating the two classes.
Sources
- Symfony Blog — CVE-2026-48489: Security Firewall Bypass via failure_forward Subrequest (27 May 2026)
- Symfony Blog — Security Advisories (overview, as of 31 May 2026)
- Symfony — patch commit for branch 5.4 (CVE-2026-48489)
- NVD — CVE-2026-48489 (as of 31 May 2026: not yet published in NVD / RESERVED, hence no CVSS score)
About the author
![[Translate to English:] Foto von Kai Ole Hartwig.](/fileadmin/_processed_/e/9/csm_ole-neu_73323ad80d.jpeg)
Kai Ole Hartwig
Programming since 2002 – self-taught, set up my own business with KO-Web in 2012. Over 100 projects, with a focus on security, performance, automation and quality. Today freelance: DevSecOps consulting, training and software development.
