From 681ba6fc3c296cd6cd11050531b8f4e785141786 Mon Sep 17 00:00:00 2001
From: Aleksander Machniak <alec@alec.pl>
Date: Tue, 16 Dec 2014 07:28:48 -0500
Subject: [PATCH] Improve system security by using optional special URL with security token Allows to define separate server/path for image/js/css files Fix bugs where CSRF attacks were still possible on some requests

---
 program/include/rcmail_output_html.php |  137 ++++++++++++++++++++++++++++++++++++++++-----
 1 files changed, 122 insertions(+), 15 deletions(-)

diff --git a/program/include/rcmail_output_html.php b/program/include/rcmail_output_html.php
index 026e9f8..c6c43b5 100644
--- a/program/include/rcmail_output_html.php
+++ b/program/include/rcmail_output_html.php
@@ -45,6 +45,8 @@
     protected $footer = '';
     protected $body = '';
     protected $base_path = '';
+    protected $assets_path;
+    protected $assets_dir = RCUBE_INSTALL_PATH;
     protected $devel_mode = false;
 
     // deprecated names of templates used before 0.5
@@ -79,6 +81,8 @@
         $skin = $this->config->get('skin');
         $this->set_skin($skin);
         $this->set_env('skin', $skin);
+
+        $this->set_assets_path($this->config->get('assets_path'), $this->config->get('assets_dir'));
 
         if (!empty($_REQUEST['_extwin']))
             $this->set_env('extwin', 1);
@@ -142,6 +146,55 @@
         if ($addtojs || isset($this->js_env[$name])) {
             $this->js_env[$name] = $value;
         }
+    }
+
+    /**
+     * Parse and set assets path
+     *
+     * @param string Assets path (relative or absolute URL)
+     */
+    public function set_assets_path($path, $fs_dir = null)
+    {
+        if (empty($path)) {
+            return;
+        }
+
+        $path = rtrim($path, '/') . '/';
+
+        // handle relative assets path
+        if (!preg_match('|^https?://|', $path) && $path[0] != '/') {
+            // save the path to search for asset files later
+            $this->assets_dir = $path;
+
+            $base = preg_replace('/[?#&].*$/', '', $_SERVER['REQUEST_URI']);
+            $base = rtrim($base, '/');
+
+            // remove url token if exists
+            if ($len = intval($this->config->get('use_secure_urls'))) {
+                $_base  = explode('/', $base);
+                $last   = count($_base) - 1;
+                $length = $len > 1 ? $len : 16; // as in rcube::get_secure_url_token()
+
+                // we can't use real token here because it
+                // does not exists in unauthenticated state,
+                // hope this will not produce false-positive matches
+                if ($last > -1 && preg_match('/^[a-f0-9]{' . $length . '}$/', $_base[$last])) {
+                    $path = '../' . $path;
+                }
+            }
+        }
+
+        // set filesystem path for assets
+        if ($fs_dir) {
+            if ($fs_dir[0] != '/') {
+                $fs_dir = realpath(RCUBE_INSTALL_PATH . $fs_dir);
+            }
+            // ensure the path ends with a slash
+            $this->assets_dir = rtrim($fs_dir, '/') . '/';
+        }
+
+        $this->assets_path = $path;
+        $this->set_env('assets_path', $path);
     }
 
     /**
@@ -251,6 +304,7 @@
      * @param string File name/path to resolve (starting with /)
      * @param string Reference to the base path of the matching skin
      * @param string Additional path to search in
+     *
      * @return mixed Relative path to the requested file or False if not found
      */
     public function get_skin_file($file, &$skin_path = null, $add_path = null)
