No Code Attached Yet
avatar tadosborn
tadosborn
25 May 2026

What happened?

When Session::checkToken() redirects an unauthenticated/expired-session request, it calls Route::_('index.php') without overriding the default $xhtml = true. Route::_() runs htmlspecialchars() on the URL, converting every & into the literal 5-character string &. That value is passed straight to $app->redirect(), which writes it into the HTTP Location: response header.

& is not a valid query separator. Per RFC 3986, ampersands in a Location header must be raw &. Some crawlers (e.g. XOVI) and a few HTTP libraries do not decode HTML entities before following a redirect, so they fetch the next URL with & still embedded. On that next hop, PHP's parse_str treats &Itemid=101 as separator-& plus param-name-amp;Itemid. Any downstream redirect that rebuilds the query string from $_GET then re-encodes the already-encoded ampersands. Every traversal multiplies the amp; prefix.

End result is an exponential redirect chain like:

/component/jce/?task=editor.loadlanguages&lang=en&context=519
→ /component/jce/?task=editor.loadlanguages&lang=en&context=519
→ /component/jce/?task=editor.loadlanguages&lang=en&context=519
→ ...
→ HTTP 502 once URL length exceeds server limits

The trip-wire is Session::isNew() being true, which is the default state for any crawler request (no session cookie).

Reproduce on stock Joomla 6.1.0 (also reproduces on 5.4, 6.2, 7.0):

  1. Install any extension whose front-end controller calls Session::checkToken('get') on a guest-reachable URL that is emitted into rendered HTML — JCE Editor's editor.loadlanguages URL (shipped in <script src=...> on every page that loads the editor) is one widely-deployed example, but the bug is in core and any such third-party endpoint reproduces it.
  2. From a clean session (no cookie jar), GET the tokenized URL without a valid token:
    curl -ski "https://example.test/component/jce/?task=editor.loadlanguages&lang=en&context=1&profile_id=2"
  3. Observe response:
    HTTP/1.1 303 See other
    Location: https://example.test/component/jce/?task=editor.loadlanguages&amp;lang=en&amp;context=1&amp;profile_id=2
    
    Note the literal &amp; in the Location header.
  4. Follow the redirect (curl -L) once and inspect the next Location header — it now contains &amp;amp;. Each follow multiplies it.

Real-world impact observed

On infinetinc.com (Joomla 6.1.0), the SEO crawler XOVI logged 463 distinct /component/jce/?task=editor.loadlanguages&amp;amp;... and /component/users/?task=user.login&amp;amp;... URLs as temporaryRedirects (303) and serverErrors (502 after URL exceeded server limits) over a 30-day window. Once the patch below was applied, the chain stopped expanding and the crawler errors dropped to zero on the next scan.

Internal audit across 47 of our managed Joomla sites (J3 through J6) found the same pattern on every site that had a guest-reachable Session::checkToken trigger — i.e. it is reliably reproducible, not site-specific.

Version

6.1 (also confirmed in 5.4-dev, 6.2-dev, 7.0-dev — all maintained branches contain the bare Route::_('index.php') call at libraries/src/Session/Session.php line 79)

Expected result

The Location header contains a URL with raw & between query parameters, per RFC 3986. A crawler following the redirect lands at the same URL it would receive on a fresh request — no exponential growth.

Actual result

The Location header contains &amp; (literal 5-character HTML entity) between query parameters. Crawlers that do not entity-decode before following the redirect produce an exponentially-growing query string on each hop until the request URL exceeds server limits and returns 502.

System Information

  • Joomla 6.1.0 ("Nyota") — but the bug code is identical in 5.4-dev, 6.2-dev, and 7.0-dev as of 2026-05-25 (verified via raw.githubusercontent.com).
  • PHP 8.3 — irrelevant, the bug is in PHP code that's PHP-version-agnostic.
  • File: libraries/src/Session/Session.php line 79.

Additional Comments

Root cause (one line)

libraries/src/Session/Session.php:79:

$app->redirect(Route::_('index.php'));

Route::_($url, $xhtml = true, ...) — the default xhtml=true runs htmlspecialchars($url). The output of that goes into the Location: header where entity-encoding is wrong.

Proposed fix (one character)

$app->redirect(Route::_('index.php', false));

The false suppresses the entity encoding so the Location header receives raw &. There is no XHTML context here — this URL is going into an HTTP response header, not into rendered HTML — so the default is wrong for this call site.

I'm happy to open a PR against 6.1-dev (and back/forward-port to 5.4, 6.2, 7.0) if a maintainer confirms this is the right surface for the fix. An alternative would be to entity-decode inside AbstractWebApplication::redirect() itself, but that's more intrusive and could break callers that intentionally pass entity-encoded URLs.

Why this hasn't been reported before

Browsers tolerate &amp; in Location headers (they decode entities before re-issuing the request), so a human testing in Chrome/Firefox never sees the chain. The bug only surfaces with crawlers/automated agents that follow Location: literally — and even among those, only the ones that re-build the URL from parsed $_GET on the next hop produce the multiplying chain. Google's crawler normalizes entity encoding before re-fetching, so Search Console doesn't typically flag it. XOVI is the most reliable detector we've encountered.

avatar tadosborn tadosborn - open - 25 May 2026
avatar joomla-cms-bot joomla-cms-bot - change - 25 May 2026
Title
Session::checkToken emits HTML-encoded `&amp;` into HTTP Location header, causing exponential redirect chains
Session::checkToken emits HTML-encoded `&` into HTTP Location header, causing exponential redirect chains
Labels Added: No Code Attached Yet
avatar joomla-cms-bot joomla-cms-bot - labeled - 25 May 2026

Add a Comment

Login with GitHub to post a comment