Fix XSS problem with unsanitized style tags in messages [CVE-2011-2023]
[squirrelmail.git] / functions / mime.php
index 48dccdaee2649e5260232c1d49e1fba95575d7a7..1e8fc880c1b82237c8d316e0851b3e6cca0a489b 100644 (file)
@@ -6,7 +6,7 @@
  * This contains the functions necessary to detect and decode MIME
  * messages.
  *
- * @copyright © 1999-2007 The SquirrelMail Project Team
+ * @copyright 1999-2011 The SquirrelMail Project Team
  * @license http://opensource.org/licenses/gpl-license.php GNU Public License
  * @version $Id$
  * @package squirrelmail
@@ -60,17 +60,17 @@ function mime_structure ($bodystructure, $flags=array()) {
 
     if (!is_object($msg)) {
         global $color, $mailbox;
-        /* removed urldecode because $_GET is auto urldecoded ??? */
         displayPageHeader( $color, $mailbox );
         $errormessage  = _("SquirrelMail could not decode the bodystructure of the message");
         $errormessage .= '<br />'._("The bodystructure provided by your IMAP server:").'<br /><br />';
         $errormessage .= '<pre>' . htmlspecialchars($read) . '</pre>';
-        plain_error_message( $errormessage, $color );
+        plain_error_message( $errormessage );
         echo '</body></html>';
         exit;
     }
     if (count($flags)) {
         foreach ($flags as $flag) {
+//FIXME: please document why it is we have to check the first char of the flag but we then go ahead and do a full string comparison anyway.  Is this a speed enhancement?  If not, let's keep it simple and just compare the full string and forget the switch block.
             $char = strtoupper($flag{1});
             switch ($char) {
                 case 'S':
@@ -92,6 +92,9 @@ function mime_structure ($bodystructure, $flags=array()) {
                     if (strtolower($flag) == '\\flagged') {
                         $msg->is_flagged = true;
                     }
+                    else if (strtolower($flag) == '$forwarded') {
+                        $msg->is_forwarded = true;
+                    }
                     break;
                 case 'M':
                     if (strtolower($flag) == '$mdnsent') {
@@ -137,7 +140,7 @@ function mime_fetch_body($imap_stream, $id, $ent_id=1, $fetch_size=0) {
     } while($topline && ($topline[0] == '*') && !preg_match('/\* [0-9]+ FETCH.*/i', $topline)) ;
 
     $wholemessage = implode('', $data);
-    if (ereg('\\{([^\\}]*)\\}', $topline, $regs)) {
+    if (preg_match('/\{([^\}]*)\}/', $topline, $regs)) {
         $ret = substr($wholemessage, 0, $regs[1]);
         /* There is some information in the content info header that could be important
          * in order to parse html messages. Let's get them here.
@@ -145,8 +148,10 @@ function mime_fetch_body($imap_stream, $id, $ent_id=1, $fetch_size=0) {
 //        if ($ret{0} == '<') {
 //            $data = sqimap_run_command ($imap_stream, "FETCH $id BODY[$ent_id.MIME]", true, $response, $message, TRUE);
 //        }
-    } else if (ereg('"([^"]*)"', $topline, $regs)) {
+    } else if (preg_match('/"([^"]*)"/', $topline, $regs)) {
         $ret = $regs[1];
+    } else if ((stristr($topline, 'nil') !== false) && (empty($wholemessage))) {
+        $ret = $wholemessage;
     } else {
         global $where, $what, $mailbox, $passed_id, $startMessage;
         $par = 'mailbox=' . urlencode($mailbox) . '&amp;passed_id=' . $passed_id;
@@ -179,7 +184,7 @@ function mime_fetch_body($imap_stream, $id, $ent_id=1, $fetch_size=0) {
     return $ret;
 }
 
-function mime_print_body_lines ($imap_stream, $id, $ent_id=1, $encoding, $rStream='php://stdout') {
+function mime_print_body_lines ($imap_stream, $id, $ent_id=1, $encoding, $rStream='php://stdout', $force_crlf='') {
 
     /* Don't kill the connection if the browser is over a dialup
      * and it would take over 30 seconds to download it.
@@ -201,9 +206,9 @@ function mime_print_body_lines ($imap_stream, $id, $ent_id=1, $encoding, $rStrea
     } else {
         $body = mime_fetch_body ($imap_stream, $id, $ent_id);
         if (is_resource($rStream)) {
-            fputs($rStream,decodeBody($body,$encoding));
+            fputs($rStream,decodeBody($body, $encoding, $force_crlf));
         } else {
-            echo decodeBody($body, $encoding);
+            echo decodeBody($body, $encoding, $force_crlf);
         }
     }
 
@@ -289,7 +294,8 @@ function translateText(&$body, $wrap_at, $charset) {
 
     $body_ary = explode("\n", $body);
     for ($i=0; $i < count($body_ary); $i++) {
-        $line = $body_ary[$i];
+        $line = rtrim($body_ary[$i],"\r");
+
         if (strlen($line) - 2 >= $wrap_at) {
             sqWordWrap($line, $wrap_at, $charset);
         }
@@ -340,10 +346,9 @@ function translateText(&$body, $wrap_at, $charset) {
  * @param string $ent_num (since 1.3.0) message part id
  * @param integer $id (since 1.3.0) message id
  * @param string $mailbox (since 1.3.0) imap folder name
- * @param boolean $clean (since 1.5.1) Do not output stuff that's irrelevant for the printable version.
  * @return string html formated message text
  */
-function formatBody($imap_stream, $message, $color, $wrap_at, $ent_num, $id, $mailbox='INBOX', $clean=FALSE) {
+function formatBody($imap_stream, $message, $color, $wrap_at, $ent_num, $id, $mailbox='INBOX') {
     /* This if statement checks for the entity to show as the
      * primary message. To add more of them, just put them in the
      * order that is their priority.
@@ -351,14 +356,15 @@ function formatBody($imap_stream, $message, $color, $wrap_at, $ent_num, $id, $ma
     global $startMessage, $languages, $squirrelmail_language,
            $show_html_default, $sort, $has_unsafe_images, $passed_ent_id,
            $use_iframe, $iframe_height, $download_and_unsafe_link,
-           $download_href, $unsafe_image_toggle_href, $unsafe_image_toggle_text;
+           $download_href, $unsafe_image_toggle_href, $unsafe_image_toggle_text,
+           $oTemplate, $nbsp;
 
     // workaround for not updated config.php
     if (! isset($use_iframe)) $use_iframe = false;
 
-    if( !sqgetGlobalVar('view_unsafe_images', $view_unsafe_images, SQ_GET) ) {
-        $view_unsafe_images = false;
-    }
+    // If there's no "view_unsafe_images" variable in the URL, turn unsafe
+    // images off by default.
+    sqgetGlobalVar('view_unsafe_images', $view_unsafe_images, SQ_GET, FALSE);
 
     $body = '';
     $urlmailbox = urlencode($mailbox);
@@ -398,9 +404,7 @@ function formatBody($imap_stream, $message, $color, $wrap_at, $ent_num, $id, $ma
                 $body = trim($body);
                 translateText($body, $wrap_at,
                         $body_message->header->getParameter('charset'));
-            } elseif ($use_iframe && ! $clean) {
-                // $clean is used to remove iframe in printable view.
-
+            } elseif ($use_iframe) {
                 /**
                  * If we don't add html message between iframe tags,
                  * we must detect unsafe images and modify $has_unsafe_images.
@@ -418,31 +422,42 @@ function formatBody($imap_stream, $message, $color, $wrap_at, $ent_num, $id, $ma
 
                 global $oTemplate;
                 $oTemplate->assign('iframe_url', $iframeurl);
+                $oTemplate->assign('iframe_height', $iframe_height);
                 $oTemplate->assign('html_body', $html_body);
 
                 $body = $oTemplate->fetch('read_html_iframe.tpl');
             } else {
                 // old way of html rendering
-                $body = magicHTML($body, $id, $message, $mailbox);
                 /**
                  * convert character set. charset_decode does not remove html special chars
                  * applied by magicHTML functions and does not sanitize them second time if
                  * fourth argument is true.
                  */
-                $body = charset_decode($body_message->header->getParameter('charset'),$body,false,true);
+                $charset = $body_message->header->getParameter('charset');
+                if (!empty($charset)) {
+                    $body = charset_decode($charset,$body,false,true);
+                }
+                $body = magicHTML($body, $id, $message, $mailbox);
             }
         } else {
             translateText($body, $wrap_at,
                     $body_message->header->getParameter('charset'));
         }
 
-        // if this is the clean display (i.e. printer friendly), stop here.
-        if ( $clean ) {
-            return $body;
-        }
+        /*
+         * Previously the links for downloading and unsafe images were printed
+         * under the mail. By putting the links in a global variable we can
+         * print it in the toolbar where it belongs. Since the original code was
+         * in this place it's left here. It might be possible to move it to some
+         * other place if that makes sense. The possibility to do so has not
+         * been evaluated yet.
+         */
 
+        // Initialize the global variable to an empty string.
+        // FIXME: To have $download_and_unsafe_link as a global variable might not be needed since the use of separate variables ($download_href, $unsafe_image_toggle_href, and $unsafe_image_toggle_text) for the templates was introduced.
         $download_and_unsafe_link = '';
 
+        // Prepare and build a link for downloading the mail.
         $link = 'passed_id=' . $id . '&amp;ent_id='.$ent_num.
             '&amp;mailbox=' . $urlmailbox .'&amp;sort=' . $sort .
             '&amp;startMessage=' . $startMessage . '&amp;show_more=0';
@@ -450,7 +465,16 @@ function formatBody($imap_stream, $message, $color, $wrap_at, $ent_num, $id, $ma
             $link .= '&amp;passed_ent_id='.$passed_ent_id;
         }
         $download_href = SM_PATH . 'src/download.php?absolute_dl=true&amp;' . $link;
-        $download_and_unsafe_link .= '&nbsp;|&nbsp;<a href="'. $download_href .'">' . _("Download this as a file") .  '</a>';
+
+        // Always add the link for downloading the mail as a file to the global
+        // variable.
+        $download_and_unsafe_link .= "$nbsp|$nbsp" 
+            . create_hyperlink($download_href, _("Download this as a file"));
+
+        // Find out the right text to use in the link depending on the
+        // circumstances. If the unsafe images are displayed the link should
+        // hide them, if they aren't displayed the link should only appear if
+        // the mail really contains unsafe images.
         if ($view_unsafe_images) {
             $text = _("Hide Unsafe Images");
         } else {
@@ -461,19 +485,21 @@ function formatBody($imap_stream, $message, $color, $wrap_at, $ent_num, $id, $ma
                 $text = '';
             }
         }
+
+        // Only create a link for unsafe images if there's need for one. If so:
+        // add it to the global variable.
         if($text != '') {
             $unsafe_image_toggle_href = SM_PATH . 'src/read_body.php?'.$link;
             $unsafe_image_toggle_text = $text;
-            $download_and_unsafe_link .= '&nbsp;|&nbsp;<a href="'. $unsafe_image_toggle_href .'">' . $text . '</a>';
+            $download_and_unsafe_link .= "$nbsp|$nbsp"
+                . create_hyperlink($unsafe_image_toggle_href, $text);
         }
     }
     return $body;
 }
 
 /**
- * Generate attachments array for passing to templates.  Separated from
- * formatAttachments() below so that the same array can be given to the
- * print-friendly version.
+ * Generate attachments array for passing to templates.
  *
  * @since 1.5.2
  * @param object $message SquirrelMail message object
@@ -549,36 +575,49 @@ function buildAttachmentArray($message, $exclude_id, $mailbox, $id) {
         }
 
         /* This executes the attachment hook with a specific MIME-type.
-         * If that doesn't have results, it tries if there's a rule
-         * for a more generic type. Finally, a hook for ALL attachment
-         * types is run as well.
+         * It also allows plugins to run if there's a rule for a more
+         * generic type. Finally, a hook for ALL attachment types is
+         * run as well.
          */
+        // First remember the default link.
+        $defaultlink_orig = $defaultlink;
+
         /* The API for this hook has changed as of 1.5.2 so that all plugin
            arguments are passed in an array instead of each their own plugin
            argument, and arguments are passed by reference, so instead of
            returning any changes, changes should simply be made to the original
            arguments themselves. */
-        do_hook("attachment $type0/$type1", $temp=array(&$links,
-                &$startMessage, &$id, &$urlMailbox, &$ent, &$defaultlink,
-                &$display_filename, &$where, &$what));
-        if(count($links) <= 1) {
-            /* The API for this hook has changed as of 1.5.2 so that all plugin
-               arguments are passed in an array instead of each their own plugin
-               argument, and arguments are passed by reference, so instead of
-               returning any changes, changes should simply be made to the original
-               arguments themselves. */
-            do_hook("attachment $type0/*", $temp=array(&$links,
-                    &$startMessage, &$id, &$urlMailbox, &$ent, &$defaultlink,
-                    &$display_filename, &$where, &$what));
+        $temp = array(&$links, &$startMessage, &$id, &$urlMailbox, &$ent,
+                    &$defaultlink, &$display_filename, &$where, &$what);
+        do_hook("attachment $type0/$type1", $temp);
+        /* The API for this hook has changed as of 1.5.2 so that all plugin
+           arguments are passed in an array instead of each their own plugin
+           argument, and arguments are passed by reference, so instead of
+           returning any changes, changes should simply be made to the original
+           arguments themselves. */
+        $temp = array(&$links, &$startMessage, &$id, &$urlMailbox, &$ent,
+                      &$defaultlink, &$display_filename, &$where, &$what);
+        // Do not let a generic plugin change the default link if a more
+        // specialized one already did it...
+        if ($defaultlink != $defaultlink_orig) {
+            $dummy = '';
+            $temp[5] = &$dummy;
         }
+        do_hook("attachment $type0/*", $temp);
         /* The API for this hook has changed as of 1.5.2 so that all plugin
            arguments are passed in an array instead of each their own plugin
            argument, and arguments are passed by reference, so instead of
            returning any changes, changes should simply be made to the original
            arguments themselves. */
-        do_hook("attachment */*", $temp=array(&$links,
-                &$startMessage, &$id, &$urlMailbox, &$ent, &$defaultlink, 
-                &$display_filename, &$where, &$what));
+        $temp = array(&$links, &$startMessage, &$id, &$urlMailbox, &$ent, 
+                      &$defaultlink, &$display_filename, &$where, &$what);
+        // Do not let a generic plugin change the default link if a more
+        // specialized one already did it...
+        if ($defaultlink != $defaultlink_orig) {
+            $dummy = '';
+            $temp[5] = &$dummy;
+        }
+        do_hook("attachment */*", $temp);
 
         $this_attachment = array();
         $this_attachment['Name'] = decodeHeader($display_filename);
@@ -657,60 +696,95 @@ function sqimap_base64_decode(&$string) {
 }
 
 /**
- * Decodes encoded message body
+ * Decodes encoded string (usually message body)
+ *
+ * This function decodes a string (usually the message body)
+ * depending on the encoding type.  Currently quoted-printable
+ * and base64 encodings are supported.
+ *
+ * The decode_body hook was added to this function in 1.4.2/1.5.0.
+ * The $force_crlf parameter was added in 1.5.2.
+ *
+ * @param string $string     The encoded string
+ * @param string $encoding   used encoding
+ * @param string $force_crlf Whether or not to force CRLF or LF
+ *                           line endings (or to leave as is).
+ *                           If given as "LF", line endings will
+ *                           all be converted to LF; if "CRLF",
+ *                           line endings will all be converted
+ *                           to CRLF.  If given as an empty value,
+ *                           the global $force_crlf_default will
+ *                           be consulted (it can be specified in
+ *                           config/config_local.php).  Otherwise,
+ *                           any other value will cause the string
+ *                           to be left alone.  Note that this will
+ *                           be overridden to "LF" if not using at
+ *                           least PHP version 4.3.0. (OPTIONAL;
+ *                           default is empty - consult global
+ *                           default value)
+ *
+ * @return string The decoded string
  *
- * This function decodes the body depending on the encoding type.
- * Currently quoted-printable and base64 encodings are supported.
- * decode_body hook was added to this function in 1.4.2/1.5.0
- * @param string $body encoded message body
- * @param string $encoding used encoding
- * @return string decoded string
  * @since 1.0
+ *
  */
-function decodeBody($body, $encoding) {
+function decodeBody($string, $encoding, $force_crlf='') {
+
+    global $force_crlf_default;
+    if (empty($force_crlf)) $force_crlf = $force_crlf_default;
+    $force_crlf = strtoupper($force_crlf);
+
+    // must force line endings to LF due to broken
+    // quoted_printable_decode() in PHP versions
+    // before 4.3.0 (see below)
+    //
+    if (!check_php_version(4, 3, 0) || $force_crlf == 'LF')
+        $string = str_replace("\r\n", "\n", $string);
+    else if ($force_crlf == 'CRLF')
+        $string = str_replace("\n", "\r\n", $string);
 
-    $body = str_replace("\r\n", "\n", $body);
     $encoding = strtolower($encoding);
 
     $encoding_handler = do_hook('decode_body', $encoding);
 
 
-    // plugins get first shot at decoding the body
+    // plugins get first shot at decoding the string
     //
     if (!empty($encoding_handler) && function_exists($encoding_handler)) {
-        $body = $encoding_handler('decode', $body);
+        $string = $encoding_handler('decode', $string);
 
     } elseif ($encoding == 'quoted-printable' ||
             $encoding == 'quoted_printable') {
-        /**
-         * quoted_printable_decode() function is broken in older
-         * php versions. Text with \r\n decoding was fixed only
-         * in php 4.3.0. Minimal code requirement 4.0.4 +
-         * str_replace("\r\n", "\n", $body); call.
-         */
-        $body = quoted_printable_decode($body);
+
+        // quoted_printable_decode() function is broken in older
+        // php versions.  Text with \r\n decoding was fixed only
+        // in php 4.3.0.  Minimal code requirement is PHP 4.0.4+
+        // and the above call to:  str_replace("\r\n", "\n", $string);
+        //
+        $string = quoted_printable_decode($string);
+
     } elseif ($encoding == 'base64') {
-        $body = base64_decode($body);
+        $string = base64_decode($string);
     }
 
     // All other encodings are returned raw.
-    return $body;
+    return $string;
 }
 
 /**
  * Decodes headers
  *
- * This functions decode strings that is encoded according to
+ * This function decodes strings that are encoded according to
  * RFC1522 (MIME Part Two: Message Header Extensions for Non-ASCII Text).
  * Patched by Christian Schmidt <christian@ostenfeld.dk>  23/03/2002
  *
  * @param string $string header string that has to be made readable
  * @param boolean $utfencode change message in order to be readable on user's charset. defaults to true
- * @param boolean $htmlsave preserve spaces and sanitize html special characters. defaults to true
+ * @param boolean $htmlsafe preserve spaces and sanitize html special characters. defaults to true
  * @param boolean $decide decide if string can be utfencoded. defaults to false
  * @return string decoded header string
  */
-function decodeHeader ($string, $utfencode=true,$htmlsave=true,$decide=false) {
+function decodeHeader ($string, $utfencode=true,$htmlsafe=true,$decide=false) {
     global $languages, $squirrelmail_language,$default_charset;
     if (is_array($string)) {
         $string = implode("\n", $string);
@@ -742,7 +816,7 @@ function decodeHeader ($string, $utfencode=true,$htmlsave=true,$decide=false) {
         while ($match = preg_match('/^(.*)=\?([^?]*)\?(Q|B)\?([^?]*)\?=(.*)$/Ui',$chunk,$res)) {
             /* if the last chunk isn't an encoded string then put back the space, otherwise don't */
             if ($iLastMatch !== $j) {
-                if ($htmlsave) {
+                if ($htmlsafe) {
                     $ret .= '&#32;';
                 } else {
                     $ret .= ' ';
@@ -750,7 +824,7 @@ function decodeHeader ($string, $utfencode=true,$htmlsave=true,$decide=false) {
             }
             $iLastMatch = $i;
             $j = $i;
-            if ($htmlsave) {
+            if ($htmlsafe) {
                 $ret .= htmlspecialchars($res[1]);
             } else {
                 $ret .= $res[1];
@@ -773,13 +847,13 @@ function decodeHeader ($string, $utfencode=true,$htmlsave=true,$decide=false) {
                             /* convert string to different charset,
                              * if functions asks for it (usually in compose)
                              */
-                            $ret .= charset_convert($res[2],$replace,$default_charset,$htmlsave);
+                            $ret .= charset_convert($res[2],$replace,$default_charset,$htmlsafe);
                         } else {
                             // convert string to html codes in order to display it
                             $ret .= charset_decode($res[2],$replace);
                         }
                     } else {
-                        if ($htmlsave) {
+                        if ($htmlsafe) {
                             $replace = htmlspecialchars($replace);
                         }
                         $ret.= $replace;
@@ -794,13 +868,13 @@ function decodeHeader ($string, $utfencode=true,$htmlsave=true,$decide=false) {
                             /* convert string to different charset,
                              * if functions asks for it (usually in compose)
                              */
-                            $replace = charset_convert($res[2], $replace,$default_charset,$htmlsave);
+                            $replace = charset_convert($res[2], $replace,$default_charset,$htmlsafe);
                         } else {
                             // convert string to html codes in order to display it
                             $replace = charset_decode($res[2], $replace);
                         }
                     } else {
-                        if ($htmlsave) {
+                        if ($htmlsafe) {
                             $replace = htmlspecialchars($replace);
                         }
                     }
@@ -813,14 +887,14 @@ function decodeHeader ($string, $utfencode=true,$htmlsave=true,$decide=false) {
             $encoded = true;
         }
         if (!$encoded) {
-            if ($htmlsave) {
+            if ($htmlsafe) {
                 $ret .= '&#32;';
             } else {
                 $ret .= ' ';
             }
         }
 
-        if (!$encoded && $htmlsave) {
+        if (!$encoded && $htmlsafe) {
             $ret .= htmlspecialchars($chunk);
         } else {
             $ret .= $chunk;
@@ -829,7 +903,7 @@ function decodeHeader ($string, $utfencode=true,$htmlsave=true,$decide=false) {
     }
     /* remove the first added space */
     if ($ret) {
-        if ($htmlsave) {
+        if ($htmlsafe) {
             $ret = substr($ret,5);
         } else {
             $ret = substr($ret,1);
@@ -888,6 +962,7 @@ function encodeHeader ($string) {
     for($i = 0; $i < $j; ++$i) {
         switch($string{$i})
         {
+            case '"':
             case '=':
             case '<':
             case '>':
@@ -1191,8 +1266,8 @@ function sq_fixIE_idiocy(&$attvalue) {
                         array('&#x029F;', '&#0671;' ,/* L UNICODE IPA Extension */
                               '&#x0280;', '&#0640;' ,/* R UNICODE IPA Extension */
                               '&#x0274;', '&#0628;' ,/* N UNICODE IPA Extension */
-                              '&#xFF25;', '&#65317' ,/* Unicode FULLWIDTH LATIN CAPITAL LETTER E */
-                              '&#xFF45;', '&#65349' ,/* Unicode FULLWIDTH LATIN SMALL LETTER E */
+                              '&#xFF25;', '&#65317;' ,/* Unicode FULLWIDTH LATIN CAPITAL LETTER E */
+                              '&#xFF45;', '&#65349;' ,/* Unicode FULLWIDTH LATIN SMALL LETTER E */
                               '&#xFF38;', '&#65336;',/* Unicode FULLWIDTH LATIN CAPITAL LETTER X */
                               '&#xFF58;', '&#65368;',/* Unicode FULLWIDTH LATIN SMALL LETTER X */
                               '&#xFF30;', '&#65328;',/* Unicode FULLWIDTH LATIN CAPITAL LETTER P */
@@ -1212,26 +1287,34 @@ function sq_fixIE_idiocy(&$attvalue) {
                               '&#xFF35;', '&#65333;',/* Unicode FULLWIDTH LATIN CAPITAL LETTER U */
                               '&#xFF55;', '&#65365;',/* Unicode FULLWIDTH LATIN SMALL LETTER U */
                               '&#x207F;', '&#8319;' ,/* Unicode SUPERSCRIPT LATIN SMALL LETTER N */
-                              '&#x8264;', /* Shift JIS FULLWIDTH LATIN CAPITAL LETTER E */   // in unicode this is some Chinese char range
-                              '&#x8285;', /* Shift JIS FULLWIDTH LATIN SMALL LETTER E */
-                              '&#x8277;', /* Shift JIS FULLWIDTH LATIN CAPITAL LETTER X */
-                              '&#x8298;', /* Shift JIS FULLWIDTH LATIN SMALL LETTER X */
-                              '&#x826F;', /* Shift JIS FULLWIDTH LATIN CAPITAL LETTER P */
-                              '&#x8290;', /* Shift JIS FULLWIDTH LATIN SMALL LETTER P */
-                              '&#x8271;', /* Shift JIS FULLWIDTH LATIN CAPITAL LETTER R */
-                              '&#x8292;', /* Shift JIS FULLWIDTH LATIN SMALL LETTER R */
-                              '&#x8272;', /* Shift JIS FULLWIDTH LATIN CAPITAL LETTER S */
-                              '&#x8293;', /* Shift JIS FULLWIDTH LATIN SMALL LETTER S */
-                              '&#x8268;', /* Shift JIS FULLWIDTH LATIN CAPITAL LETTER I */
-                              '&#x8289;', /* Shift JIS FULLWIDTH LATIN SMALL LETTER I */
-                              '&#x826E;', /* Shift JIS FULLWIDTH LATIN CAPITAL LETTER O */
-                              '&#x828F;', /* Shift JIS FULLWIDTH LATIN SMALL LETTER O */
-                              '&#x826D;', /* Shift JIS FULLWIDTH LATIN CAPITAL LETTER N */
-                              '&#x828E;'), /* Shift JIS FULLWIDTH LATIN SMALL LETTER N */
+                              "\xEF\xBC\xA5", /* Shift JIS FULLWIDTH LATIN CAPITAL LETTER E */   // in unicode this is some Chinese char range
+                              "\xEF\xBD\x85", /* Shift JIS FULLWIDTH LATIN SMALL LETTER E */
+                              "\xEF\xBC\xB8", /* Shift JIS FULLWIDTH LATIN CAPITAL LETTER X */
+                              "\xEF\xBD\x98", /* Shift JIS FULLWIDTH LATIN SMALL LETTER X */
+                              "\xEF\xBC\xB0", /* Shift JIS FULLWIDTH LATIN CAPITAL LETTER P */
+                              "\xEF\xBD\x90", /* Shift JIS FULLWIDTH LATIN SMALL LETTER P */
+                              "\xEF\xBC\xB2", /* Shift JIS FULLWIDTH LATIN CAPITAL LETTER R */
+                              "\xEF\xBD\x92", /* Shift JIS FULLWIDTH LATIN SMALL LETTER R */
+                              "\xEF\xBC\xB3", /* Shift JIS FULLWIDTH LATIN CAPITAL LETTER S */
+                              "\xEF\xBD\x93", /* Shift JIS FULLWIDTH LATIN SMALL LETTER S */
+                              "\xEF\xBC\xA9", /* Shift JIS FULLWIDTH LATIN CAPITAL LETTER I */
+                              "\xEF\xBD\x89", /* Shift JIS FULLWIDTH LATIN SMALL LETTER I */
+                              "\xEF\xBC\xAF", /* Shift JIS FULLWIDTH LATIN CAPITAL LETTER O */
+                              "\xEF\xBD\x8F", /* Shift JIS FULLWIDTH LATIN SMALL LETTER O */
+                              "\xEF\xBC\xAE", /* Shift JIS FULLWIDTH LATIN CAPITAL LETTER N */
+                              "\xEF\xBD\x8E", /* Shift JIS FULLWIDTH LATIN SMALL LETTER N */
+                              "\xEF\xBC\xAC", /* Shift JIS FULLWIDTH LATIN CAPITAL LETTER L */
+                              "\xEF\xBD\x8C", /* Shift JIS FULLWIDTH LATIN SMALL LETTER L */
+                              "\xEF\xBC\xB5", /* Shift JIS FULLWIDTH LATIN CAPITAL LETTER U */
+                              "\xEF\xBD\x95", /* Shift JIS FULLWIDTH LATIN SMALL LETTER U */
+                              "\xE2\x81\xBF", /* Shift JIS FULLWIDTH SUPERSCRIPT N */
+                              "\xCA\x9F", /* L UNICODE IPA Extension */
+                              "\xCA\x80", /* R UNICODE IPA Extension */
+                              "\xC9\xB4"),  /* N UNICODE IPA Extension */
                        array('l', 'l', 'r','r','n','n',
-                             'E','E','e','e','X','X','x','x','P','P','p','p','S','S','s','s','I','I',
-                             'i','i','O','O','o','o','N','N','n','n','L','L','l','l','U','U','u','u','n',
-                             'E','e','X','x','P','p','S','s','I','i','O','o','N','n'));
+                             'E','E','e','e','X','X','x','x','P','P','p','p','R','R','r','r','S','S','s','s','I','I',
+                             'i','i','O','O','o','o','N','N','n','n','L','L','l','l','U','U','u','u','n','n',
+                             'E','e','X','x','P','p','R','r','S','s','I','i','O','o','N','n','L','l','U','u','n','l','r','n'));
     $attvalue = str_replace($aDangerousCharsReplacementTable[0],$aDangerousCharsReplacementTable[1],$attvalue);
 
     // Escapes are useful for special characters like "{}[]()'&. In other cases they are
@@ -1746,38 +1829,34 @@ function sq_fixatts($tagname,
                             preg_replace($valmatch, $valrepl, $attvalue);
                         if ($newvalue != $attvalue){
                             $attary{$attname} = $newvalue;
+                            $attvalue = $newvalue;
                         }
                     }
                 }
             }
         }
-
-        /**
-         * Replace empty src tags with the blank image.  src is only used
-         * for frames, images, and image inputs.  Doing a replace should
-         * not affect them working as should be, however it will stop
-         * IE from being kicked off when src for img tags are not set
-         */
-        if (($attname == 'src') && ($attvalue == '""')) {
-            $attary{$attname} = '"' . SM_PATH . 'images/blank.png"';
-        }
-
-        /**
-         * Turn cid: urls into http-friendly ones.
-         */
-        if (preg_match("/^[\'\"]\s*cid:/si", $attvalue)){
-            $attary{$attname} = sq_cid2http($message, $id, $attvalue, $mailbox);
+        if ($attname == 'style') {
+            if (preg_match('/[\0-\37\200-\377]+/',$attvalue)) {
+                // 8bit and control characters in style attribute values can be used for XSS, remove them
+                $attary{$attname} = '"disallowed character"';
+            }
+            preg_match_all("/url\s*\((.+)\)/si",$attvalue,$aMatch);
+            if (count($aMatch)) {
+                foreach($aMatch[1] as $sMatch) {
+                    // url value
+                    $urlvalue = $sMatch;
+                    sq_fix_url($attname, $urlvalue, $message, $id, $mailbox,"'");
+                    $attary{$attname} = str_replace($sMatch,$urlvalue,$attvalue);
+                }
+            }
         }
-
         /**
-         * "Hack" fix for Outlook using propriatary outbind:// protocol in img tags.
-         * One day MS might actually make it match something useful, for now, falling
-         * back to using cid2http, so we can grab the blank.png.
+         * Use white list based filtering on attributes which can contain url's
          */
-        if (preg_match("/^[\'\"]\s*outbind:\/\//si", $attvalue)) {
-            $attary{$attname} = sq_cid2http($message, $id, $attvalue, $mailbox);
+        else if ($attname == 'href' || $attname == 'src' || $attname == 'background') {
+            sq_fix_url($attname, $attvalue, $message, $id, $mailbox);
+            $attary{$attname} = $attvalue;
         }
-
     }
     /**
      * See if we need to append any attributes to this tag.
@@ -1790,6 +1869,157 @@ function sq_fixatts($tagname,
     return $attary;
 }
 
+/**
+ * This function filters url's
+ *
+ * @param  $attvalue        String with attribute value to filter
+ * @param  $message         message object
+ * @param  $id               message id
+ * @param  $mailbox         mailbox
+ * @param  $sQuote          quoting characters around url's
+ */
+function sq_fix_url($attname, &$attvalue, $message, $id, $mailbox,$sQuote = '"') {
+    $attvalue = trim($attvalue);
+    if ($attvalue && ($attvalue[0] =='"'|| $attvalue[0] == "'")) {
+        // remove the double quotes
+        $sQuote = $attvalue[0];
+        $attvalue = trim(substr($attvalue,1,-1));
+    }
+
+    // If there's no "view_unsafe_images" variable in the URL, turn unsafe
+    // images off by default.
+    sqgetGlobalVar('view_unsafe_images', $view_unsafe_images, SQ_GET, FALSE);
+
+    $secremoveimg = '../images/' . _("sec_remove_eng.png");
+
+    /**
+     * Replace empty src tags with the blank image.  src is only used
+     * for frames, images, and image inputs.  Doing a replace should
+     * not affect them working as should be, however it will stop
+     * IE from being kicked off when src for img tags are not set
+     */
+    if ($attvalue == '') {
+        $attvalue = '"' . SM_PATH . 'images/blank.png"';
+    } else {
+        // first, disallow 8 bit characters and control characters
+        if (preg_match('/[\0-\37\200-\377]+/',$attvalue)) {
+            switch ($attname) {
+                case 'href':
+                    $attvalue = $sQuote . 'http://invalid-stuff-detected.example.com' . $sQuote;
+                    break;
+                default:
+                    $attvalue = $sQuote . SM_PATH . 'images/blank.png'. $sQuote;
+                    break;
+            }
+        } else {
+            $aUrl = parse_url($attvalue);
+            if (isset($aUrl['scheme'])) {
+                switch(strtolower($aUrl['scheme'])) {
+                    case 'mailto':
+                    case 'http':
+                    case 'https':
+                    case 'ftp':
+                        if ($attname != 'href') {
+                            if ($view_unsafe_images == false) {
+                                $attvalue = $sQuote . $secremoveimg . $sQuote;
+                            } else {
+                                if (isset($aUrl['path'])) {
+
+                                    // No one has been able to show that image URIs
+                                    // can be exploited, so for now, no restrictions
+                                    // are made at all.  If this proves to be a problem,
+                                    // the commented-out code below can be of help.
+                                    // (One consideration is that I see nothing in this
+                                    // function that specifically says that we will
+                                    // only ever arrive here when inspecting an image
+                                    // tag, although that does seem to be the end
+                                    // result - e.g., <script src="..."> where malicious
+                                    // image URIs are in fact a problem are already
+                                    // filtered out elsewhere.
+                                    /* ---------------------------------
+                                    // validate image extension.
+                                    $ext = strtolower(substr($aUrl['path'],strrpos($aUrl['path'],'.')));
+                                    if (!in_array($ext,array('.jpeg','.jpg','xjpeg','.gif','.bmp','.jpe','.png','.xbm'))) {
+                                        // If URI is to something other than
+                                        // a regular image file, get the contents
+                                        // and try to see if it is an image.
+                                        // Don't use Fileinfo (finfo_file()) because
+                                        // we'd need to make the admin configure the
+                                        // location of the magic.mime file (FIXME: add finfo_file() support later?)
+                                        //
+                                        $mime_type = '';
+                                        if (function_exists('mime_content_type')
+                                         && ($FILE = @fopen($attvalue, 'rb', FALSE))) {
+
+                                            // fetch file
+                                            //
+                                            $file_contents = '';
+                                            while (!feof($FILE)) {
+                                                $file_contents .= fread($FILE, 8192);
+                                            }
+                                            fclose($FILE);
+
+                                            // store file locally
+                                            //
+                                            global $attachment_dir, $username;
+                                            $hashed_attachment_dir = getHashedDir($username, $attachment_dir);
+                                            $localfilename = GenerateRandomString(32, '', 7);
+                                            $full_localfilename = "$hashed_attachment_dir/$localfilename";
+                                            while (file_exists($full_localfilename)) {
+                                                $localfilename = GenerateRandomString(32, '', 7);
+                                                $full_localfilename = "$hashed_attachment_dir/$localfilename";
+                                            }
+                                            $FILE = fopen("$hashed_attachment_dir/$localfilename", 'wb');
+                                            fwrite($FILE, $file_contents);
+                                            fclose($FILE);
+
+                                            // get mime type and remove file
+                                            //
+                                            $mime_type = mime_content_type("$hashed_attachment_dir/$localfilename");
+                                            unlink("$hashed_attachment_dir/$localfilename");
+                                        }
+                                        // debug: echo "$attvalue FILE TYPE IS $mime_type<HR>";
+                                        if (substr(strtolower($mime_type), 0, 5) != 'image') {
+                                            $attvalue = $sQuote . SM_PATH . 'images/blank.png'. $sQuote;
+                                        }
+                                    }
+                                    --------------------------------- */
+                                } else {
+                                    $attvalue = $sQuote . SM_PATH . 'images/blank.png'. $sQuote;
+                                }
+                            }
+                        } else {
+                            $attvalue = $sQuote . $attvalue . $sQuote;
+                        }
+                        break;
+                    case 'outbind':
+                        /**
+                         * "Hack" fix for Outlook using propriatary outbind:// protocol in img tags.
+                         * One day MS might actually make it match something useful, for now, falling
+                         * back to using cid2http, so we can grab the blank.png.
+                         */
+                        $attvalue = $sQuote . sq_cid2http($message, $id, $attvalue, $mailbox) . $sQuote;
+                        break;
+                    case 'cid':
+                        /**
+                            * Turn cid: urls into http-friendly ones.
+                            */
+                        $attvalue = $sQuote . sq_cid2http($message, $id, $attvalue, $mailbox) . $sQuote;
+                        break;
+                    default:
+                        $attvalue = $sQuote . SM_PATH . 'images/blank.png' . $sQuote;
+                        break;
+                }
+            } else {
+                if (!(isset($aUrl['path']) && $aUrl['path'] == $secremoveimg)) {
+                    // parse_url did not lead to satisfying result
+                    $attvalue = $sQuote . SM_PATH . 'images/blank.png' . $sQuote;
+                }
+            }
+        }
+    }
+}
+
 /**
  * This function edits the style definition to make them friendly and
  * usable in SquirrelMail.
@@ -1801,7 +2031,6 @@ function sq_fixatts($tagname,
  * @return           a string with edited content.
  */
 function sq_fixstyle($body, $pos, $message, $id, $mailbox){
-    global $view_unsafe_images;
     $me = 'sq_fixstyle';
     // workaround for </style> in between comments
     $iCurrentPos = $pos;
@@ -1813,7 +2042,7 @@ function sq_fixstyle($body, $pos, $message, $id, $mailbox){
         $char = $body{$i};
         switch ($char) {
             case '<':
-                $sToken .= $char;
+                $sToken = $char;
                 break;
             case '/':
                  if ($sToken == '<') {
@@ -1868,11 +2097,11 @@ function sq_fixstyle($body, $pos, $message, $id, $mailbox){
 
 
     /**
-    * First look for general BODY style declaration, which would be
-    * like so:
-    * body {background: blah-blah}
-    * and change it to .bodyclass so we can just assign it to a <div>
-    */
+     * First look for general BODY style declaration, which would be
+     * like so:
+     * body {background: blah-blah}
+     * and change it to .bodyclass so we can just assign it to a <div>
+     */
     $content = preg_replace("|body(\s*\{.*?\})|si", ".bodyclass\\1", $content);
     $secremoveimg = '../images/' . _("sec_remove_eng.png");
     /**
@@ -1881,6 +2110,12 @@ function sq_fixstyle($body, $pos, $message, $id, $mailbox){
     //   $content = preg_replace("|url\s*\(\s*([\'\"])\s*\S+script\s*:.*?([\'\"])\s*\)|si",
     //                           "url(\\1$secremoveimg\\2)", $content);
 
+    // first check for 8bit sequences and disallowed control characters
+    if (preg_match('/[\16-\37\200-\377]+/',$content)) {
+        $content = '<!-- style block removed by html filter due to presence of 8bit characters -->';
+        return array($content, $newpos);
+    }
+
     // IE Sucks hard. We have a special function for it.
     sq_fixIE_idiocy($content);
 
@@ -1890,51 +2125,22 @@ function sq_fixstyle($body, $pos, $message, $id, $mailbox){
     // translate ur\l and variations (IE parses that)
     // TODO check if the sq_fixIE_idiocy function already handles this.
     $content = preg_replace("/(\\\\)?u(\\\\)?r(\\\\)?l(\\\\)?/i", 'url', $content);
-    // NB I insert NUL characters to keep to avoid an infinite loop. They are removed after the loop.
-    while (preg_match("/url\s*\(\s*[\'\"]?([^:]+):(.*)?[\'\"]?\s*\)/si", $content, $matches)) {
-        $sProto = strtolower($matches[1]);
-        switch ($sProto) {
-            /**
-             * Fix url('https*://.*) declarations but only if $view_unsafe_images
-             * is false.
-             */
-            case 'https':
-            case 'http':
-                if (!$view_unsafe_images){
-
-                    $sExpr = "/url\s*\(\s*[\'\"]?\s*$sProto*:.*[\'\"]?\s*\)/si";
-                    $content = preg_replace($sExpr, "u\0r\0l(\\1$secremoveimg\\2)", $content);
-
-                } else {
-                    $content = preg_replace('/url/i',"u\0r\0l",$content);
-                }
-                break;
-            /**
-             * Fix urls that refer to cid:
-             */
-            case 'cid':
-                $cidurl = 'cid:'. $matches[2];
-                $httpurl = sq_cid2http($message, $id, $cidurl, $mailbox);
-                // escape parentheses that can modify the regular expression
-                $cidurl = str_replace(array('(',')'),array('\\(','\\)'),$cidurl);
-                $content = preg_replace("|url\s*\(\s*$cidurl\s*\)|si",
-                                        "u\0r\0l($httpurl)", $content);
-                break;
-            default:
-                /**
-                 * replace url with protocol other then the white list
-                 * http,https and cid by an empty string.
-                 */
-                $content = preg_replace("/url\s*\(\s*[\'\"]?([^:]+):(.*)?[\'\"]?\s*\)/si",
-                                 "", $content);
-                break;
+    preg_match_all("/url\s*\((.+)\)/si",$content,$aMatch);
+    if (count($aMatch)) {
+        $aValue = $aReplace = array();
+        foreach($aMatch[1] as $sMatch) {
+            // url value
+            $urlvalue = $sMatch;
+            sq_fix_url('style',$urlvalue, $message, $id, $mailbox,"'");
+            $aValue[] = $sMatch;
+            $aReplace[] = $urlvalue;
         }
+        $content = str_replace($aValue,$aReplace,$content);
     }
-    // remove NUL
-    $content = str_replace("\0", "", $content);
-   /**
-    * Remove any backslashes, entities, and extraneous whitespace.
-    */
+
+    /**
+     * Remove any backslashes, entities, and extraneous whitespace.
+     */
     $contentTemp = $content;
     sq_defang($contentTemp);
     sq_unspace($contentTemp);
@@ -1942,6 +2148,12 @@ function sq_fixstyle($body, $pos, $message, $id, $mailbox){
     /**
      * Fix stupid css declarations which lead to vulnerabilities
      * in IE.
+     *
+     * Also remove "position" attribute, as it can easily be set
+     * to "fixed" or "absolute" with "left" and "top" attributes
+     * of zero, taking over the whole content frame.  It can also
+     * be set to relative and move itself anywhere it wants to,
+     * displaying content in areas it shouldn't be allowed to touch.
      */
     $match   = Array('/\/\*.*\*\//',
                     '/expression/i',
@@ -1949,8 +2161,9 @@ function sq_fixstyle($body, $pos, $message, $id, $mailbox){
                     '/binding/i',
                     '/include-source/i',
                     '/javascript/i',
-                    '/script/i');
-    $replace = Array('','idiocy', 'idiocy', 'idiocy', 'idiocy', 'idiocy', 'idiocy');
+                    '/script/i',
+                    '/position/i');
+    $replace = Array('','idiocy', 'idiocy', 'idiocy', 'idiocy', 'idiocy', 'idiocy', '');
     $contentNew = preg_replace($match, $replace, $contentTemp);
     if ($contentNew !== $contentTemp) {
         // insecure css declarations are used. From now on we don't care
@@ -1988,8 +2201,8 @@ function sq_cid2http($message, $id, $cidurl, $mailbox){
     $cidurl = preg_replace($match_str, $str_rep, $cidurl);
 
     $linkurl = find_ent_id($cidurl, $message);
-    /* in case of non-save cid links $httpurl should be replaced by a sort of
-       unsave link image */
+    /* in case of non-safe cid links $httpurl should be replaced by a sort of
+       unsafe link image */
     $httpurl = '';
 
     /**
@@ -2011,7 +2224,7 @@ function sq_cid2http($message, $id, $cidurl, $mailbox){
     }
 
     if (!empty($linkurl)) {
-        $httpurl = $quotchar . SM_PATH . 'src/download.php?absolute_dl=true&amp;' .
+        $httpurl = $quotchar . sqm_baseuri() . 'src/download.php?absolute_dl=true&amp;' .
             "passed_id=$id&amp;mailbox=" . urlencode($mailbox) .
             '&amp;ent_id=' . $linkurl . $quotchar;
     } else {
@@ -2137,6 +2350,15 @@ function sq_sanitize($body,
             list($free_content, $curpos) =
                 sq_fixstyle($body, $gt+1, $message, $id, $mailbox);
             if ($free_content != FALSE){
+                $attary = sq_fixatts($tagname,
+                                     $attary,
+                                     $rm_attnames,
+                                     $bad_attvals,
+                                     $add_attr_to_tag,
+                                     $message,
+                                     $id,
+                                     $mailbox
+                                     );
                 $trusted .= sq_tagprint($tagname, $attary, $tagtype);
                 $trusted .= $free_content;
                 $trusted .= sq_tagprint($tagname, false, 2);
@@ -2355,12 +2577,28 @@ function magicHTML($body, $id, $message, $mailbox = 'INBOX', $take_mailto_links
                     "/binding/i",
                     "/behaviou*r/i",
                     "/include-source/i",
-                    "/position\s*:\s*absolute/i",
+
+                    // position:relative can also be exploited
+                    // to put content outside of email body area
+                    // and position:fixed is similarly exploitable
+                    // as position:absolute, so we'll remove it
+                    // altogether....
+                    //
+                    // Does this screw up legitimate HTML messages?
+                    // If so, the only fix I see is to allow position
+                    // attributes (any values?  I think we still have
+                    // to block static and fixed) only if $use_iframe
+                    // is enabled (1.5.0+)
+                    //
+                    // was:   "/position\s*:\s*absolute/i",
+                    //
+                    "/position\s*:/i",
+
                     "/(\\\\)?u(\\\\)?r(\\\\)?l(\\\\)?/i",
                     "/url\s*\(\s*([\'\"])\s*\S+script\s*:.*([\'\"])\s*\)/si",
                     "/url\s*\(\s*([\'\"])\s*mocha\s*:.*([\'\"])\s*\)/si",
                     "/url\s*\(\s*([\'\"])\s*about\s*:.*([\'\"])\s*\)/si",
-                    "/(.*)\s*:\s*url\s*\(\s*([\'\"]*)\s*\S+script\s*:.*([\'\"]*)\s*\)/si"
+                    "/(.*)\s*:\s*url\s*\(\s*([\'\"]*)\s*\S+script\s*:.*([\'\"]*)\s*\)/si",
                     ),
                 Array(
                     "",
@@ -2368,7 +2606,7 @@ function magicHTML($body, $id, $message, $mailbox = 'INBOX', $take_mailto_links
                     "idiocy",
                     "idiocy",
                     "idiocy",
-                    "",
+                    "idiocy",
                     "url",
                     "url(\\1#\\1)",
                     "url(\\1#\\1)",
@@ -2378,9 +2616,11 @@ function magicHTML($body, $id, $message, $mailbox = 'INBOX', $take_mailto_links
                 )
             )
         );
-    if( !sqgetGlobalVar('view_unsafe_images', $view_unsafe_images, SQ_GET) ) {
-        $view_unsafe_images = false;
-    }
+
+    // If there's no "view_unsafe_images" variable in the URL, turn unsafe
+    // images off by default.
+    sqgetGlobalVar('view_unsafe_images', $view_unsafe_images, SQ_GET, FALSE);
+
     if (!$view_unsafe_images){
         /**
          * Remove any references to http/https if view_unsafe_images set
@@ -2414,7 +2654,7 @@ function magicHTML($body, $id, $message, $mailbox = 'INBOX', $take_mailto_links
                            $id,
                            $mailbox
                            );
-    if (preg_match("|$secremoveimg|i", $trusted)){
+    if (strpos($trusted,$secremoveimg)){
         $has_unsafe_images = true;
     }
 
@@ -2508,7 +2748,7 @@ function SendDownloadHeaders($type0, $type1, $filename, $force, $filesize=0) {
         $filename =
             call_user_func($languages[$squirrelmail_language]['XTRA_CODE'] . '_downloadfilename', $filename, $HTTP_USER_AGENT);
     } else {
-        $filename = ereg_replace('[\\/:\*\?"<>\|;]', '_', str_replace('&nbsp;', ' ', $filename));
+        $filename = preg_replace('/[\\\\\/:*?"<>|;]/', '_', str_replace('&nbsp;', ' ', $filename));
     }
 
     // A Pox on Microsoft and it's Internet Explorer!