Fix broken mailto links created by some (Microsoft?) clients
[squirrelmail.git] / functions / mime.php
index 2e9d68e74c1a5d44efa72ba15d68f23dc0456a29..a140f7bcc41a80fe64df37fc10117507b31ddd03 100644 (file)
@@ -6,7 +6,7 @@
  * This contains the functions necessary to detect and decode MIME
  * messages.
  *
- * @copyright © 1999-2009 The SquirrelMail Project Team
+ * @copyright 1999-2018 The SquirrelMail Project Team
  * @license http://opensource.org/licenses/gpl-license.php GNU Public License
  * @version $Id$
  * @package squirrelmail
@@ -63,7 +63,7 @@ function mime_structure ($bodystructure, $flags=array()) {
         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>';
+        $errormessage .= '<pre>' . sm_encode_html_special_chars($read) . '</pre>';
         plain_error_message( $errormessage );
         echo '</body></html>';
         exit;
@@ -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') {
@@ -134,10 +137,16 @@ function mime_fetch_body($imap_stream, $id, $ent_id=1, $fetch_size=0) {
     $data = sqimap_run_command ($imap_stream, $cmd, true, $response, $message, TRUE);
     do {
         $topline = trim(array_shift($data));
-    } while($topline && ($topline[0] == '*') && !preg_match('/\* [0-9]+ FETCH.*/i', $topline)) ;
+    } while($topline && ($topline[0] == '*') && !preg_match('/\* [0-9]+ FETCH .*BODY.*/i', $topline)) ;
+    // Matching with "BODY" above is difficult: in most cases "FETCH \(BODY" would work
+    // but some servers may put other things in the same result, perhaps something such
+    // as "* 23 FETCH (FLAGS (\Seen) BODY[1] {174}".  There is some small chance that
+    // if the character sequence "BODY" appears in a response where it isn't actually
+    // a FETCH response data item name, the current regex will break things.  The better
+    // way to do this would be to parse the response correctly and not use a regex.
 
     $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,7 +154,7 @@ 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;
@@ -572,9 +581,9 @@ 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;
@@ -584,19 +593,23 @@ function buildAttachmentArray($message, $exclude_id, $mailbox, $id) {
            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, 
+        $temp = array(&$links, &$startMessage, &$id, &$urlMailbox, &$ent,
                     &$defaultlink, &$display_filename, &$where, &$what);
         do_hook("attachment $type0/$type1", $temp);
-        if(count($links) <= 1 && $defaultlink == $defaultlink_orig) {
-            /* 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_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. */
+        $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
@@ -619,7 +632,7 @@ function buildAttachmentArray($message, $exclude_id, $mailbox, $id) {
         $this_attachment['DownloadHREF'] = $links['download link']['href'];
         $this_attachment['ViewHREF'] = isset($links['attachment_common']) ? $links['attachment_common']['href'] : '';
         $this_attachment['Size'] = $header->size;
-        $this_attachment['ContentType'] = htmlspecialchars($type0 .'/'. $type1);
+        $this_attachment['ContentType'] = sm_encode_html_special_chars($type0 .'/'. $type1);
         $this_attachment['OtherLinks'] = array();
         foreach ($links as $val) {
             if ($val['text']==_("Download") || $val['text'] == _("View"))
@@ -793,6 +806,7 @@ function decodeHeader ($string, $utfencode=true,$htmlsafe=true,$decide=false) {
     $iLastMatch = -2;
     $encoded = true;
 
+// FIXME: spaces are allowed inside quoted-printable encoding, but the following line will bust up any such encoded strings
     $aString = explode(' ',$string);
     $ret = '';
     foreach ($aString as $chunk) {
@@ -818,7 +832,7 @@ function decodeHeader ($string, $utfencode=true,$htmlsafe=true,$decide=false) {
             $iLastMatch = $i;
             $j = $i;
             if ($htmlsafe) {
-                $ret .= htmlspecialchars($res[1]);
+                $ret .= sm_encode_html_special_chars($res[1]);
             } else {
                 $ret .= $res[1];
             }
@@ -847,14 +861,15 @@ function decodeHeader ($string, $utfencode=true,$htmlsafe=true,$decide=false) {
                         }
                     } else {
                         if ($htmlsafe) {
-                            $replace = htmlspecialchars($replace);
+                            $replace = sm_encode_html_special_chars($replace);
                         }
                         $ret.= $replace;
                     }
                     break;
                 case 'Q':
                     $replace = str_replace('_', ' ', $res[4]);
-                    $replace = preg_replace('/=([0-9a-f]{2})/ie', 'chr(hexdec("\1"))',
+                    $replace = preg_replace_callback('/=([0-9a-f]{2})/i',
+                            create_function ('$matches', 'return chr(hexdec($matches[1]));'),
                             $replace);
                     if ($utfencode) {
                         if ($can_be_encoded) {
@@ -868,7 +883,7 @@ function decodeHeader ($string, $utfencode=true,$htmlsafe=true,$decide=false) {
                         }
                     } else {
                         if ($htmlsafe) {
-                            $replace = htmlspecialchars($replace);
+                            $replace = sm_encode_html_special_chars($replace);
                         }
                     }
                     $ret .= $replace;
@@ -888,7 +903,7 @@ function decodeHeader ($string, $utfencode=true,$htmlsafe=true,$decide=false) {
         }
 
         if (!$encoded && $htmlsafe) {
-            $ret .= htmlspecialchars($chunk);
+            $ret .= sm_encode_html_special_chars($chunk);
         } else {
             $ret .= $chunk;
         }
@@ -955,6 +970,7 @@ function encodeHeader ($string) {
     for($i = 0; $i < $j; ++$i) {
         switch($string{$i})
         {
+            case '"':
             case '=':
             case '<':
             case '>':
@@ -1882,7 +1898,9 @@ function sq_fix_url($attname, &$attvalue, $message, $id, $mailbox,$sQuote = '"')
     // images off by default.
     sqgetGlobalVar('view_unsafe_images', $view_unsafe_images, SQ_GET, FALSE);
 
-    $secremoveimg = '../images/' . _("sec_remove_eng.png");
+    global $use_transparent_security_image;
+    if ($use_transparent_security_image) $secremoveimg = '../images/spacer.png';
+    else $secremoveimg = '../images/' . _("sec_remove_eng.png");
 
     /**
      * Replace empty src tags with the blank image.  src is only used
@@ -2003,7 +2021,7 @@ function sq_fix_url($attname, &$attvalue, $message, $id, $mailbox,$sQuote = '"')
                         break;
                 }
             } else {
-                if (!(isset($aUrl['path']) && $aUrl['path'] == $secremoveimg)) {
+                if (!isset($aUrl['path']) || $aUrl['path'] != $secremoveimg) {
                     // parse_url did not lead to satisfying result
                     $attvalue = $sQuote . SM_PATH . 'images/blank.png' . $sQuote;
                 }
@@ -2095,7 +2113,11 @@ function sq_fixstyle($body, $pos, $message, $id, $mailbox){
      * 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");
+
+    global $use_transparent_security_image;
+    if ($use_transparent_security_image) $secremoveimg = '../images/spacer.png';
+    else $secremoveimg = '../images/' . _("sec_remove_eng.png");
+
     /**
     * Fix url('blah') declarations.
     */
@@ -2140,6 +2162,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',
@@ -2147,8 +2175,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
@@ -2335,6 +2364,17 @@ function sq_sanitize($body,
             list($free_content, $curpos) =
                 sq_fixstyle($body, $gt+1, $message, $id, $mailbox);
             if ($free_content != FALSE){
+                if ( !empty($attary) ) {
+                    $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);
@@ -2515,7 +2555,10 @@ function magicHTML($body, $id, $message, $mailbox = 'INBOX', $take_mailto_links
                 )
             );
 
-    $secremoveimg = "../images/" . _("sec_remove_eng.png");
+    global $use_transparent_security_image;
+    if ($use_transparent_security_image) $secremoveimg = '../images/spacer.png';
+    else $secremoveimg = '../images/' . _("sec_remove_eng.png");
+
     $bad_attvals = Array(
             "/.*/" =>
             Array(
@@ -2553,12 +2596,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(
                     "",
@@ -2626,19 +2685,25 @@ function magicHTML($body, $id, $message, $mailbox = 'INBOX', $take_mailto_links
     if ($take_mailto_links) {
         // parseUrl($trusted);   // this even parses URLs inside of tags... too aggressive
         global $MailTo_PReg_Match;
-        $MailTo_PReg_Match = '/mailto:' . substr($MailTo_PReg_Match, 1) ;
+        // some mailers (Microsoft, surprise surprise) produce mailto strings without being
+        // inside an anchor (link) tag, so we have to make sure the regex looks for the
+        // quote before mailto, and we'll also try to convert the non-links back into links
+        $MailTo_PReg_Match = '/([\'"])?mailto:' . substr($MailTo_PReg_Match, 1) ;
         if ((preg_match_all($MailTo_PReg_Match, $trusted, $regs)) && ($regs[0][0] != '')) {
             foreach ($regs[0] as $i => $mailto_before) {
-                $mailto_params = $regs[10][$i];
+                $mailto_params = $regs[11][$i];
+
+                // get rid of any leading quote we may have captured but don't care about
+                //
+                $mailto_before = ltrim($mailto_before, '"\'');
+
                 // get rid of any tailing quote since we have to add send_to to the end
                 //
-                if (substr($mailto_before, strlen($mailto_before) - 1) == '"')
-                    $mailto_before = substr($mailto_before, 0, strlen($mailto_before) - 1);
-                if (substr($mailto_params, strlen($mailto_params) - 1) == '"')
-                    $mailto_params = substr($mailto_params, 0, strlen($mailto_params) - 1);
+                $mailto_before = rtrim($mailto_before, '"\'');
+                $mailto_params = rtrim($mailto_params, '"\'');
 
-                if ($regs[1][$i]) {    //if there is an email addr before '?', we need to merge it with the params
-                    $to = 'to=' . $regs[1][$i];
+                if ($regs[2][$i]) {    //if there is an email addr before '?', we need to merge it with the params
+                    $to = 'to=' . $regs[2][$i];
                     if (strpos($mailto_params, 'to=') > -1)    //already a 'to='
                         $mailto_params = str_replace('to=', $to . '%2C%20', $mailto_params);
                     else {
@@ -2663,8 +2728,12 @@ function magicHTML($body, $id, $message, $mailbox = 'INBOX', $take_mailto_links
                 // remove <a href=" and anything after the next quote (we only
                 // need the uri, not the link HTML) in compose uri
                 //
-                $comp_uri = substr($comp_uri, 9);
-                $comp_uri = substr($comp_uri, 0, strpos($comp_uri, '"', 1));
+                // but only do this if the original mailto was in a real anchor tag
+                //
+                if (!empty($regs[1][$i])) {
+                    $comp_uri = substr($comp_uri, 9);
+                    $comp_uri = substr($comp_uri, 0, strpos($comp_uri, '"', 1));
+                }
                 $trusted = str_replace($mailto_before, $comp_uri, $trusted);
             }
         }
@@ -2708,7 +2777,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!
@@ -2727,7 +2796,8 @@ function SendDownloadHeaders($type0, $type1, $filename, $force, $filesize=0) {
         $filename=rawurlencode($filename);
         header ("Pragma: public");
         header ("Cache-Control: no-store, max-age=0, no-cache, must-revalidate"); // HTTP/1.1
-        header ("Cache-Control: post-check=0, pre-check=0", false);
+        // does nothing - see: https://blogs.msdn.microsoft.com/ieinternals/2009/07/20/internet-explorers-cache-control-extensions/
+        // header ("Cache-Control: post-check=0, pre-check=0", false);
         header ("Cache-Control: private");
 
         //set the inline header for IE, we'll add the attachment header later if we need it