CVE-2012-4451
Description
Multiple XSS vulnerabilities in Zend Framework 2.0.x before 2.0.1 allow remote attackers to inject arbitrary web script or HTML via unsanitized input in several components.
AI Insight
LLM-synthesized narrative grounded in this CVE's description and references.
Multiple XSS vulnerabilities in Zend Framework 2.0.x before 2.0.1 allow remote attackers to inject arbitrary web script or HTML via unsanitized input in several components.
Vulnerability
Multiple cross-site scripting (XSS) vulnerabilities exist in Zend Framework 2.0.x versions prior to 2.0.1. The affected components—Zend\Debug, Zend\Feed\PubSubHubbub, Zend\Log\Formatter\Xml, Zend\Tag\Cloud\Decorator, Zend\Uri, Zend\View\Helper\HeadStyle, Zend\View\Helper\Navigation\Sitemap, and Zend\View\Helper\Placeholder\Container\AbstractStandalone—failed to use context-appropriate escaping via Zend\Escaper\Escaper when rendering HTML, HTML attributes, or URLs [3][4]. While some escaping was performed, it was not context-aware, leaving the components open to XSS injection.
Exploitation
An attacker can supply crafted input to any of the listed components. Because the input is not properly sanitized before being returned to the user, the attacker can inject arbitrary HTML and script code that executes in the victim's browser session [3][4]. No special privileges or user interaction beyond normal browsing is required; the attack vector is remote and can be triggered by simply visiting a page that renders the attacker-controlled data through one of the vulnerable components.
Impact
Successful exploitation allows an attacker to execute arbitrary web script or HTML in the context of the affected site. This can lead to information disclosure, session hijacking, defacement, or other malicious actions typically associated with stored or reflected XSS [2][3]. The impact is limited to the browser session of the victim, but the attacker can potentially access sensitive data or perform actions on behalf of the user.
Mitigation
The vulnerability is fixed in Zend Framework version 2.0.1 and later, including the 2.1 development branch [3]. Users should upgrade to 2.0.1 or greater immediately. No workarounds are documented; the patch involves composing the Zend\Escaper\Escaper class as an injectable dependency and using its context-appropriate escaping methods [1]. The issue is tracked as ZF2012-03 [3] and has been assigned CVE-2012-4451.
AI Insight generated on May 24, 2026. Synthesized from this CVE's description and the cited reference URLs; citations are validated against the source bundle.
Affected products
2- Range: 2.0.x before 2.0.1
Patches
127131ca9520bMerge branch 'security/escaper-usage'
22 files changed · +579 −275
library/Zend/Debug/composer.json+6 −2 modified@@ -13,6 +13,10 @@ }, "target-dir": "Zend/Debug", "require": { - "php": ">=5.3.3" + "php": ">=5.3.3", + "zendframework/zend-escaper": "self.version" + }, + "suggest": { + "ext/xdebug": "XDebug, for better backtrace output" } -} \ No newline at end of file +}
library/Zend/Debug/Debug.php+32 −1 modified@@ -10,6 +10,8 @@ namespace Zend\Debug; +use Zend\Escaper\Escaper; + /** * Concrete class for generating debug dumps related to the output source. * @@ -18,6 +20,10 @@ */ class Debug { + /** + * @var Escaper + */ + protected static $escaper = null; /** * @var string @@ -50,6 +56,31 @@ public static function setSapi($sapi) self::$sapi = $sapi; } + /** + * Set Escaper instance + * + * @param Escaper $escaper + */ + public static function setEscaper(Escaper $escaper) + { + static::$escaper = $escaper; + } + + /** + * Get Escaper instance + * + * Lazy loads an instance if none provided. + * + * @return Escaper + */ + public static function getEscaper() + { + if (null === static::$escaper) { + static::setEscaper(new Escaper()); + } + return static::$escaper; + } + /** * Debug helper function. This is a wrapper for var_dump() that adds * the <pre /> tags, cleans up newlines and indents, and runs @@ -78,7 +109,7 @@ public static function dump($var, $label=null, $echo=true) . PHP_EOL; } else { if (!extension_loaded('xdebug')) { - $output = htmlspecialchars($output, ENT_QUOTES); + $output = static::getEscaper()->escapeHtml($output); } $output = '<pre>'
library/Zend/Feed/composer.json+1 −0 modified@@ -14,6 +14,7 @@ "target-dir": "Zend/Feed", "require": { "php": ">=5.3.3", + "zendframework/zend-escaper": "self.version", "zendframework/zend-stdlib": "self.version" }, "suggest": {
library/Zend/Feed/PubSubHubbub/PubSubHubbub.php+41 −7 modified@@ -10,6 +10,7 @@ namespace Zend\Feed\PubSubHubbub; +use Zend\Escaper\Escaper; use Zend\Feed\Reader; use Zend\Http; @@ -32,10 +33,15 @@ class PubSubHubbub const SUBSCRIPTION_NOTVERIFIED = 'not_verified'; const SUBSCRIPTION_TODELETE = 'to_delete'; + /** + * @var Escaper + */ + protected static $escaper; + /** * Singleton instance if required of the HTTP client * - * @var \Zend\Http\Client + * @var Http\Client */ protected static $httpClient = null; @@ -67,7 +73,7 @@ public static function detectHubs($source) * Allows the external environment to make Zend_Oauth use a specific * Client instance. * - * @param \Zend\Http\Client $httpClient + * @param Http\Client $httpClient * @return void */ public static function setHttpClient(Http\Client $httpClient) @@ -80,15 +86,15 @@ public static function setHttpClient(Http\Client $httpClient) * the instance is reset and cleared of previous parameters GET/POST. * Headers are NOT reset but handled by this component if applicable. * - * @return \Zend\Http\Client + * @return Http\Client */ public static function getHttpClient() { - if (!isset(self::$httpClient)): + if (!isset(self::$httpClient)) { self::$httpClient = new Http\Client; - else: + } else { self::$httpClient->resetParameters(); - endif; + } return self::$httpClient; } @@ -103,6 +109,33 @@ public static function clearHttpClient() self::$httpClient = null; } + /** + * Set the Escaper instance + * + * If null, resets the instance + * + * @param null|Escaper $escaper + */ + public static function setEscaper(Escaper $escaper = null) + { + static::$escaper = $escaper; + } + + /** + * Get the Escaper instance + * + * If none registered, lazy-loads an instance. + * + * @return Escaper + */ + public static function getEscaper() + { + if (null === static::$escaper) { + static::setEscaper(new Escaper()); + } + return static::$escaper; + } + /** * RFC 3986 safe url encoding method * @@ -111,7 +144,8 @@ public static function clearHttpClient() */ public static function urlencode($string) { - $rawencoded = rawurlencode($string); + $escaper = static::getEscaper(); + $rawencoded = $escaper->escapeUrl($string); $rfcencoded = str_replace('%7E', '~', $rawencoded); return $rfcencoded; }
library/Zend/Log/composer.json+1 −0 modified@@ -20,6 +20,7 @@ "suggest": { "ext-mongo": "*", "zendframework/zend-db": "Zend\\Db component", + "zendframework/zend-escaper": "Zend\\Escaper component, for use in the XML formatter", "zendframework/zend-mail": "Zend\\Mail component", "zendframework/zend-validator": "Zend\\Validator component" }
library/Zend/Log/Formatter/Xml.php+38 −4 modified@@ -14,6 +14,7 @@ use DOMDocument; use DOMElement; use Traversable; +use Zend\Escaper\Escaper; use Zend\Stdlib\ArrayUtils; /** @@ -38,6 +39,11 @@ class Xml implements FormatterInterface */ protected $encoding; + /** + * @var Escaper instance + */ + protected $escaper; + /** * Format specifier for DateTime objects in event data (default: ISO 8601) * @@ -121,6 +127,33 @@ public function setEncoding($value) return $this; } + /** + * Set Escaper instance + * + * @param Escaper $escaper + * @return Xml + */ + public function setEscaper(Escaper $escaper) + { + $this->escaper = $escaper; + return $this; + } + + /** + * Get Escaper instance + * + * Lazy-loads an instance with the current encoding if none registered. + * + * @return Escaper + */ + public function getEscaper() + { + if (null === $this->escaper) { + $this->setEscaper(new Escaper($this->getEncoding())); + } + return $this->escaper; + } + /** * Formats data into a single line to be written by the writer. * @@ -142,17 +175,18 @@ public function format($event) } } - $enc = $this->getEncoding(); - $dom = new DOMDocument('1.0', $enc); - $elt = $dom->appendChild(new DOMElement($this->rootElement)); + $enc = $this->getEncoding(); + $escaper = $this->getEscaper(); + $dom = new DOMDocument('1.0', $enc); + $elt = $dom->appendChild(new DOMElement($this->rootElement)); foreach ($dataToInsert as $key => $value) { if (empty($value) || is_scalar($value) || (is_object($value) && method_exists($value,'__toString')) ) { if ($key == "message") { - $value = htmlspecialchars($value, ENT_COMPAT, $enc); + $value = $escaper->escapeHtml($value); } elseif ($key == "extra" && empty($value)) { continue; }
library/Zend/Tag/Cloud/Decorator/AbstractCloud.php+1 −51 modified@@ -10,62 +10,12 @@ namespace Zend\Tag\Cloud\Decorator; -use Traversable; -use Zend\Stdlib\ArrayUtils; -use Zend\Tag\Cloud\Decorator\DecoratorInterface as Decorator; - /** * Abstract class for cloud decorators * * @category Zend * @package Zend_Tag */ -abstract class AbstractCloud implements Decorator +abstract class AbstractCloud extends AbstractDecorator { - /** - * Option keys to skip when calling setOptions() - * - * @var array - */ - protected $skipOptions = array( - 'options', - 'config', - ); - - /** - * Create a new cloud decorator with options - * - * @param array|Traversable $options - */ - public function __construct($options = null) - { - if ($options instanceof Traversable) { - $options = ArrayUtils::iteratorToArray($options); - } - if (is_array($options)) { - $this->setOptions($options); - } - } - - /** - * Set options from array - * - * @param array $options Configuration for the decorator - * @return AbstractCloud - */ - public function setOptions(array $options) - { - foreach ($options as $key => $value) { - if (in_array(strtolower($key), $this->skipOptions)) { - continue; - } - - $method = 'set' . $key; - if (method_exists($this, $method)) { - $this->$method($value); - } - } - - return $this; - } }
library/Zend/Tag/Cloud/Decorator/AbstractDecorator.php+190 −0 added@@ -0,0 +1,190 @@ +<?php +/** + * Zend Framework (http://framework.zend.com/) + * + * @link http://github.com/zendframework/zf2 for the canonical source repository + * @copyright Copyright (c) 2005-2012 Zend Technologies USA Inc. (http://www.zend.com) + * @license http://framework.zend.com/license/new-bsd New BSD License + * @package Zend_Tag + */ + +namespace Zend\Tag\Cloud\Decorator; + +use Traversable; +use Zend\Escaper\Escaper; +use Zend\Stdlib\ArrayUtils; +use Zend\Tag\Cloud\Decorator\DecoratorInterface as Decorator; +use Zend\Tag\Exception; + +/** + * Abstract class for decorators + * + * @category Zend + * @package Zend_Tag + */ +abstract class AbstractDecorator implements Decorator +{ + /** + * @var string Encoding to use + */ + protected $encoding = 'UTF-8'; + + /** + * @var Escaper + */ + protected $escaper; + + /** + * Option keys to skip when calling setOptions() + * + * @var array + */ + protected $skipOptions = array( + 'options', + 'config', + ); + + /** + * Create a new decorator with options + * + * @param array|Traversable $options + */ + public function __construct($options = null) + { + if ($options instanceof Traversable) { + $options = ArrayUtils::iteratorToArray($options); + } + if (is_array($options)) { + $this->setOptions($options); + } + } + + /** + * Set options from array + * + * @param array $options Configuration for the decorator + * @return AbstractTag + */ + public function setOptions(array $options) + { + foreach ($options as $key => $value) { + if (in_array(strtolower($key), $this->skipOptions)) { + continue; + } + + $method = 'set' . $key; + if (method_exists($this, $method)) { + $this->$method($value); + } + } + + return $this; + } + + /** + * Get encoding + * + * @return string + */ + public function getEncoding() + { + return $this->encoding; + } + + /** + * Set encoding + * + * @param string + * @return HTMLCloud + */ + public function setEncoding($value) + { + $this->encoding = (string) $value; + return $this; + } + + /** + * Set Escaper instance + * + * @param Escaper $escaper + * @return HtmlCloud + */ + public function setEscaper($escaper) + { + $this->escaper = $escaper; + return $this; + } + + /** + * Retrieve Escaper instance + * + * If none registered, instantiates and registers one using current encoding. + * + * @return Escaper + */ + public function getEscaper() + { + if (null === $this->escaper) { + $this->setEscaper(new Escaper($this->getEncoding())); + } + return $this->escaper; + } + + /** + * Validate an HTML element name + * + * @param string $name + * @throws Exception\InvalidElementNameException + */ + protected function validateElementName($name) + { + if (!preg_match('/^[a-z0-9]+$/i', $name)) { + throw new Exception\InvalidElementNameException(sprintf( + '%s: Invalid element name "%s" provided; please provide valid HTML element names', + __METHOD__, + $this->getEscaper()->escapeHtml($name) + )); + } + } + + /** + * Validate an HTML attribute name + * + * @param string $name + * @throws Exception\InvalidAttributeNameException + */ + protected function validateAttributeName($name) + { + if (!preg_match('/^[a-z_:][-a-z0-9_:.]*$/i', $name)) { + throw new Exception\InvalidAttributeNameException(sprintf( + '%s: Invalid HTML attribute name "%s" provided; please provide valid HTML attribute names', + __METHOD__, + $this->getEscaper()->escapeHtml($name) + )); + } + } + + protected function wrapTag($html) + { + $escaper = $this->getEscaper(); + foreach ($this->getHTMLTags() as $key => $data) { + if (is_array($data)) { + $attributes = ''; + $htmlTag = $key; + $this->validateElementName($htmlTag); + + foreach ($data as $param => $value) { + $this->validateAttributeName($param); + $attributes .= ' ' . $param . '="' . $escaper->escapeHtmlAttr($value) . '"'; + } + } else { + $attributes = ''; + $htmlTag = $data; + $this->validateElementName($htmlTag); + } + + $html = sprintf('<%1$s%3$s>%2$s</%1$s>', $htmlTag, $html, $attributes); + } + return $html; + } +}
library/Zend/Tag/Cloud/Decorator/AbstractTag.php+1 −51 modified@@ -10,62 +10,12 @@ namespace Zend\Tag\Cloud\Decorator; -use Traversable; -use Zend\Stdlib\ArrayUtils; -use Zend\Tag\Cloud\Decorator\DecoratorInterface as Decorator; - /** * Abstract class for tag decorators * * @category Zend * @package Zend_Tag */ -abstract class AbstractTag implements Decorator +abstract class AbstractTag extends AbstractDecorator { - /** - * Option keys to skip when calling setOptions() - * - * @var array - */ - protected $skipOptions = array( - 'options', - 'config', - ); - - /** - * Create a new cloud decorator with options - * - * @param array|Traversable $options - */ - public function __construct($options = null) - { - if ($options instanceof Traversable) { - $options = ArrayUtils::iteratorToArray($options); - } - if (is_array($options)) { - $this->setOptions($options); - } - } - - /** - * Set options from array - * - * @param array $options Configuration for the decorator - * @return AbstractTag - */ - public function setOptions(array $options) - { - foreach ($options as $key => $value) { - if (in_array(strtolower($key), $this->skipOptions)) { - continue; - } - - $method = 'set' . $key; - if (method_exists($this, $method)) { - $this->$method($value); - } - } - - return $this; - } }
library/Zend/Tag/Cloud/Decorator/HtmlCloud.php+1 −45 modified@@ -18,11 +18,6 @@ */ class HtmlCloud extends AbstractCloud { - /** - * @var string Encoding to use - */ - protected $encoding = 'UTF-8'; - /** * List of HTML tags * @@ -39,28 +34,6 @@ class HtmlCloud extends AbstractCloud */ protected $separator = ' '; - /** - * Get encoding - * - * @return string - */ - public function getEncoding() - { - return $this->encoding; - } - - /** - * Set encoding - * - * @param string - * @return HTMLCloud - */ - public function setEncoding($value) - { - $this->encoding = (string) $value; - return $this; - } - /** * Set the HTML tags surrounding all tags * @@ -121,24 +94,7 @@ public function render($tags) )); } $cloudHTML = implode($this->getSeparator(), $tags); - - $enc = $this->getEncoding(); - foreach ($this->getHTMLTags() as $key => $data) { - if (is_array($data)) { - $htmlTag = $key; - $attributes = ''; - - foreach ($data as $param => $value) { - $attributes .= ' ' . $param . '="' . htmlspecialchars($value, ENT_COMPAT, $enc) . '"'; - } - } else { - $htmlTag = $data; - $attributes = ''; - } - - $cloudHTML = sprintf('<%1$s%3$s>%2$s</%1$s>', $htmlTag, $cloudHTML, $attributes); - } - + $cloudHTML = $this->wrapTag($cloudHTML); return $cloudHTML; } }
library/Zend/Tag/Cloud/Decorator/HtmlTag.php+4 −47 modified@@ -29,11 +29,6 @@ class HtmlTag extends AbstractTag */ protected $classList = null; - /** - * @var string Encoding to utilize - */ - protected $encoding = 'UTF-8'; - /** * Unit for the fontsize * @@ -107,28 +102,6 @@ public function getClassList() return $this->classList; } - /** - * Get encoding - * - * @return string - */ - public function getEncoding() - { - return $this->encoding; - } - - /** - * Set encoding - * - * @param string $value - * @return HTMLTag - */ - public function setEncoding($value) - { - $this->encoding = (string) $value; - return $this; - } - /** * Set the font size unit * @@ -259,32 +232,16 @@ public function render($tags) $result = array(); - $enc = $this->getEncoding(); + $escaper = $this->getEscaper(); foreach ($tags as $tag) { if (null === ($classList = $this->getClassList())) { $attribute = sprintf('style="font-size: %d%s;"', $tag->getParam('weightValue'), $this->getFontSizeUnit()); } else { - $attribute = sprintf('class="%s"', htmlspecialchars($tag->getParam('weightValue'), ENT_COMPAT, $enc)); - } - - $tagHTML = sprintf('<a href="%s" %s>%s</a>', htmlSpecialChars($tag->getParam('url'), ENT_COMPAT, $enc), $attribute, $tag->getTitle()); - - foreach ($this->getHTMLTags() as $key => $data) { - if (is_array($data)) { - $htmlTag = $key; - $attributes = ''; - - foreach ($data as $param => $value) { - $attributes .= ' ' . $param . '="' . htmlspecialchars($value, ENT_COMPAT, $enc) . '"'; - } - } else { - $htmlTag = $data; - $attributes = ''; - } - - $tagHTML = sprintf('<%1$s%3$s>%2$s</%1$s>', $htmlTag, $tagHTML, $attributes); + $attribute = sprintf('class="%s"', $escaper->escapeHtmlAttr($tag->getParam('weightValue'))); } + $tagHTML = sprintf('<a href="%s" %s>%s</a>', $escaper->escapeHtml($tag->getParam('url')), $attribute, $escaper->escapeHtml($tag->getTitle())); + $tagHTML = $this->wrapTag($tagHTML); $result[] = $tagHTML; }
library/Zend/Tag/composer.json+3 −2 modified@@ -13,6 +13,7 @@ }, "target-dir": "Zend/Tag", "require": { - "php": ">=5.3.3" + "php": ">=5.3.3", + "zendframework/zend-escaper": "self.version" } -} \ No newline at end of file +}
library/Zend/Tag/Exception/InvalidAttributeNameException.php+16 −0 added@@ -0,0 +1,16 @@ +<?php +/** + * Zend Framework (http://framework.zend.com/) + * + * @link http://github.com/zendframework/zf2 for the canonical source repository + * @copyright Copyright (c) 2005-2012 Zend Technologies USA Inc. (http://www.zend.com) + * @license http://framework.zend.com/license/new-bsd New BSD License + * @package Zend_Tag + */ + +namespace Zend\Tag\Exception; + +use DomainException; + +class InvalidAttributeNameException extends DomainException implements ExceptionInterface +{}
library/Zend/Tag/Exception/InvalidElementNameException.php+16 −0 added@@ -0,0 +1,16 @@ +<?php +/** + * Zend Framework (http://framework.zend.com/) + * + * @link http://github.com/zendframework/zf2 for the canonical source repository + * @copyright Copyright (c) 2005-2012 Zend Technologies USA Inc. (http://www.zend.com) + * @license http://framework.zend.com/license/new-bsd New BSD License + * @package Zend_Tag + */ + +namespace Zend\Tag\Exception; + +use DomainException; + +class InvalidElementNameException extends DomainException implements ExceptionInterface +{}
library/Zend/Uri/composer.json+2 −1 modified@@ -14,6 +14,7 @@ "target-dir": "Zend/Uri", "require": { "php": ">=5.3.3", + "zendframework/zend-escaper": "self.version", "zendframework/zend-validator": "self.version" } -} \ No newline at end of file +}
library/Zend/Uri/Uri.php+40 −6 modified@@ -10,6 +10,7 @@ namespace Zend\Uri; +use Zend\Escaper\Escaper; use Zend\Validator; /** @@ -125,6 +126,11 @@ class Uri implements UriInterface */ protected static $defaultPorts = array(); + /** + * @var Escaper + */ + protected static $escaper; + /** * Create a new URI object * @@ -152,6 +158,31 @@ public function __construct($uri = null) } } + /** + * Set Escaper instance + * + * @param Escaper $escaper + */ + public static function setEscaper(Escaper $escaper) + { + static::$escaper = $escaper; + } + + /** + * Retrieve Escaper instance + * + * Lazy-loads one if none provided + * + * @return Escaper + */ + public static function getEscaper() + { + if (null === static::$escaper) { + static::setEscaper(new Escaper()); + } + return static::$escaper; + } + /** * Check if the URI is valid * @@ -935,8 +966,9 @@ public static function encodeUserInfo($userInfo) } $regex = '/(?:[^' . self::CHAR_UNRESERVED . self::CHAR_SUB_DELIMS . '%:]|%(?![A-Fa-f0-9]{2}))/'; - $replace = function($match) { - return rawurlencode($match[0]); + $escaper = static::getEscaper(); + $replace = function ($match) use ($escaper) { + return $escaper->escapeUrl($match[0]); }; return preg_replace_callback($regex, $replace, $userInfo); @@ -962,8 +994,9 @@ public static function encodePath($path) } $regex = '/(?:[^' . self::CHAR_UNRESERVED . ':@&=\+\$,\/;%]+|%(?![A-Fa-f0-9]{2}))/'; - $replace = function($match) { - return rawurlencode($match[0]); + $escaper = static::getEscaper(); + $replace = function ($match) use ($escaper) { + return $escaper->escapeUrl($match[0]); }; return preg_replace_callback($regex, $replace, $path); @@ -990,8 +1023,9 @@ public static function encodeQueryFragment($input) } $regex = '/(?:[^' . self::CHAR_UNRESERVED . self::CHAR_SUB_DELIMS . '%:@\/\?]+|%(?![A-Fa-f0-9]{2}))/'; - $replace = function($match) { - return rawurlencode($match[0]); + $escaper = static::getEscaper(); + $replace = function ($match) use ($escaper) { + return $escaper->escapeUrl($match[0]); }; return preg_replace_callback($regex, $replace, $input);
library/Zend/View/Helper/HeadStyle.php+2 −1 modified@@ -311,6 +311,7 @@ public function itemToString(stdClass $item, $indent) ) { $enc = $this->view->getEncoding(); } + $escaper = $this->getEscaper($enc); foreach ($item->attributes as $key => $value) { if (!in_array($key, $this->optionalAttributes)) { continue; @@ -333,7 +334,7 @@ public function itemToString(stdClass $item, $indent) $value = substr($value, 0, -1); } } - $attrString .= sprintf(' %s="%s"', $key, htmlspecialchars($value, ENT_COMPAT, $enc)); + $attrString .= sprintf(' %s="%s"', $key, $escaper->escapeHtmlAttr($value)); } }
library/Zend/View/Helper/Navigation/Sitemap.php+2 −8 modified@@ -242,14 +242,8 @@ public function getServerUrl() */ protected function xmlEscape($string) { - $enc = 'UTF-8'; - if ($this->view instanceof View\Renderer\RendererInterface - && method_exists($this->view, 'getEncoding') - ) { - $enc = $this->view->getEncoding(); - } - - return htmlspecialchars($string, ENT_QUOTES, $enc, false); + $escaper = $this->view->plugin('escapeHtml'); + return $escaper($string); } // Public methods:
library/Zend/View/Helper/Placeholder/Container/AbstractStandalone.php+40 −11 modified@@ -10,8 +10,10 @@ namespace Zend\View\Helper\Placeholder\Container; +use Zend\Escaper\Escaper; use Zend\View\Exception; use Zend\View\Helper\Placeholder\Registry; +use Zend\View\Renderer\RendererInterface; /** * Base class for targeted placeholder helpers @@ -28,6 +30,11 @@ abstract class AbstractStandalone */ protected $container; + /** + * @var Escaper[] + */ + protected $escapers = array(); + /** * @var \Zend\View\Helper\Placeholder\Registry */ @@ -78,6 +85,35 @@ public function setRegistry(Registry $registry) return $this; } + /** + * Set Escaper instance + * + * @param Escaper $escaper + * @return AbstractStandalone + */ + public function setEscaper(Escaper $escaper) + { + $encoding = $escaper->getEncoding(); + $this->escapers[$encoding] = $escaper; + return $this; + } + + /** + * Get Escaper instance + * + * Lazy-loads one if none available + * + * @return mixed + */ + public function getEscaper($enc = 'UTF-8') + { + $enc = strtolower($enc); + if (!isset($this->escapers[$enc])) { + $this->setEscaper(new Escaper($enc)); + } + return $this->escapers[$enc]; + } + /** * Set whether or not auto escaping should be used * @@ -108,23 +144,16 @@ public function getAutoEscape() */ protected function escape($string) { - $enc = 'UTF-8'; - if ($this->view instanceof \Zend\View\Renderer\RendererInterface + if ($this->view instanceof RendererInterface && method_exists($this->view, 'getEncoding') ) { - $enc = $this->view->getEncoding(); + $enc = $this->view->getEncoding(); $escaper = $this->view->plugin('escapeHtml'); return $escaper((string) $string); } - /** - * bump this out to a protected method to kill the instance penalty! - */ - $escaper = new \Zend\Escaper\Escaper($enc); + + $escaper = $this->getEscaper(); return $escaper->escapeHtml((string) $string); - /** - * Replaced to ensure consistent escaping - */ - //return htmlspecialchars((string) $string, ENT_COMPAT, $enc); } /**
tests/ZendTest/Tag/Cloud/CloudTest.php+2 −2 modified@@ -208,7 +208,7 @@ public function testSkipOptions() public function testRender() { $cloud = $this->_getCloud(array('tags' => array(array('title' => 'foo', 'weight' => 1), array('title' => 'bar', 'weight' => 3)))); - $expected = '<ul class="Zend\Tag\Cloud">' + $expected = '<ul class="Zend\Tag\Cloud">' . '<li><a href="" style="font-size: 10px;">foo</a></li> ' . '<li><a href="" style="font-size: 20px;">bar</a></li>' . '</ul>'; @@ -224,7 +224,7 @@ public function testRenderEmptyCloud() public function testRenderViaToString() { $cloud = $this->_getCloud(array('tags' => array(array('title' => 'foo', 'weight' => 1), array('title' => 'bar', 'weight' => 3)))); - $expected = '<ul class="Zend\Tag\Cloud">' + $expected = '<ul class="Zend\Tag\Cloud">' . '<li><a href="" style="font-size: 10px;">foo</a></li> ' . '<li><a href="" style="font-size: 20px;">bar</a></li>' . '</ul>';
tests/ZendTest/Tag/Cloud/Decorator/HtmlCloudTest.php+58 −3 modified@@ -25,7 +25,7 @@ public function testDefaultOutput() { $decorator = new Decorator\HtmlCloud(); - $this->assertEquals('<ul class="Zend\Tag\Cloud">foo bar</ul>', $decorator->render(array('foo', 'bar'))); + $this->assertEquals('<ul class="Zend\Tag\Cloud">foo bar</ul>', $decorator->render(array('foo', 'bar'))); } public function testNestedTags() @@ -41,7 +41,7 @@ public function testSeparator() $decorator = new Decorator\HtmlCloud(); $decorator->setSeparator('-'); - $this->assertEquals('<ul class="Zend\Tag\Cloud">foo-bar</ul>', $decorator->render(array('foo', 'bar'))); + $this->assertEquals('<ul class="Zend\Tag\Cloud">foo-bar</ul>', $decorator->render(array('foo', 'bar'))); } public function testConstructorWithArray() @@ -71,5 +71,60 @@ public function testSkipOptions() $decorator = new Decorator\HtmlCloud(array('options' => 'foobar')); // In case would fail due to an error } -} + public function invalidHtmlTagProvider() + { + return array( + array(array('_foo')), + array(array('&foo')), + array(array(' foo')), + array(array(' foo')), + array(array( + '_foo' => array(), + )), + ); + } + + /** + * @dataProvider invalidHtmlTagProvider + */ + public function testInvalidHtmlTagsRaiseAnException($tags) + { + $decorator = new Decorator\HtmlCloud(); + $decorator->setHTMLTags($tags); + $this->setExpectedException('Zend\Tag\Exception\InvalidElementNameException'); + $decorator->render(array()); + } + + public function invalidAttributeProvider() + { + return array( + array(array( + 'foo' => array( + '&bar' => 'baz', + ), + )), + array(array( + 'foo' => array( + ':bar&baz' => 'bat', + ), + )), + array(array( + 'foo' => array( + 'bar/baz' => 'bat', + ), + )), + ); + } + + /** + * @dataProvider invalidAttributeProvider + */ + public function testInvalidAttributeNamesRaiseAnException($tags) + { + $decorator = new Decorator\HtmlCloud(); + $decorator->setHTMLTags($tags); + $this->setExpectedException('Zend\Tag\Exception\InvalidAttributeNameException'); + $decorator->render(array()); + } +}
tests/ZendTest/Tag/Cloud/Decorator/HtmlTagTest.php+82 −33 modified@@ -10,9 +10,9 @@ namespace ZendTest\Tag\Cloud\Decorator; -use Zend\Tag, - Zend\Tag\Cloud\Decorator, - Zend\Tag\Cloud\Decorator\Exception\InvalidArgumentException; +use Zend\Tag; +use Zend\Tag\Cloud\Decorator; +use Zend\Tag\Cloud\Decorator\Exception\InvalidArgumentException; /** * @category Zend @@ -74,60 +74,40 @@ public function testEmptyClassList() { $decorator = new Decorator\HtmlTag(); - try { - $decorator->setClassList(array()); - $this->fail('An expected Zend\Tag\Cloud\Decorator\Exception\InvalidArgumentException was not raised'); - } catch (InvalidArgumentException $e) { - $this->assertEquals($e->getMessage(), 'Classlist is empty'); - } + $this->setExpectedException('Zend\Tag\Cloud\Decorator\Exception\InvalidArgumentException', 'Classlist is empty'); + $decorator->setClassList(array()); } public function testInvalidClassList() { $decorator = new Decorator\HtmlTag(); - try { - $decorator->setClassList(array(array())); - $this->fail('An expected Zend\Tag\Cloud\Decorator\Exception\InvalidArgumentException was not raised'); - } catch (InvalidArgumentException $e) { - $this->assertEquals($e->getMessage(), 'Classlist contains an invalid classname'); - } + $this->setExpectedException('Zend\Tag\Cloud\Decorator\Exception\InvalidArgumentException', 'Classlist contains an invalid classname'); + $decorator->setClassList(array(array())); } public function testInvalidFontSizeUnit() { $decorator = new Decorator\HtmlTag(); - try { - $decorator->setFontSizeUnit('foo'); - $this->fail('An expected Zend\Tag\Cloud\Decorator\Exception\InvalidArgumentException was not raised'); - } catch (InvalidArgumentException $e) { - $this->assertEquals($e->getMessage(), 'Invalid fontsize unit specified'); - } + $this->setExpectedException('Zend\Tag\Cloud\Decorator\Exception\InvalidArgumentException', 'Invalid fontsize unit specified'); + $decorator->setFontSizeUnit('foo'); } public function testInvalidMinFontSize() { $decorator = new Decorator\HtmlTag(); - try { - $decorator->setMinFontSize('foo'); - $this->fail('An expected Zend\Tag\Cloud\Decorator\Exception\InvalidArgumentException was not raised'); - } catch (InvalidArgumentException $e) { - $this->assertEquals($e->getMessage(), 'Fontsize must be numeric'); - } + $this->setExpectedException('Zend\Tag\Cloud\Decorator\Exception\InvalidArgumentException', 'Fontsize must be numeric'); + $decorator->setMinFontSize('foo'); } public function testInvalidMaxFontSize() { $decorator = new Decorator\HtmlTag(); - try { - $decorator->setMaxFontSize('foo'); - $this->fail('An expected Zend\Tag\Cloud\Decorator\Exception\InvalidArgumentException was not raised'); - } catch (InvalidArgumentException $e) { - $this->assertEquals($e->getMessage(), 'Fontsize must be numeric'); - } + $this->setExpectedException('Zend\Tag\Cloud\Decorator\Exception\InvalidArgumentException', 'Fontsize must be numeric'); + $decorator->setMaxFontSize('foo'); } public function testConstructorWithArray() @@ -173,4 +153,73 @@ protected function _getTagList() return $list; } + + public function getTags() + { + $tags = new Tag\ItemList(); + $tags[] = new Tag\Item(array( + 'title' => 'tag', + 'weight' => 1, + 'params' => array( + 'url' => 'http://testing', + ), + )); + return $tags; + } + + public function invalidHtmlElementProvider() + { + return array( + array(array('_foo')), + array(array('&foo')), + array(array(' foo')), + array(array(' foo')), + array(array( + '_foo' => array(), + )), + ); + } + + /** + * @dataProvider invalidHtmlElementProvider + */ + public function testInvalidElementNamesRaiseAnException($tags) + { + $decorator = new Decorator\HtmlTag(); + $decorator->setHTMLTags($tags); + $this->setExpectedException('Zend\Tag\Exception\InvalidElementNameException'); + $decorator->render($this->getTags()); + } + + public function invalidAttributeProvider() + { + return array( + array(array( + 'foo' => array( + '&bar' => 'baz', + ), + )), + array(array( + 'foo' => array( + ':bar&baz' => 'bat', + ), + )), + array(array( + 'foo' => array( + 'bar/baz' => 'bat', + ), + )), + ); + } + + /** + * @dataProvider invalidAttributeProvider + */ + public function testInvalidAttributesRaiseAnException($tags) + { + $decorator = new Decorator\HtmlTag(); + $decorator->setHTMLTags($tags); + $this->setExpectedException('Zend\Tag\Exception\InvalidAttributeNameException'); + $decorator->render($this->getTags()); + } }
Vulnerability mechanics
Root cause
"Multiple Zend Framework 2.0.x components output user-controlled data using raw `htmlspecialchars()` or no escaping instead of the context‑aware `Zend\Escaper\Escaper` methods, enabling cross‑site scripting."
Attack vector
An attacker supplies crafted input (such as a tag title, URL parameter, log message, or debug output) that contains HTML metacharacters like `<`, `>`, `"`, or `&`. Because the affected components did not use `Zend\Escaper\Escaper` to properly escape HTML, HTML attributes, or URLs, the unescaped input is rendered in a web page context, allowing arbitrary script or HTML injection [ref_id=1]. The attack is remote and requires no special privileges; the attacker simply needs to deliver the malicious input through any channel that feeds into one of the eight listed components (e.g., a tag cloud, a feed subscription request, a log entry, a debug dump, or a URI).
Affected code
The patch touches eight components: `Zend\Debug`, `Zend\Feed\PubSubHubbub`, `Zend\Log\Formatter\Xml`, `Zend\Tag\Cloud\Decorator` (AbstractDecorator, AbstractTag, AbstractCloud, HtmlTag, HtmlCloud), `Zend\Uri\Uri`, `Zend\View\Helper\HeadStyle`, `Zend\View\Helper\Navigation\Sitemap`, and `Zend\View\Helper\Placeholder\Container\AbstractStandalone` [patch_id=2243828]. The core defect is that these components output user-controlled data using raw `htmlspecialchars()` or no escaping at all instead of using `Zend\Escaper\Escaper` methods (`escapeHtml`, `escapeHtmlAttr`, `escapeUrl`) [ref_id=1].
What the fix does
The patch introduces `Zend\Escaper\Escaper` instances into each affected component and replaces ad‑hoc `htmlspecialchars()` calls with the appropriate Escaper method (`escapeHtml`, `escapeHtmlAttr`, or `escapeUrl`) [ref_id=1]. For example, in `Zend\Debug` the old `htmlspecialchars($output, ENT_QUOTES)` is replaced by `static::getEscaper()->escapeHtml($output)`, and in `Zend\Tag\Cloud\Decorator\HtmlTag` the `wrapTag()` method now calls `$escaper->escapeHtmlAttr($value)` for attribute values and `$escaper->escapeHtml()` for tag content [patch_id=2243828]. The new `AbstractDecorator` base class provides shared `getEscaper()`/`setEscaper()` methods and a `wrapTag()` helper that consistently applies escaping, while `AbstractTag` and `AbstractCloud` are refactored to extend this base class rather than duplicating option‑handling logic [patch_id=2243828].
Preconditions
- configThe application must use one of the eight affected Zend Framework 2.0.x components (Debug, Feed\PubSubHubbub, Log\Formatter\Xml, Tag\Cloud\Decorator, Uri, View\Helper\HeadStyle, View\Helper\Navigation\Sitemap, or View\Helper\Placeholder\Container\AbstractStandalone).
- inputThe attacker must be able to supply input that reaches the vulnerable component (e.g., a tag title, a URL parameter, a log message, or debug output).
Generated on May 24, 2026. Inputs: CWE entries + fix-commit diffs from this CVE's patches. Citations validated against bundle.
References
8- framework.zend.com/security/advisory/ZF2012-03mitrex_refsource_MISC
- seclists.org/oss-sec/2012/q3/571mitrex_refsource_MISC
- seclists.org/oss-sec/2012/q3/573mitrex_refsource_MISC
- www.securityfocus.com/bid/55636mitrex_refsource_MISC
- bugs.debian.org/cgi-bin/bugreport.cgimitrex_refsource_MISC
- bugs.gentoo.org/show_bug.cgimitrex_refsource_MISC
- bugzilla.redhat.com/show_bug.cgimitrex_refsource_MISC
- github.com/zendframework/zf2/commit/27131ca9520bdf1d4c774c71459eba32f2b10733mitrex_refsource_MISC
News mentions
0No linked articles in our index yet.