Affected versions
Symfony HTML Sanitizer versions >=6.1, <6.4.50, >=7, <7.4.23, >=8, <8.1.11 are affected by this security issue.
The issue has been fixed in Symfony HTML Sanitizer 6.4.50, 7.4.23, 8.1.11.
Description
The MetaRefreshAttributeSanitizer introduced for CVE-2026-48761 sanitizes the refresh URL only when the content attribute matches:
1
/^(\s*\d+\s*[;,]\s*url\s*=\s*)(["\']?)(.+?)\2(\s*)$/i
The browser grammar, the WHATWG shared declarative refresh steps, is broader: the separator after the time may be ASCII whitespace alone, with no ; or ,, and the url= keyword is optional. Directives the browser honours but the regex misses are returned unchanged, so the attacker URL skips UrlSanitizer entirely and both allowLinkSchemes and allowLinkHosts are defeated for the refresh navigation.
1 2 3 4 5 6 7 8 9 10
$c = (new HtmlSanitizerConfig())->allowStaticElements()->allowLinkHosts(['trusted.example']);
$s = new HtmlSanitizer($c);
$s->sanitizeFor('head', '<meta http-equiv="refresh" content="0;url=https://evil.example/">');
// <meta http-equiv="refresh" />, correct
$s->sanitizeFor('head', '<meta http-equiv="refresh" content="0 url=https://evil.example/">');
$s->sanitizeFor('head', '<meta http-equiv="refresh" content="0;https://evil.example/">');
$s->sanitizeFor('head', '<meta http-equiv="refresh" content="9 , https://evil.example/">');
// URL survives in all three, host allow-list defeated
The rendered = is entity-decoded back to = by the HTML parser before the refresh algorithm runs, so the browser sees a valid directive. Because the URL never reaches UrlSanitizer, a javascript: refresh URL is not filtered either.
Three further spellings were found while fixing and are covered by the same patch: 0,https://evil.example/, .5;url=https://evil.example/ with an empty integer part, and the specification's incomplete-url= quirk 0;uhttps://evil.example/.
This is reachable under any configuration that allows meta with http-equiv and content, including the documented allowStaticElements() preset.
Resolution
The sanitizer now parses the value following the WHATWG shared declarative refresh steps, and it fails safe: once a value looks like a refresh directive, any parse that does not end in a sanitized URL causes the attribute to be dropped rather than passed through.
Reviewers should note one unavoidable consequence. sanitizeAttribute() receives only the element name and the attribute value, never http-equiv, so every <meta content> value must be treated alike. A value such as <meta name="description" content="10 tips for cooking"> is now dropped, or rewritten as a relative URL when allowRelativeLinks() is enabled, because that same string on an http-equiv="refresh" element does make a browser navigate. The collateral is bounded to meta content values beginning with a digit or a full stop.
The patches for this issue are available here for branch 6.4 (and forward-ported to 7.4, 8.1 and 8.2).
Credits
We would like to thank Ilia Alshanetsky for reporting the issue.