{"id":"CVE-2026-31859","aliases":["GHSA-fvwq-45qv-xvhv"],"url":"https://o3.security/vulnerability/CVE-2026-31859","summary":"Craft has Reflective XSS via incomplete return URL sanitization","details":"### Summary\n\nThe fix for CVE-2025-35939 in `craftcms/cms` introduced a `strip_tags()` call in `src/web/User.php` to sanitize return URLs before they are stored in the session. However, `strip_tags()` only removes HTML tags (angle brackets) -- it does not inspect or filter URL schemes. Payloads like `javascript:alert(document.cookie)` contain no HTML tags and pass through `strip_tags()` completely unmodified, enabling reflected XSS when the return URL is rendered in an `href` attribute.\n\n### Details\nThe patched code in is:\n\n```php\npublic function setReturnUrl($url): void\n{\n    parent::setReturnUrl(strip_tags($url));\n}\n```\n\n`strip_tags()` removes HTML tags (e.g., `<script>`, `<img>`) from a string, but it is **not** a URL sanitizer. When the sanitized return URL is subsequently rendered in an `href` attribute context (e.g., `<a href=\"{{ returnUrl }}\">`), the following dangerous payloads survive `strip_tags()` completely unmodified:\n\n1. **`javascript:` protocol URLs** -- `javascript:alert(document.cookie)` contains no HTML tags, so `strip_tags()` returns it verbatim. When placed in an `href`, clicking the link executes the JavaScript.\n\n2. **`data:` URIs** -- `data:text/html;base64,PHNjcmlwdD5hbGVydCgxKTwvc2NyaXB0Pg==` uses Base64 encoding and contains no tags at all, bypassing `strip_tags()` entirely.\n\n3. **Protocol-relative URLs** -- `//evil.com/steal` contains no tags and is passed through unchanged. When rendered as an `href`, the browser resolves it relative to the current page’s protocol, redirecting the user to an attacker-controlled domain.\n\nThe core issue is that `strip_tags()` operates on HTML syntax (angle brackets) while the threat model here requires URL scheme validation. These are fundamentally different security concerns.\n\n### Impact\n\n**Reflected XSS via crafted return URL.** An attacker constructs a malicious link such as `https://target.example.com/craft/?returnUrl=javascript:alert(document.cookie)` and sends it to a victim. The attack flow is:\n\n1. Victim clicks the link, visiting the Craft CMS site.\n2. The application calls `setReturnUrl()` with the attacker-controlled value.\n3. `strip_tags()` processes the URL but finds no HTML tags -- it passes through unchanged.\n4. The URL is stored in the session and later rendered in an `href` attribute (e.g., a \"Return\" or \"Continue\" link).\n5. When the victim clicks that link, `javascript:alert(document.cookie)` executes in the context of the Craft CMS origin.\n\nThis enables:\n- **Session hijacking** via cookie theft (`document.cookie`)\n- **Data exfiltration** via `fetch()` to an attacker-controlled server\n- **Phishing** by redirecting to a lookalike domain (protocol-relative URL)\n- **CSRF** by performing actions on behalf of the authenticated user","published":"2026-03-11T17:37:19.065Z","modified":"2026-08-12T03:51:09.859404754Z","cvss":null,"epss":{"score":0.00185,"percentile":0.08338,"asOf":"2026-09-17"},"cisaKev":null,"exploitsKnown":0,"affectedPackages":[{"ecosystem":"Packagist","name":"craftcms/cms","fixedVersion":"4.17.3"},{"ecosystem":"Packagist","name":"craftcms/cms","fixedVersion":"5.9.7"}],"fix":{"url":"https://github.com/craftcms/cms/commit/cc9921c14897ee2b592a431c2356af8a04ce4cfe","label":"craftcms/cms@cc9921c"},"references":[{"type":"ADVISORY","url":"https://github.com/CVEProject/cvelistV5/tree/main/cves/2026/31xxx/CVE-2026-31859.json"},{"type":"ADVISORY","url":"https://github.com/craftcms/cms/security/advisories/GHSA-fvwq-45qv-xvhv"},{"type":"ADVISORY","url":"https://nvd.nist.gov/vuln/detail/CVE-2026-31859"},{"type":"WEB","url":"https://github.com/craftcms/cms/commit/cc9921c14897ee2b592a431c2356af8a04ce4cfe"},{"type":"PACKAGE","url":"https://github.com/craftcms/cms"}],"provenance":{"sources":["OSV.dev","FIRST.org (EPSS)"],"lastVerified":"2026-08-12T03:51:09.859404754Z"}}