thomascube
2011-12-10 e02694c3a6dbe753c5683d201b6b6b14c2b30660
Backported CSS sanitization (r5586:r5590)

4 files modified
63 ■■■■ changed files
CHANGELOG 1 ●●●● patch | view | raw | blame | history
program/include/main.inc 34 ●●●● patch | view | raw | blame | history
program/lib/washtml.php 12 ●●●● patch | view | raw | blame | history
program/steps/mail/func.inc 16 ●●●●● patch | view | raw | blame | history
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)
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;
}
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];
  }
}
?>
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,