@@ -261,9 +315,18 @@
         }
 
         foreach ($skin_paths as $skin_path) {
-            $path = realpath($skin_path . $file);
-            if (is_file($path)) {
+            $path = realpath(RCUBE_INSTALL_PATH . $skin_path . $file);
+
+            if ($path && is_file($path)) {
                 return $skin_path . $file;
+            }
+
+            if ($this->assets_dir != RCUBE_INSTALL_PATH) {
+                $path = realpath($this->assets_dir . $skin_path . $file);
+
+                if ($path && is_file($path)) {
+                    return $skin_path . $file;
+                }
             }
         }
 
@@ -369,14 +432,15 @@
     /**
      * Redirect to a certain url
      *
-     * @param mixed $p     Either a string with the action or url parameters as key-value pairs
-     * @param int   $delay Delay in seconds
+     * @param mixed $p      Either a string with the action or url parameters as key-value pairs
+     * @param int   $delay  Delay in seconds
+     * @param bool  $secure Redirect to secure location (see rcmail::url())
      */
-    public function redirect($p = array(), $delay = 1)
+    public function redirect($p = array(), $delay = 1, $secure = false)
     {
         if ($this->env['extwin'])
             $p['extwin'] = 1;
-        $location = $this->app->url($p);
+        $location = $this->app->url($p, false, false, $secure);
         header('Location: ' . $location);
         exit;
     }
@@ -490,11 +554,11 @@
         // find skin template
         $path = false;
         foreach ($this->skin_paths as $skin_path) {
-            $path = "$skin_path/templates/$name.html";
+            $path = RCUBE_INSTALL_PATH . "$skin_path/templates/$name.html";
 
             // fallback to deprecated template names
             if (!is_readable($path) && $this->deprecated_templates[$realname]) {
-                $path = "$skin_path/templates/" . $this->deprecated_templates[$realname] . ".html";
+                $path = RCUBE_INSTALL_PATH . "$skin_path/templates/" . $this->deprecated_templates[$realname] . ".html";
 
                 if (is_readable($path)) {
                     rcube::raise_error(array(
@@ -667,6 +731,21 @@
         exit;
     }
 
+    /**
+     * Modify path by adding URL prefix if configured
+     */
+    public function asset_url($path)
+    {
+        // iframe content can't be in a different domain
+        // @TODO: check if assests are on a different domain
+
+        if (!$this->assets_path || in_array($path[0], array('?', '/', '.')) || strpos($path, '://')) {
+            return $path;
+        }
+
+        return $this->assets_path . $path;
+    }
+
 
     /*****  Template parsing methods  *****/
 
@@ -704,7 +783,7 @@
     }
 
     /**
-     * Callback function for preg_replace_callback in write()
+     * Callback function for preg_replace_callback in fix_paths()
      *
      * @return string Parsed string
      */
@@ -727,6 +806,28 @@
     }
 
     /**
+     * Correct paths of asset files according to assets_path
+     */
+    protected function fix_assets_paths($output)
+    {
+        return preg_replace_callback(
+            '!(src|href|background)=(["\']?)([a-z0-9/_.?=-]+)(["\'\s>])!i',
+            array($this, 'assets_callback'), $output);
+    }
+
+    /**
+     * Callback function for preg_replace_callback in fix_assets_paths()
+     *
+     * @return string Parsed string
+     */
+    protected function assets_callback($matches)
+    {
+        $file = $this->asset_url($matches[3]);
+
+        return $matches[1] . '=' . $matches[2] . $file . $matches[4];
+    }
+
+    /**
      * Modify file by adding mtime indicator
      */
     protected function file_mod($file)
@@ -737,12 +838,12 @@
         // use minified file if exists (not in development mode)
         if (!$this->devel_mode && !preg_match('/\.min\.' . $ext . '$/', $file)) {
             $minified_file = substr($file, 0, strlen($ext) * -1) . 'min.' . $ext;
-            if ($fs = @filemtime($minified_file)) {
+            if ($fs = @filemtime($this->assets_dir . $minified_file)) {
                 return $minified_file . '?s=' . $fs;
             }
         }
 
-        if ($fs = @filemtime($file)) {
+        if ($fs = @filemtime($this->assets_dir . $file)) {
             $file .= '?s=' . $fs;
         }
 
@@ -969,7 +1070,7 @@
                 if (!empty($attrib['skin_path'])) $attrib['skinpath'] = $attrib['skin_path'];
                 if ($path = $this->get_skin_file($attrib['file'], $skin_path, $attrib['skinpath'])) {
                     $this->base_path = preg_replace('!plugins/\w+/!', '', $skin_path);  // set base_path to core skin directory (not plugin's skin)
-                    $path = realpath($path);
+                    $path = realpath(RCUBE_INSTALL_PATH . $path);
                 }
 
                 if (is_readable($path)) {
@@ -1521,6 +1622,10 @@
 
         $output = $this->parse_with_globals($this->fix_paths($output));
 
+        if ($this->assets_path) {
+            $output = $this->fix_assets_paths($output);
+        }
+
         // trigger hook with final HTML content to be sent
         $hook = $this->app->plugins->exec_hook("send_page", array('content' => $output));
         if (!$hook['abort']) {
@@ -1549,12 +1654,12 @@
         }
 
         $attrib['name'] = $attrib['id'];
-        $attrib['src'] = $attrib['src'] ? $this->abs_url($attrib['src'], true) : 'program/resources/blank.gif';
+        $attrib['src']  = $attrib['src'] ? $this->abs_url($attrib['src'], true) : 'program/resources/blank.gif';
 
         // register as 'contentframe' object
         if ($is_contentframe || $attrib['contentframe']) {
             $this->set_env('contentframe', $attrib['contentframe'] ? $attrib['contentframe'] : $attrib['name']);
-            $this->set_env('blankpage', $attrib['src']);
+            $this->set_env('blankpage', $this->asset_url($attrib['src']));
         }
 
         return html::iframe($attrib);
@@ -1766,9 +1871,11 @@
     {
         $images = preg_split('/[\s\t\n,]+/', $attrib['images'], -1, PREG_SPLIT_NO_EMPTY);
         $images = array_map(array($this, 'abs_url'), $images);
+        $images = array_map(array($this, 'asset_url'), $images);
 
-        if (empty($images) || $this->app->task == 'logout')
+        if (empty($images) || $_REQUEST['_task'] == 'logout') {
             return;
+        }
 
         $this->add_script('var images = ' . self::json_serialize($images) .';
             for (var i=0; i<images.length; i++) {

--
Gitblit v1.9.1