GHSA-H5X3-XFC9-M39H
Vulnerability from github – Published: 2026-06-15 17:33 – Updated: 2026-06-15 17:33Description
Symfony\Component\Routing\Generator\UrlGenerator::doGenerate() percent-encodes . and .. path segments so that the generated URL still resolves to the originating route after RFC 3986 §5.2.4 dot-segment removal (which strict RFC-3986 consumers — routers, reverse proxies, HTTP clients — perform before percent-decoding).
The encoding was implemented as strtr($url, ['/../' => '/%2E%2E/', '/./' => '/%2E/']) plus a trailing-segment fixup. strtr advances past the trailing / of each match, so the next dot-segment in a chained sequence was left unescaped:
| Input | Output (before fix) | Expected |
|---|---|---|
/../../../ |
/%2E%2E/../%2E%2E/ |
/%2E%2E/%2E%2E/%2E%2E/ |
/foo/../../../bar |
/foo/%2E%2E/../%2E%2E/bar |
/foo/%2E%2E/%2E%2E/%2E%2E/bar |
When a route exposes a parameter constrained by a permissive requirement (.+, .*, or similar) that accepts dots and slashes, attacker-controlled chained .. or . segments produce a generated URL that, under strict RFC 3986 normalization, collapses to a different path than the originating route. The Twig path() / url() helpers and any server-side use of UrlGenerator are affected. Same class of route round-trip integrity issue as CVE-2026-45065.
Note: WHATWG-conformant browsers treat %2E/%2E%2E as dot-segments during URL parsing, so the encoding never protected browser-side traversal. The defense exists for RFC-3986-conformant consumers; restoring it for chained segments closes the gap there.
Resolution
UrlGenerator now matches every /. or /.. dot-segment in a single left-to-right preg_replace_callback pass using a lookahead that does not consume the trailing /, so adjacent dot-segments are encoded correctly.
The patches for this issue are available here for branch 5.4 (and forward-ported to 6.4, 7.4, 8.0 and 8.1).
Credits
Symfony would like to thank Alex Pott for reporting the issue and Nicolas Grekas for providing the fix.
{
"affected": [
{
"package": {
"ecosystem": "Packagist",
"name": "symfony/routing"
},
"ranges": [
{
"events": [
{
"introduced": "0"
},
{
"fixed": "5.4.53"
}
],
"type": "ECOSYSTEM"
}
]
},
{
"package": {
"ecosystem": "Packagist",
"name": "symfony/routing"
},
"ranges": [
{
"events": [
{
"introduced": "6.0.0"
},
{
"fixed": "6.4.41"
}
],
"type": "ECOSYSTEM"
}
]
},
{
"package": {
"ecosystem": "Packagist",
"name": "symfony/routing"
},
"ranges": [
{
"events": [
{
"introduced": "7.0.0"
},
{
"fixed": "7.4.13"
}
],
"type": "ECOSYSTEM"
}
]
},
{
"package": {
"ecosystem": "Packagist",
"name": "symfony/routing"
},
"ranges": [
{
"events": [
{
"introduced": "8.0.0"
},
{
"fixed": "8.0.13"
}
],
"type": "ECOSYSTEM"
}
]
},
{
"package": {
"ecosystem": "Packagist",
"name": "symfony/symfony"
},
"ranges": [
{
"events": [
{
"introduced": "0"
},
{
"fixed": "5.4.53"
}
],
"type": "ECOSYSTEM"
}
]
},
{
"package": {
"ecosystem": "Packagist",
"name": "symfony/symfony"
},
"ranges": [
{
"events": [
{
"introduced": "6.0.0"
},
{
"fixed": "6.4.41"
}
],
"type": "ECOSYSTEM"
}
]
},
{
"package": {
"ecosystem": "Packagist",
"name": "symfony/symfony"
},
"ranges": [
{
"events": [
{
"introduced": "7.0.0"
},
{
"fixed": "7.4.13"
}
],
"type": "ECOSYSTEM"
}
]
},
{
"package": {
"ecosystem": "Packagist",
"name": "symfony/symfony"
},
"ranges": [
{
"events": [
{
"introduced": "8.0.0"
},
{
"fixed": "8.0.13"
}
],
"type": "ECOSYSTEM"
}
]
}
],
"aliases": [
"CVE-2026-48784"
],
"database_specific": {
"cwe_ids": [
"CWE-172",
"CWE-601"
],
"github_reviewed": true,
"github_reviewed_at": "2026-06-15T17:33:46Z",
"nvd_published_at": null,
"severity": "MODERATE"
},
"details": "### Description\n\n`Symfony\\Component\\Routing\\Generator\\UrlGenerator::doGenerate()` percent-encodes `.` and `..` path segments so that the generated URL still resolves to the originating route after RFC 3986 \u00a75.2.4 dot-segment removal (which strict RFC-3986 consumers \u2014 routers, reverse proxies, HTTP clients \u2014 perform *before* percent-decoding).\n\nThe encoding was implemented as `strtr($url, [\u0027/../\u0027 =\u003e \u0027/%2E%2E/\u0027, \u0027/./\u0027 =\u003e \u0027/%2E/\u0027])` plus a trailing-segment fixup. `strtr` advances past the trailing `/` of each match, so the next dot-segment in a chained sequence was left unescaped:\n\n| Input | Output (before fix) | Expected |\n| -------------------- | ---------------------------------------- | ----------------------------------- |\n| `/../../../` | `/%2E%2E/../%2E%2E/` | `/%2E%2E/%2E%2E/%2E%2E/` |\n| `/foo/../../../bar` | `/foo/%2E%2E/../%2E%2E/bar` | `/foo/%2E%2E/%2E%2E/%2E%2E/bar` |\n\nWhen a route exposes a parameter constrained by a permissive requirement (`.+`, `.*`, or similar) that accepts dots and slashes, attacker-controlled chained `..` or `.` segments produce a generated URL that, under strict RFC 3986 normalization, collapses to a different path than the originating route. The Twig `path()` / `url()` helpers and any server-side use of `UrlGenerator` are affected. Same class of route round-trip integrity issue as CVE-2026-45065.\n\nNote: WHATWG-conformant browsers treat `%2E`/`%2E%2E` as dot-segments during URL parsing, so the encoding never protected browser-side traversal. The defense exists for RFC-3986-conformant consumers; restoring it for chained segments closes the gap there.\n\n### Resolution\n\n`UrlGenerator` now matches every `/.` or `/..` dot-segment in a single left-to-right `preg_replace_callback` pass using a lookahead that does not consume the trailing `/`, so adjacent dot-segments are encoded correctly.\n\nThe patches for this issue are available [here](https://github.com/symfony/symfony/commit/4b63c3a3f7af04ecd79c89a594b0b02a01990b1d) for branch 5.4 (and forward-ported to 6.4, 7.4, 8.0 and 8.1).\n\n### Credits\n\nSymfony would like to thank Alex Pott for reporting the issue and Nicolas Grekas for providing the fix.",
"id": "GHSA-h5x3-xfc9-m39h",
"modified": "2026-06-15T17:33:46Z",
"published": "2026-06-15T17:33:46Z",
"references": [
{
"type": "WEB",
"url": "https://github.com/symfony/symfony/security/advisories/GHSA-h5x3-xfc9-m39h"
},
{
"type": "WEB",
"url": "https://github.com/symfony/symfony/commit/4b63c3a3f7af04ecd79c89a594b0b02a01990b1d"
},
{
"type": "WEB",
"url": "https://github.com/FriendsOfPHP/security-advisories/blob/master/symfony/routing/CVE-2026-48784.yaml"
},
{
"type": "WEB",
"url": "https://github.com/FriendsOfPHP/security-advisories/blob/master/symfony/symfony/CVE-2026-48784.yaml"
},
{
"type": "PACKAGE",
"url": "https://github.com/symfony/symfony"
},
{
"type": "WEB",
"url": "https://symfony.com/cve-2026-48784"
}
],
"schema_version": "1.4.0",
"severity": [
{
"score": "CVSS:4.0/AV:N/AC:L/AT:N/PR:N/UI:N/VC:N/VI:L/VA:N/SC:L/SI:L/SA:N",
"type": "CVSS_V4"
}
],
"summary": "Symfony: UrlGenerator Dot-Segment Encoding Skips Every Other Chained `../` or `./` \u2192 Generated URL Collapses Off-Route Under RFC 3986 Normalization"
}
Sightings
| Author | Source | Type | Date | Other |
|---|
Nomenclature
- Seen: The vulnerability was mentioned, discussed, or observed by the user.
- Confirmed: The vulnerability has been validated from an analyst's perspective.
- Published Proof of Concept: A public proof of concept is available for this vulnerability.
- Exploited: The vulnerability was observed as exploited by the user who reported the sighting.
- Patched: The vulnerability was observed as successfully patched by the user who reported the sighting.
- Not exploited: The vulnerability was not observed as exploited by the user who reported the sighting.
- Not confirmed: The user expressed doubt about the validity of the vulnerability.
- Not patched: The vulnerability was not observed as successfully patched by the user who reported the sighting.