From e02694c3a6dbe753c5683d201b6b6b14c2b30660 Mon Sep 17 00:00:00 2001
From: thomascube <thomas@roundcube.net>
Date: Sat, 10 Dec 2011 09:16:31 -0500
Subject: [PATCH] Backported CSS sanitization (r5586:r5590)

---
 CHANGELOG                   |    1 
 program/include/main.inc    |   34 ++++++++++++-----
 program/steps/mail/func.inc |   16 +++++---
 program/lib/washtml.php     |   12 +++++-
 4 files changed, 45 insertions(+), 18 deletions(-)

diff --git a/CHANGELOG b/CHANGELOG
index 2ee37fb..c872df3 100644
--- a/CHANGELOG
+++ b/CHANGELOG
@@ -3,6 +3,7 @@
 
 RELEASE 0.7
 -----------
+- Make Roundcube render the Email Standards Project Acid Test correctly
 - Replace prompt() with jQuery UI dialog (#1485135)
 - Fix navigation in messages search results
 - Improved handling of some malformed values encoded with quoted-printable (#1488232)
diff --git a/program/include/main.inc b/program/include/main.inc
index 7147649..d40dbda 100644
--- a/program/include/main.inc
+++ b/program/include/main.inc
@@ -883,23 +883,37 @@
  * @param string Container ID to use as prefix
  * @return string Modified CSS source
  */
-function rcmail_mod_css_styles($source, $container_id)
+function rcmail_mod_css_styles($source, $container_id, $allow_remote=false)
   {
   $last_pos = 0;
   $replacements = new rcube_string_replacer;
 
   // ignore the whole block if evil styles are detected
-  $stripped = preg_replace('/[^a-z\(:;]/', '', rcmail_xss_entity_decode($source));
-  if (preg_match('/expression|behavior|url\(|import[^a]/', $stripped))
+  $source = rcmail_xss_entity_decode($source);
+  $stripped = preg_replace('/[^a-z\(:;]/i', '', $source);
+  $evilexpr = 'expression|behavior|javascript:|import[^a]' . (!$allow_remote ? '|url\(' : '');
+  if (preg_match("/$evilexpr/i", $stripped))
     return '/* evil! */';
 
-  // remove css comments (sometimes used for some ugly hacks)
-  $source = preg_replace('!/\*(.+)\*/!Ums', '', $source);
-
   // cut out all contents between { and }
-  while (($pos = strpos($source, '{', $last_pos)) && ($pos2 = strpos($source, '}', $pos)))
-  {
-    $key = $replacements->add(substr($source, $pos+1, $pos2-($pos+1)));
+  while (($pos = strpos($source, '{', $last_pos)) && ($pos2 = strpos($source, '}', $pos))) {
+    $styles = substr($source, $pos+1, $pos2-($pos+1));
+
+    // check every line of a style block...
+    if ($allow_remote) {
+      $a_styles = preg_split('/;[\r\n]*/', $styles, -1, PREG_SPLIT_NO_EMPTY);
+      foreach ($a_styles as $line) {
+        $stripped = preg_replace('/[^a-z\(:;]/i', '', $line);
+        // ... and only allow strict url() values
+        if (stripos($stripped, 'url(') && !preg_match('!url\s*\([ "\'](https?:)//[a-z0-9/._+-]+["\' ]\)!Uims', $line)) {
+          $a_styles = array('/* evil! */');
+          break;
+        }
+      }
+      $styles = join(";\n", $a_styles);
+    }
+
+    $key = $replacements->add($styles);
     $source = substr($source, 0, $pos+1) . $replacements->get_replacement($key) . substr($source, $pos2, strlen($source)-$pos2);
     $last_pos = $pos+2;
   }
@@ -937,7 +951,7 @@
 {
   $out = html_entity_decode(html_entity_decode($content));
   $out = preg_replace_callback('/\\\([0-9a-f]{4})/i', 'rcmail_xss_entity_decode_callback', $out);
-  $out = preg_replace('#/\*.*\*/#Um', '', $out);
+  $out = preg_replace('#/\*.*\*/#Ums', '', $out);
   return $out;
 }
 
diff --git a/program/lib/washtml.php b/program/lib/washtml.php
index 9c8625f..f8c3251 100644
--- a/program/lib/washtml.php
+++ b/program/lib/washtml.php
@@ -168,7 +168,7 @@
                 || ($src = $this->config['cid_map'][$this->config['base_url'].$match[2]])) {
               $value .= ' url('.htmlspecialchars($src, ENT_QUOTES) . ')';
             }
-            else if (preg_match('/^(http|https|ftp):.*$/i', $match[2], $url)) {
+            else if (preg_match('!^(https?:)?//[a-z0-9/._+-]+$!i', $match[2], $url)) {
               if ($this->config['allow_remote'])
                 $value .= ' url('.htmlspecialchars($url[0], ENT_QUOTES).')';
               else
@@ -243,7 +243,7 @@
       case XML_ELEMENT_NODE: //Check element
         $tagName = strtolower($node->tagName);
         if ($callback = $this->handlers[$tagName]) {
-          $dump .= call_user_func($callback, $tagName, $this->wash_attribs($node), $this->dumpHtml($node));
+          $dump .= call_user_func($callback, $tagName, $this->wash_attribs($node), $this->dumpHtml($node), $this);
         }
         else if (isset($this->_html_elements[$tagName])) {
           $content = $this->dumpHtml($node);
@@ -301,6 +301,14 @@
     return $this->dumpHtml($node);
   }
 
+  /**
+   * Getter for config parameters
+   */
+  public function get_config($prop)
+  {
+      return $this->config[$prop];
+  }
+
 }
 
 ?>
diff --git a/program/steps/mail/func.inc b/program/steps/mail/func.inc
index b0a1e95..e51c256 100644
--- a/program/steps/mail/func.inc
+++ b/program/steps/mail/func.inc
@@ -821,7 +821,7 @@
 /**
  * Callback function for washtml cleaning class
  */
-function rcmail_washtml_callback($tagname, $attrib, $content)
+function rcmail_washtml_callback($tagname, $attrib, $content, $washtml)
 {
   switch ($tagname) {
     case 'form':
@@ -833,8 +833,11 @@
       $stripped = preg_replace('/[^a-zA-Z\(:;]/', '', rcmail_xss_entity_decode($content));
 
       // now check for evil strings like expression, behavior or url()
-      if (!preg_match('/expression|behavior|url\(|import[^a]/', $stripped)) {
-        $out = html::tag('style', array('type' => 'text/css'), $content);
+      if (!preg_match('/expression|behavior|javascript:|import[^a]/i', $stripped)) {
+        if (!$washtml->get_config('allow_remote') && stripos($stripped, 'url('))
+          $washtml->extlinks = true;
+        else
+          $out = html::tag('style', array('type' => 'text/css'), $content);
         break;
       }
 
@@ -1014,7 +1017,7 @@
         $body = rcmail_print_body($part, array('safe' => $safe_mode, 'plain' => !$CONFIG['prefer_html']));
 
         if ($part->ctype_secondary == 'html') {
-          $body = rcmail_html4inline($body, $attrib['id'], 'rcmBody', $attrs);
+          $body = rcmail_html4inline($body, $attrib['id'], 'rcmBody', $attrs, $safe_mode);
           $div_attr = array('class' => 'message-htmlpart');
           $style = array();
 
@@ -1088,7 +1091,7 @@
 /**
  * modify a HTML message that it can be displayed inside a HTML page
  */
-function rcmail_html4inline($body, $container_id, $body_id='', &$attributes=null)
+function rcmail_html4inline($body, $container_id, $body_id='', &$attributes=null, $allow_remote=false)
 {
   $last_style_pos = 0;
   $body_lc = strtolower($body);
@@ -1101,7 +1104,7 @@
 
     // replace all css definitions with #container [def]
     $styles = rcmail_mod_css_styles(
-      substr($body, $pos, $pos2-$pos), $cont_id);
+      substr($body, $pos, $pos2-$pos), $cont_id, $allow_remote);
 
     $body = substr($body, 0, $pos) . $styles . substr($body, $pos2);
     $body_lc = strtolower($body);
@@ -1279,6 +1282,7 @@
             'href' => "#add",
             'onclick' => sprintf("return %s.command('add-contact','%s',this)", JS_OBJECT_NAME, urlencode($string)),
             'title' => rcube_label('addtoaddressbook'),
+            'class' => 'rcmaddcontact',
           ),
           html::img(array(
             'src' => $CONFIG['skin_path'] . $addicon,

--
Gitblit v1.9.1