Medium severityOSV Advisory· Published Aug 12, 2025· Updated Apr 15, 2026
CVE-2025-55166
CVE-2025-55166
Description
savg-sanitizer is a PHP SVG/XML sanitizer. Prior to version 0.22.0, the sanitization logic in the cleanXlinkHrefs method only searches for lower-case attribute name, which allows to by-pass the isHrefSafeValue check. As a result this allows cross-site scripting or linking to external domains. This issue has been patched in version 0.22.0.
Affected packages
Versions sourced from the GitHub Security Advisory.
| Package | Affected versions | Patched versions |
|---|---|---|
enshrined/svg-sanitizePackagist | < 0.22.0 | 0.22.0 |
Affected products
1- Range: 0.1.0, 0.1.1, 0.1.2, …
Patches
20afa95ea74beMerge commit from fork
5 files changed · +62 −23
src/Sanitizer.php+38 −15 modified@@ -421,7 +421,7 @@ protected function cleanAttributesOnWhitelist(\DOMElement $element) * Such as xlink:href when the xlink namespace isn't imported. * We have to do this as the link is still ran in this case. */ - if (false !== strpos($attrName, 'href')) { + if (false !== stripos($attrName, 'href')) { $href = $element->getAttribute($attrName); if (false === $this->isHrefSafeValue($href)) { $element->removeAttribute($attrName); @@ -453,14 +453,17 @@ protected function cleanAttributesOnWhitelist(\DOMElement $element) */ protected function cleanXlinkHrefs(\DOMElement $element) { - $xlinks = $element->getAttributeNS('http://www.w3.org/1999/xlink', 'href'); - if (false === $this->isHrefSafeValue($xlinks)) { - $element->removeAttributeNS( 'http://www.w3.org/1999/xlink', 'href' ); - $this->xmlIssues[] = array( - 'message' => 'Suspicious attribute \'href\'', - 'line' => $element->getLineNo(), - ); + foreach ($element->attributes as $attribute) { + // remove attributes with unexpected namespace prefix, e.g. `XLinK:href` (instead of `xlink:href`) + if ($attribute->prefix === '' && strtolower($attribute->nodeName) === 'xlink:href') { + $element->removeAttribute($attribute->nodeName); + $this->xmlIssues[] = array( + 'message' => sprintf('Unexpected attribute \'%s\'', $attribute->nodeName), + 'line' => $element->getLineNo(), + ); + } } + $this->cleanHrefAttributes($element, 'xlink'); } /** @@ -470,13 +473,33 @@ protected function cleanXlinkHrefs(\DOMElement $element) */ protected function cleanHrefs(\DOMElement $element) { - $href = $element->getAttribute('href'); - if (false === $this->isHrefSafeValue($href)) { - $element->removeAttribute('href'); - $this->xmlIssues[] = array( - 'message' => 'Suspicious attribute \'href\'', - 'line' => $element->getLineNo(), - ); + $this->cleanHrefAttributes($element); + } + + protected function cleanHrefAttributes(\DOMElement $element, string $prefix = ''): void + { + $relevantAttributes = array_filter( + iterator_to_array($element->attributes), + static function (\DOMAttr $attr) use ($prefix) { + return strtolower($attr->name) === 'href' && strtolower($attr->prefix) === $prefix; + } + ); + foreach ($relevantAttributes as $attribute) { + if (!$this->isHrefSafeValue($attribute->value)) { + $element->removeAttribute($attribute->nodeName); + $this->xmlIssues[] = array( + 'message' => sprintf('Suspicious attribute \'%s\'', $attribute->nodeName), + 'line' => $element->getLineNo(), + ); + continue; + } + // in case the attribute name is `HrEf`/`xlink:HrEf`, adjust it to `href`/`xlink:href` + if (!in_array($attribute->nodeName, $this->allowedAttrs, true) + && in_array(strtolower($attribute->nodeName), $this->allowedAttrs, true) + ) { + $element->removeAttribute($attribute->nodeName); + $element->setAttribute(strtolower($attribute->nodeName), $attribute->value); + } } }
tests/data/hrefCleanOne.svg+11 −4 modified@@ -1,8 +1,15 @@ <svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" height="600px" id="Layer_1" width="600px" x="0px" y="0px" xml:space="preserve"> - <a>test 1</a> - <a>test 2</a> - <a href="#test3">test 3</a> - <a xlink:href="#test">test 4</a> + <a>test 1.a</a> + <a>test 2.a</a> + <a href="#test3">test 3.a</a> + <a xlink:href="#test">test 4.a</a> + + <a>test 1.b</a> + <a>test 2.b</a> + <a>test 2.c</a> + <a href="#test3">test 3.b</a> + <a xlink:href="#test">test 4.b</a> + <a>test 4.c</a> <a>test 5</a> <a>test 6</a>
tests/data/hrefTestOne.svg+11 −4 modified@@ -1,8 +1,15 @@ <svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" height="600px" id="Layer_1" width="600px" x="0px" y="0px" xml:space="preserve"> - <a href="javascript:alert(2)">test 1</a> - <a xlink:href="javascript:alert(2)">test 2</a> - <a href="#test3">test 3</a> - <a xlink:href="#test">test 4</a> + <a href="javascript:alert(2)">test 1.a</a> + <a xlink:href="javascript:alert(2)">test 2.a</a> + <a href="#test3">test 3.a</a> + <a xlink:href="#test">test 4.a</a> + + <a HrEf="javascript:alert(2)">test 1.b</a> + <a xlink:HrEf="javascript:alert(2)">test 2.b</a> + <a XLinK:HrEf="javascript:alert(2)">test 2.c</a> + <a HrEf="#test3">test 3.b</a> + <a xlink:HrEf="#test">test 4.b</a> + <a XLinK:HrEf="#test">test 4.c</a> <a href="data:data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' onload='alert(88)'%3E%3C/svg%3E">test 5</a> <a xlink:href="data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' onload='alert(88)'%3E%3C/svg%3E">test 6</a>
tests/data/svgCleanOne.svg+1 −0 modified@@ -8,5 +8,6 @@ <line fill="none" stroke="#000000" stroke-miterlimit="10" x1="457.114" y1="126.623" x2="458.753" y2="363.508"/> + <line fill="none" stroke="#000000" stroke-miterlimit="10" x1="541.54" y1="299.573" x2="543.179" y2="536.458"/> </svg> \ No newline at end of file
tests/data/svgTestOne.svg+1 −0 modified@@ -11,5 +11,6 @@ <line fill="none" stroke="#000000" stroke-miterlimit="10" x1="457.114" y1="126.623" x2="458.753" y2="363.508"/> <this>shouldn't be here</this> <script>alert(1);</script> +<ScRiPt>alert(1);</ScRiPt> <line fill="none" stroke="#000000" stroke-miterlimit="10" x1="541.54" y1="299.573" x2="543.179" y2="536.458"/> </svg>
5a0a1eaf0c6b[SECURITY] Prevent bypass via mixed-case SVG attributes
5 files changed · +62 −23
src/Sanitizer.php+38 −15 modified@@ -421,7 +421,7 @@ protected function cleanAttributesOnWhitelist(\DOMElement $element) * Such as xlink:href when the xlink namespace isn't imported. * We have to do this as the link is still ran in this case. */ - if (false !== strpos($attrName, 'href')) { + if (false !== stripos($attrName, 'href')) { $href = $element->getAttribute($attrName); if (false === $this->isHrefSafeValue($href)) { $element->removeAttribute($attrName); @@ -453,14 +453,17 @@ protected function cleanAttributesOnWhitelist(\DOMElement $element) */ protected function cleanXlinkHrefs(\DOMElement $element) { - $xlinks = $element->getAttributeNS('http://www.w3.org/1999/xlink', 'href'); - if (false === $this->isHrefSafeValue($xlinks)) { - $element->removeAttributeNS( 'http://www.w3.org/1999/xlink', 'href' ); - $this->xmlIssues[] = array( - 'message' => 'Suspicious attribute \'href\'', - 'line' => $element->getLineNo(), - ); + foreach ($element->attributes as $attribute) { + // remove attributes with unexpected namespace prefix, e.g. `XLinK:href` (instead of `xlink:href`) + if ($attribute->prefix === '' && strtolower($attribute->nodeName) === 'xlink:href') { + $element->removeAttribute($attribute->nodeName); + $this->xmlIssues[] = array( + 'message' => sprintf('Unexpected attribute \'%s\'', $attribute->nodeName), + 'line' => $element->getLineNo(), + ); + } } + $this->cleanHrefAttributes($element, 'xlink'); } /** @@ -470,13 +473,33 @@ protected function cleanXlinkHrefs(\DOMElement $element) */ protected function cleanHrefs(\DOMElement $element) { - $href = $element->getAttribute('href'); - if (false === $this->isHrefSafeValue($href)) { - $element->removeAttribute('href'); - $this->xmlIssues[] = array( - 'message' => 'Suspicious attribute \'href\'', - 'line' => $element->getLineNo(), - ); + $this->cleanHrefAttributes($element); + } + + protected function cleanHrefAttributes(\DOMElement $element, string $prefix = ''): void + { + $relevantAttributes = array_filter( + iterator_to_array($element->attributes), + static function (\DOMAttr $attr) use ($prefix) { + return strtolower($attr->name) === 'href' && strtolower($attr->prefix) === $prefix; + } + ); + foreach ($relevantAttributes as $attribute) { + if (!$this->isHrefSafeValue($attribute->value)) { + $element->removeAttribute($attribute->nodeName); + $this->xmlIssues[] = array( + 'message' => sprintf('Suspicious attribute \'%s\'', $attribute->nodeName), + 'line' => $element->getLineNo(), + ); + continue; + } + // in case the attribute name is `HrEf`/`xlink:HrEf`, adjust it to `href`/`xlink:href` + if (!in_array($attribute->nodeName, $this->allowedAttrs, true) + && in_array(strtolower($attribute->nodeName), $this->allowedAttrs, true) + ) { + $element->removeAttribute($attribute->nodeName); + $element->setAttribute(strtolower($attribute->nodeName), $attribute->value); + } } }
tests/data/hrefCleanOne.svg+11 −4 modified@@ -1,8 +1,15 @@ <svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" height="600px" id="Layer_1" width="600px" x="0px" y="0px" xml:space="preserve"> - <a>test 1</a> - <a>test 2</a> - <a href="#test3">test 3</a> - <a xlink:href="#test">test 4</a> + <a>test 1.a</a> + <a>test 2.a</a> + <a href="#test3">test 3.a</a> + <a xlink:href="#test">test 4.a</a> + + <a>test 1.b</a> + <a>test 2.b</a> + <a>test 2.c</a> + <a href="#test3">test 3.b</a> + <a xlink:href="#test">test 4.b</a> + <a>test 4.c</a> <a>test 5</a> <a>test 6</a>
tests/data/hrefTestOne.svg+11 −4 modified@@ -1,8 +1,15 @@ <svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" height="600px" id="Layer_1" width="600px" x="0px" y="0px" xml:space="preserve"> - <a href="javascript:alert(2)">test 1</a> - <a xlink:href="javascript:alert(2)">test 2</a> - <a href="#test3">test 3</a> - <a xlink:href="#test">test 4</a> + <a href="javascript:alert(2)">test 1.a</a> + <a xlink:href="javascript:alert(2)">test 2.a</a> + <a href="#test3">test 3.a</a> + <a xlink:href="#test">test 4.a</a> + + <a HrEf="javascript:alert(2)">test 1.b</a> + <a xlink:HrEf="javascript:alert(2)">test 2.b</a> + <a XLinK:HrEf="javascript:alert(2)">test 2.c</a> + <a HrEf="#test3">test 3.b</a> + <a xlink:HrEf="#test">test 4.b</a> + <a XLinK:HrEf="#test">test 4.c</a> <a href="data:data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' onload='alert(88)'%3E%3C/svg%3E">test 5</a> <a xlink:href="data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' onload='alert(88)'%3E%3C/svg%3E">test 6</a>
tests/data/svgCleanOne.svg+1 −0 modified@@ -8,5 +8,6 @@ <line fill="none" stroke="#000000" stroke-miterlimit="10" x1="457.114" y1="126.623" x2="458.753" y2="363.508"/> + <line fill="none" stroke="#000000" stroke-miterlimit="10" x1="541.54" y1="299.573" x2="543.179" y2="536.458"/> </svg> \ No newline at end of file
tests/data/svgTestOne.svg+1 −0 modified@@ -11,5 +11,6 @@ <line fill="none" stroke="#000000" stroke-miterlimit="10" x1="457.114" y1="126.623" x2="458.753" y2="363.508"/> <this>shouldn't be here</this> <script>alert(1);</script> +<ScRiPt>alert(1);</ScRiPt> <line fill="none" stroke="#000000" stroke-miterlimit="10" x1="541.54" y1="299.573" x2="543.179" y2="536.458"/> </svg>
Vulnerability mechanics
Generated by null/stub on May 9, 2026. Inputs: CWE entries + fix-commit diffs from this CVE's patches. Citations validated against bundle.
References
7- github.com/advisories/GHSA-22wq-q86m-83fhghsaADVISORY
- nvd.nist.gov/vuln/detail/CVE-2025-55166ghsaADVISORY
- github.com/darylldoyle/svg-sanitizer/blob/0.21.0/src/Sanitizer.phpghsaWEB
- github.com/darylldoyle/svg-sanitizer/commit/0afa95ea74be155a7bcd6c6fb60c276c39984500ghsaWEB
- github.com/darylldoyle/svg-sanitizer/commit/5a0a1eaf0c6b0b540dc945fe30c93cf106b357c1nvdWEB
- github.com/darylldoyle/svg-sanitizer/releases/tag/0.22.0ghsaWEB
- github.com/darylldoyle/svg-sanitizer/security/advisories/GHSA-22wq-q86m-83fhnvdWEB
News mentions
0No linked articles in our index yet.