From ffec857b697ce0a23134f04cf345dc3a8b45a7ae Mon Sep 17 00:00:00 2001 From: Aleksander Machniak <alec@alec.pl> Date: Thu, 28 Nov 2013 03:12:03 -0500 Subject: [PATCH] Fix handling of invalid closing tags in HTML messages (#1489446) --- program/lib/Roundcube/rcube_washtml.php | 90 +++++++++++++++++++++++++++++++++++--------- 1 files changed, 71 insertions(+), 19 deletions(-) diff --git a/program/lib/Roundcube/rcube_washtml.php b/program/lib/Roundcube/rcube_washtml.php index 2a26141..9cf3c62 100644 --- a/program/lib/Roundcube/rcube_washtml.php +++ b/program/lib/Roundcube/rcube_washtml.php @@ -113,10 +113,9 @@ 'type', 'rows', 'cols', 'disabled', 'readonly', 'checked', 'multiple', 'value' ); - /* Block elements which could be empty but cannot be returned in short form (<tag />) */ - static $block_elements = array('div', 'p', 'pre', 'blockquote', 'a', 'font', 'center', - 'table', 'ul', 'h1', 'h2', 'h3', 'h4', 'h5', 'h6', 'ol', 'dl', 'strong', - 'i', 'b', 'u', 'span', + /* Elements which could be empty and be returned in short form (<tag />) */ + static $void_elements = array('area', 'base', 'br', 'col', 'command', 'embed', 'hr', + 'img', 'input', 'keygen', 'link', 'meta', 'param', 'source', 'track', 'wbr' ); /* State for linked objects in HTML */ @@ -134,11 +133,14 @@ /* Ignore these HTML tags but process their content */ private $_ignore_elements = array(); - /* Block elements which could be empty but cannot be returned in short form (<tag />) */ - private $_block_elements = array(); + /* Elements which could be empty and be returned in short form (<tag />) */ + private $_void_elements = array(); /* Allowed HTML attributes */ private $_html_attribs = array(); + + /* Max nesting level */ + private $max_nesting_level; /** @@ -149,9 +151,9 @@ $this->_html_elements = array_flip((array)$p['html_elements']) + array_flip(self::$html_elements) ; $this->_html_attribs = array_flip((array)$p['html_attribs']) + array_flip(self::$html_attribs); $this->_ignore_elements = array_flip((array)$p['ignore_elements']) + array_flip(self::$ignore_elements); - $this->_block_elements = array_flip((array)$p['block_elements']) + array_flip(self::$block_elements); + $this->_void_elements = array_flip((array)$p['void_elements']) + array_flip(self::$void_elements); - unset($p['html_elements'], $p['html_attribs'], $p['ignore_elements'], $p['block_elements']); + unset($p['html_elements'], $p['html_attribs'], $p['ignore_elements'], $p['void_elements']); $this->config = $p + array('show_washed' => true, 'allow_remote' => false, 'cid_map' => array()); } @@ -284,10 +286,24 @@ * It output only allowed tags with allowed attributes * and allowed inline styles */ - private function dumpHtml($node) + private function dumpHtml($node, $level = 0) { if (!$node->hasChildNodes()) { return ''; + } + + $level++; + + if ($this->max_nesting_level > 0 && $level == $this->max_nesting_level - 1) { + // log error message once + if (!$this->max_nesting_level_error) { + $this->max_nesting_level_error = true; + rcube::raise_error(array('code' => 500, 'type' => 'php', + 'line' => __LINE__, 'file' => __FILE__, + 'message' => "Maximum nesting level exceeded (xdebug.max_nesting_level={$this->max_nesting_level})"), + true, false); + } + return '<!-- ignored -->'; } $node = $node->firstChild; @@ -299,19 +315,19 @@ $tagName = strtolower($node->tagName); if ($callback = $this->handlers[$tagName]) { $dump .= call_user_func($callback, $tagName, - $this->wash_attribs($node), $this->dumpHtml($node), $this); + $this->wash_attribs($node), $this->dumpHtml($node, $level), $this); } else if (isset($this->_html_elements[$tagName])) { - $content = $this->dumpHtml($node); + $content = $this->dumpHtml($node, $level); $dump .= '<' . $tagName . $this->wash_attribs($node) . - ($content != '' || isset($this->_block_elements[$tagName]) ? ">$content</$tagName>" : ' />'); + ($content === '' && isset($this->_void_elements[$tagName]) ? ' />' : ">$content</$tagName>"); } else if (isset($this->_ignore_elements[$tagName])) { $dump .= '<!-- ' . htmlspecialchars($tagName, ENT_QUOTES) . ' not allowed -->'; } else { $dump .= '<!-- ' . htmlspecialchars($tagName, ENT_QUOTES) . ' ignored -->'; - $dump .= $this->dumpHtml($node); // ignore tags not its content + $dump .= $this->dumpHtml($node, $level); // ignore tags not its content } break; @@ -324,14 +340,14 @@ break; case XML_HTML_DOCUMENT_NODE: - $dump .= $this->dumpHtml($node); + $dump .= $this->dumpHtml($node, $level); break; case XML_DOCUMENT_TYPE_NODE: break; default: - $dump . '<!-- node type ' . $node->nodeType . ' -->'; + $dump .= '<!-- node type ' . $node->nodeType . ' -->'; } } while($node = $node->nextSibling); @@ -358,7 +374,17 @@ $this->config['base_url'] = ''; } - @$node->loadHTML($html); + // Detect max nesting level (for dumpHTML) (#1489110) + $this->max_nesting_level = (int) @ini_get('xdebug.max_nesting_level'); + + // Use optimizations if supported + if (version_compare(PHP_VERSION, '5.4.0', '>=')) { + @$node->loadHTML($html, LIBXML_PARSEHUGE | LIBXML_COMPACT); + } + else { + @$node->loadHTML($html); + } + return $this->dumpHtml($node); } @@ -391,6 +417,25 @@ ); $html = preg_replace($html_search, $html_replace, trim($html)); + //-> Replace all of those weird MS Word quotes and other high characters + $badwordchars=array( + "\xe2\x80\x98", // left single quote + "\xe2\x80\x99", // right single quote + "\xe2\x80\x9c", // left double quote + "\xe2\x80\x9d", // right double quote + "\xe2\x80\x94", // em dash + "\xe2\x80\xa6" // elipses + ); + $fixedwordchars=array( + "'", + "'", + '"', + '"', + '—', + '...' + ); + $html = str_replace($badwordchars,$fixedwordchars, $html); + // PCRE errors handling (#1486856), should we use something like for every preg_* use? if ($html === null && ($preg_error = preg_last_error()) != PREG_NO_ERROR) { $errstr = "Could not clean up HTML message! PCRE Error: $preg_error."; @@ -405,15 +450,17 @@ rcube::raise_error(array('code' => 620, 'type' => 'php', 'line' => __LINE__, 'file' => __FILE__, 'message' => $errstr), true, false); + return ''; } // fix (unknown/malformed) HTML tags before "wash" - $html = preg_replace_callback('/(<[\/]*)([^\s>]+)/', array($this, 'html_tag_callback'), $html); + $html = preg_replace_callback('/(<(?!\!)[\/]*)([^\s>]+)([^>]*)/', array($this, 'html_tag_callback'), $html); // Remove invalid HTML comments (#1487759) // Don't remove valid conditional comments - $html = preg_replace('/<!--[^->[\n]*>/', '', $html); + // Don't remove MSOutlook (<!-->) conditional comments (#1489004) + $html = preg_replace('/<!--[^->\[\n]+>/', '', $html); // turn relative into absolute urls $html = self::resolve_base($html); @@ -432,7 +479,12 @@ '/[^a-z0-9_\[\]\!-]/i', // forbidden characters ), '', $tagname); - return $matches[1] . $tagname; + // fix invalid closing tags - remove any attributes (#1489446) + if ($matches[1] == '</') { + $matches[3] = ''; + } + + return $matches[1] . $tagname . $matches[3]; } /** -- Gitblit v1.9.1