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/lib/Roundcube/rcube.php                          |  112 +++++++++++
 CHANGELOG                                                |    3 
 program/include/rcmail_output_html.php                   |  137 ++++++++++++-
 program/steps/addressbook/photo.inc                      |   10 
 program/steps/mail/compose.inc                           |    2 
 plugins/acl/acl.php                                      |   12 
 program/js/editor.js                                     |    5 
 program/steps/addressbook/func.inc                       |   11 
 program/steps/addressbook/delete.inc                     |    5 
 program/steps/mail/show.inc                              |    8 
 program/include/rcmail_output.php                        |    1 
 index.php                                                |   41 ---
 plugins/legacy_browser/js/iehacks.js                     |    2 
 program/steps/utils/error.inc                            |   12 +
 plugins/acl/acl.js                                       |   27 +
 program/include/rcmail.php                               |   83 ++++---
 program/steps/settings/func.inc                          |   18 +
 plugins/managesieve/lib/Roundcube/rcube_sieve_engine.php |   10 
 program/include/rcmail_output_json.php                   |    5 
 .htaccess                                                |    2 
 program/js/app.js                                        |   24 ++
 config/defaults.inc.php                                  |   22 ++
 22 files changed, 418 insertions(+), 134 deletions(-)

diff --git a/.htaccess b/.htaccess
index a9cc5c3..32c47f0 100644
--- a/.htaccess
+++ b/.htaccess
@@ -30,7 +30,7 @@
 # security rules:
 # - deny access to files not containing a dot or starting with a dot
 #   in all locations except installer directory
-RewriteRule ^(?!installer)(\.?[^\.]+)$ - [F]
+RewriteRule ^(?!installer|[a-f0-9]{16})(\.?[^\.]+)$ - [F]
 # - deny access to some locations
 RewriteRule ^/?(\.git|\.tx|SQL|bin|config|logs|temp|tests|program\/(include|lib|localization|steps)) - [F]
 # - deny access to some documentation files
diff --git a/CHANGELOG b/CHANGELOG
index 60ec324..5b541cd 100644
--- a/CHANGELOG
+++ b/CHANGELOG
@@ -1,11 +1,14 @@
 CHANGELOG Roundcube Webmail
 ===========================
 
+- Improve system security by using optional special URL with security token - use_secure_urls
+- Allow to define separate server/path for image/js/css files - assets_url/assets_dir
 - Fix import of multiple contact email addresses from Outlook-csv format (#1490169)
 - Fix drag-n-drop to folders expanded while dragging (#1490157)
 - Fix import of multiple contact groups from Google-csv format (#1490159)
 - Fix import of contacts with multiple email addresses from Google-csv format (#1490178)
 - Fix generation of Blowfish-based password hashes (#1490184)
+- Fix bugs where CSRF attacks were still possible on some requests
 
 RELEASE 1.1-beta
 ----------------
diff --git a/config/defaults.inc.php b/config/defaults.inc.php
index e369608..5a5bffb 100644
--- a/config/defaults.inc.php
+++ b/config/defaults.inc.php
@@ -534,6 +534,28 @@
 // Note: useful when SMTP server stores sent mail in user mailbox
 $config['no_save_sent_messages'] = false;
 
+// Improve system security by using special URL with security token.
+// This can be set to a number defining token length. Default: 16.
+// Warning: This requires http server configuration. Sample:
+//    RewriteRule ^/roundcubemail/[a-f0-9]{16}/(.*) /roundcubemail/$1 [PT]
+//    Alias /roundcubemail /var/www/roundcubemail/
+// Note: Use assets_path to not prevent the browser from caching assets
+$config['use_secure_urls'] = false;
+
+// Allows to define separate server/path for image/js/css files
+// Warning: If the domain is different cross-domain access to some
+// resources need to be allowed
+// Sample:
+//    <FilesMatch ".(eot|ttf|woff)">
+//    Header set Access-Control-Allow-Origin "*"
+//    </FilesMatch>
+$config['assets_path'] = '';
+
+// While assets_path is for the browser, assets_dir informs
+// PHP code about the location of asset files in filesystem
+$config['assets_dir'] = '';
+
+
 // ----------------------------------
 // PLUGINS
 // ----------------------------------
diff --git a/index.php b/index.php
index 5ca2af6..e19d81a 100644
--- a/index.php
+++ b/index.php
@@ -90,9 +90,9 @@
 
 // try to log in
 if ($RCMAIL->task == 'login' && $RCMAIL->action == 'login') {
-    $request_valid = $_SESSION['temp'] && $RCMAIL->check_request(rcube_utils::INPUT_POST, 'login');
+    $request_valid = $_SESSION['temp'] && $RCMAIL->check_request();
 
-    // purge the session in case of new login when a session already exists 
+    // purge the session in case of new login when a session already exists
     $RCMAIL->kill_session();
 
     $auth = $RCMAIL->plugins->exec_hook('authenticate', array(
@@ -140,7 +140,7 @@
         unset($redir['abort'], $redir['_err']);
 
         // send redirect
-        $OUTPUT->redirect($redir);
+        $OUTPUT->redirect($redir, 0, true);
     }
     else {
         if (!$auth['valid']) {
@@ -171,10 +171,10 @@
     }
 }
 
-// end session (after optional referer check)
-else if ($RCMAIL->task == 'logout' && isset($_SESSION['user_id'])
-    && (!$RCMAIL->config->get('referer_check') || rcube_utils::check_referer())
-) {
+// end session
+else if ($RCMAIL->task == 'logout' && isset($_SESSION['user_id'])) {
+    $RCMAIL->request_security_check($mode = rcube_utils::INPUT_GET);
+
     $userdata = array(
         'user' => $_SESSION['username'],
         'host' => $_SESSION['storage_host'],
@@ -234,32 +234,9 @@
 
     $OUTPUT->send($plugin['task']);
 }
-// CSRF prevention
 else {
-    // don't check for valid request tokens in these actions
-    $request_check_whitelist = array('login'=>1, 'spell'=>1, 'spell_html'=>1);
-
-    if (!$request_check_whitelist[$RCMAIL->action]) {
-        // check client X-header to verify request origin
-        if ($OUTPUT->ajax_call) {
-            if (rcube_utils::request_header('X-Roundcube-Request') != $RCMAIL->get_request_token()) {
-                header('HTTP/1.1 403 Forbidden');
-                die("Invalid Request");
-            }
-        }
-        // check request token in POST form submissions
-        else if (!empty($_POST) && !$RCMAIL->check_request()) {
-            $OUTPUT->show_message('invalidrequest', 'error');
-            $OUTPUT->send($RCMAIL->task);
-        }
-
-        // check referer if configured
-        if ($RCMAIL->config->get('referer_check') && !rcube_utils::check_referer()) {
-            raise_error(array(
-                'code' => 403, 'type' => 'php',
-                'message' => "Referer check failed"), true, true);
-        }
-    }
+    // CSRF prevention
+    $RCMAIL->request_security_check();
 
     // check access to disabled actions
     $disabled_actions = (array) $RCMAIL->config->get('disabled_actions');
diff --git a/plugins/acl/acl.js b/plugins/acl/acl.js
index e59ac72..1463453 100644
--- a/plugins/acl/acl.js
+++ b/plugins/acl/acl.js
@@ -58,8 +58,11 @@
     var users = this.acl_get_usernames();
 
     if (users && users.length && confirm(this.get_label('acl.deleteconfirm'))) {
-        this.http_request('settings/plugin.acl', '_act=delete&_user='+urlencode(users.join(','))
-            + '&_mbox='+urlencode(this.env.mailbox),
+        this.http_post('settings/plugin.acl', {
+                _act: 'delete',
+                _user: users.join(','),
+                _mbox: this.env.mailbox
+            },
             this.set_busy(true, 'acl.deleting'));
     }
 }
@@ -67,7 +70,7 @@
 // Save ACL data
 rcube_webmail.prototype.acl_save = function()
 {
-    var user = $('#acluser', this.acl_form).val(), rights = '', type;
+    var data, type, rights = '', user = $('#acluser', this.acl_form).val();
 
     $((this.env.acl_advanced ? '#advancedrights :checkbox' : '#simplerights :checkbox'), this.acl_form).map(function() {
         if (this.checked)
@@ -88,12 +91,18 @@
         return;
     }
 
-    this.http_request('settings/plugin.acl', '_act=save'
-        + '&_user='+urlencode(user)
-        + '&_acl=' +rights
-        + '&_mbox='+urlencode(this.env.mailbox)
-        + (this.acl_id ? '&_old='+this.acl_id : ''),
-        this.set_busy(true, 'acl.saving'));
+    data = {
+        _act: 'save',
+        _user: user,
+        _acl: rights,
+        _mbox: this.env.mailbox
+    }
+
+    if (this.acl_id) {
+        data._old = this.acl_id;
+    }
+
+    this.http_post('settings/plugin.acl', data, this.set_busy(true, 'acl.saving'));
 }
 
 // Cancel/Hide form
diff --git a/plugins/acl/acl.php b/plugins/acl/acl.php
index 349f7e5..35a92bb 100644
--- a/plugins/acl/acl.php
+++ b/plugins/acl/acl.php
@@ -454,10 +454,10 @@
      */
     private function action_save()
     {
-        $mbox  = trim(rcube_utils::get_input_value('_mbox', rcube_utils::INPUT_GPC, true)); // UTF7-IMAP
-        $user  = trim(rcube_utils::get_input_value('_user', rcube_utils::INPUT_GPC));
-        $acl   = trim(rcube_utils::get_input_value('_acl', rcube_utils::INPUT_GPC));
-        $oldid = trim(rcube_utils::get_input_value('_old', rcube_utils::INPUT_GPC));
+        $mbox  = trim(rcube_utils::get_input_value('_mbox', rcube_utils::INPUT_POST, true)); // UTF7-IMAP
+        $user  = trim(rcube_utils::get_input_value('_user', rcube_utils::INPUT_POST));
+        $acl   = trim(rcube_utils::get_input_value('_acl', rcube_utils::INPUT_POST));
+        $oldid = trim(rcube_utils::get_input_value('_old', rcube_utils::INPUT_POST));
 
         $acl    = array_intersect(str_split($acl), $this->rights_supported());
         $users  = $oldid ? array($user) : explode(',', $user);
@@ -510,8 +510,8 @@
      */
     private function action_delete()
     {
-        $mbox = trim(rcube_utils::get_input_value('_mbox', rcube_utils::INPUT_GPC, true)); //UTF7-IMAP
-        $user = trim(rcube_utils::get_input_value('_user', rcube_utils::INPUT_GPC));
+        $mbox = trim(rcube_utils::get_input_value('_mbox', rcube_utils::INPUT_POST, true)); //UTF7-IMAP
+        $user = trim(rcube_utils::get_input_value('_user', rcube_utils::INPUT_POST));
 
         $user = explode(',', $user);
 
diff --git a/plugins/legacy_browser/js/iehacks.js b/plugins/legacy_browser/js/iehacks.js
index 8f88e6f..105b7da 100644
--- a/plugins/legacy_browser/js/iehacks.js
+++ b/plugins/legacy_browser/js/iehacks.js
@@ -102,7 +102,7 @@
 rcube_webmail.prototype.async_upload_form_frame = function(name)
 {
   document.body.insertAdjacentHTML('BeforeEnd', '<iframe name="' + name + '"'
-    + ' src="program/resources/blank.gif" style="width:0; height:0; visibility:hidden"></iframe>');
+    + ' src="' + rcmail.assets_path('program/resources/blank.gif') + '" style="width:0; height:0; visibility:hidden"></iframe>');
 
   return $('iframe[name="' + name + '"]');
 };
diff --git a/plugins/managesieve/lib/Roundcube/rcube_sieve_engine.php b/plugins/managesieve/lib/Roundcube/rcube_sieve_engine.php
index 8d0dca4..25016c8 100644
--- a/plugins/managesieve/lib/Roundcube/rcube_sieve_engine.php
+++ b/plugins/managesieve/lib/Roundcube/rcube_sieve_engine.php
@@ -349,7 +349,7 @@
                 }
             }
             else if ($action == 'setact' && !$error) {
-                $script_name = rcube_utils::get_input_value('_set', rcube_utils::INPUT_GPC, true);
+                $script_name = rcube_utils::get_input_value('_set', rcube_utils::INPUT_POST, true);
                 $result = $this->activate_script($script_name);
                 $kep14  = $this->rc->config->get('managesieve_kolab_master');
 
@@ -363,7 +363,7 @@
                 }
             }
             else if ($action == 'deact' && !$error) {
-                $script_name = rcube_utils::get_input_value('_set', rcube_utils::INPUT_GPC, true);
+                $script_name = rcube_utils::get_input_value('_set', rcube_utils::INPUT_POST, true);
                 $result = $this->deactivate_script($script_name);
 
                 if ($result === true) {
@@ -376,7 +376,7 @@
                 }
             }
             else if ($action == 'setdel' && !$error) {
-                $script_name = rcube_utils::get_input_value('_set', rcube_utils::INPUT_GPC, true);
+                $script_name = rcube_utils::get_input_value('_set', rcube_utils::INPUT_POST, true);
                 $result = $this->remove_script($script_name);
 
                 if ($result === true) {
@@ -419,14 +419,14 @@
                 $this->rc->output->command('managesieve_updatelist', 'list', array('list' => $result));
             }
             else if ($action == 'ruleadd') {
-                $rid = rcube_utils::get_input_value('_rid', rcube_utils::INPUT_GPC);
+                $rid = rcube_utils::get_input_value('_rid', rcube_utils::INPUT_POST);
                 $id = $this->genid();
                 $content = $this->rule_div($fid, $id, false);
 
                 $this->rc->output->command('managesieve_rulefill', $content, $id, $rid);
             }
             else if ($action == 'actionadd') {
-                $aid = rcube_utils::get_input_value('_aid', rcube_utils::INPUT_GPC);
+                $aid = rcube_utils::get_input_value('_aid', rcube_utils::INPUT_POST);
                 $id = $this->genid();
                 $content = $this->action_div($fid, $id, false);
 
diff --git a/program/include/rcmail.php b/program/include/rcmail.php
index 27ec831..8b47a8d 100644
--- a/program/include/rcmail.php
+++ b/program/include/rcmail.php
@@ -760,49 +760,16 @@
     }
 
     /**
-     * Generate a unique token to be used in a form request
-     *
-     * @return string The request token
-     */
-    public function get_request_token()
-    {
-        $sess_id = $_COOKIE[ini_get('session.name')];
-
-        if (!$sess_id) {
-            $sess_id = session_id();
-        }
-
-        $plugin = $this->plugins->exec_hook('request_token', array(
-            'value' => md5('RT' . $this->get_user_id() . $this->config->get('des_key') . $sess_id)));
-
-        return $plugin['value'];
-    }
-
-    /**
-     * Check if the current request contains a valid token
-     *
-     * @param int Request method
-     *
-     * @return boolean True if request token is valid false if not
-     */
-    public function check_request($mode = rcube_utils::INPUT_POST)
-    {
-        $token   = rcube_utils::get_input_value('_token', $mode);
-        $sess_id = $_COOKIE[ini_get('session.name')];
-
-        return !empty($sess_id) && $token == $this->get_request_token();
-    }
-
-    /**
      * Build a valid URL to this instance of Roundcube
      *
      * @param mixed   Either a string with the action or url parameters as key-value pairs
      * @param boolean Build an URL absolute to document root
      * @param boolean Create fully qualified URL including http(s):// and hostname
+     * @param bool    Return absolute URL in secure location
      *
      * @return string Valid application URL
      */
-    public function url($p, $absolute = false, $full = false)
+    public function url($p, $absolute = false, $full = false, $secure = false)
     {
         if (!is_array($p)) {
             if (strpos($p, 'http') === 0) {
@@ -828,9 +795,23 @@
             }
         }
 
+        $base_path = strval($_SERVER['REDIRECT_SCRIPT_URL'] ?: $_SERVER['SCRIPT_NAME']);
+        $base_path = preg_replace('![^/]+$!', '', $base_path);
+
+        if ($secure && ($token = $this->get_secure_url_token(true))) {
+            // add token to the url
+            $url = $token . '/' . $url;
+
+            // remove old token from the path
+            $base_path = rtrim($base_path, '/');
+            $base_path = preg_replace('/\/[a-f0-9]{' . strlen($token) . '}$/', '', $base_path);
+
+            // this need to be full url to make redirects work
+            $absolute = true;
+        }
+
         if ($absolute || $full) {
             // add base path to this Roundcube installation
-            $base_path = preg_replace('![^/]+$!', '', strval($_SERVER['SCRIPT_NAME']));
             if ($base_path == '') $base_path = '/';
             $prefix = $base_path;
 
@@ -880,6 +861,36 @@
     }
 
     /**
+     * CSRF attack prevention code
+     *
+     * @param int Request mode
+     */
+    public function request_security_check($mode = rcube_utils::INPUT_POST)
+    {
+        // don't check for valid request tokens in these actions
+        // @TODO: get rid of this
+        $request_check_whitelist = array('spell'=>1, 'spell_html'=>1);
+
+        if ($request_check_whitelist[$this->action]) {
+            return;
+        }
+
+        // check request token
+        if (!$this->check_request($mode)) {
+            self::raise_error(array(
+                'code' => 403, 'type' => 'php',
+                'message' => "Request security check failed"), false, true);
+        }
+
+        // check referer if configured
+        if ($this->config->get('referer_check') && !rcube_utils::check_referer()) {
+            self::raise_error(array(
+                'code' => 403, 'type' => 'php',
+                'message' => "Referer check failed"), true, true);
+        }
+    }
+
+    /**
      * Registers action aliases for current task
      *
      * @param array $map Alias-to-filename hash array
diff --git a/program/include/rcmail_output.php b/program/include/rcmail_output.php
index 0f7aaf9..76ff4e7 100644
--- a/program/include/rcmail_output.php
+++ b/program/include/rcmail_output.php
@@ -28,6 +28,7 @@
 abstract class rcmail_output extends rcube_output
 {
     const JS_OBJECT_NAME = 'rcmail';
+    const BLANK_GIF      = 'R0lGODlhDwAPAIAAAMDAwAAAACH5BAEAAAAALAAAAAAPAA8AQAINhI+py+0Po5y02otnAQA7';
 
     public $type = 'html';
     public $ajax_call = false;
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++) {
diff --git a/program/include/rcmail_output_json.php b/program/include/rcmail_output_json.php
index fa35824..91262ac 100644
--- a/program/include/rcmail_output_json.php
+++ b/program/include/rcmail_output_json.php
@@ -181,6 +181,11 @@
      */
     public function raise_error($code, $message)
     {
+        if ($code == 403) {
+            header('HTTP/1.1 403 Forbidden');
+            die("Invalid Request");
+        }
+
         $this->show_message("Application Error ($code): $message", 'error');
         $this->remote_response();
         exit;
diff --git a/program/js/app.js b/program/js/app.js
index e0a6d26..4e65a9c 100644
--- a/program/js/app.js
+++ b/program/js/app.js
@@ -58,7 +58,6 @@
     request_timeout: 180,  // seconds
     draft_autosave: 0,     // seconds
     comm_path: './',
-    blankpage: 'program/resources/blank.gif',
     recipients_separator: ',',
     recipients_delimiter: ', ',
     popup_width: 1150,
@@ -162,6 +161,9 @@
       this.goto_url('error', '_code=0x199');
       return;
     }
+
+    if (!this.env.blankpage)
+      this.env.blankpage = this.assets_path('program/resources/blank.gif');
 
     // find all registered gui containers
     for (n in this.gui_containers)
@@ -1406,8 +1408,10 @@
 
     if (task == 'mail')
       url += '&_mbox=INBOX';
-    else if (task == 'logout' && !this.env.server_error)
+    else if (task == 'logout' && !this.env.server_error) {
+      url += '&_token=' + this.env.request_token;
       this.clear_compose_data();
+    }
 
     this.redirect(url);
   };
@@ -1417,7 +1421,10 @@
     if (!url)
       url = this.env.comm_path;
 
-    return url.replace(/_task=[a-z0-9_-]+/i, '_task='+task);
+    if (url.match(/[?&]_task=[a-zA-Z0-9_-]+/))
+        return url.replace(/_task=[a-zA-Z0-9_-]+/, '_task=' + task);
+    else
+        return url.replace(/\?.*$/, '') + '?_task=' + task;
   };
 
   this.reload = function(delay)
@@ -8039,7 +8046,7 @@
 
     img.onload = function() { ref.env.browser_capabilities.tif = 1; };
     img.onerror = function() { ref.env.browser_capabilities.tif = 0; };
-    img.src = 'program/resources/blank.tif';
+    img.src = this.assets_path('program/resources/blank.tif');
   };
 
   this.pdf_support_check = function()
@@ -8096,6 +8103,15 @@
     return 0;
   };
 
+  this.assets_path = function(path)
+  {
+    if (this.env.assets_path && !path.startsWith(this.env.assets_path)) {
+      path = this.env.assets_path + path;
+    }
+
+    return path;
+  };
+
   // Cookie setter
   this.set_cookie = function(name, value, expires)
   {
diff --git a/program/js/editor.js b/program/js/editor.js
index 3dac5f3..2fc3429 100644
--- a/program/js/editor.js
+++ b/program/js/editor.js
@@ -36,12 +36,13 @@
 function rcube_text_editor(config, id)
 {
   var ref = this,
+    abs_url = location.href.replace(/[?#].*$/, '').replace(/\/$/, ''),
     conf = {
       selector: '#' + ($('#' + id).is('.mce_editor') ? id : 'fake-editor-id'),
       cache_suffix: 's=4010700',
       theme: 'modern',
       language: config.lang,
-      content_css: 'program/js/tinymce/roundcube/content.css',
+      content_css: rcmail.assets_path('program/js/tinymce/roundcube/content.css'),
       menubar: false,
       statusbar: false,
       toolbar_items_size: 'small',
@@ -83,7 +84,7 @@
       toolbar: 'bold italic underline | alignleft aligncenter alignright alignjustify'
         + ' | bullist numlist outdent indent ltr rtl blockquote | forecolor backcolor | fontselect fontsizeselect'
         + ' | link unlink table | emoticons charmap image media | code searchreplace undo redo',
-      spellchecker_rpc_url: '../../../../../?_task=utils&_action=spell_html&_remote=1',
+      spellchecker_rpc_url: abs_url + '/?_task=utils&_action=spell_html&_remote=1',
       spellchecker_language: rcmail.env.spell_lang,
       accessibility_focus: false,
       file_browser_callback: function(name, url, type, win) { ref.file_browser_callback(name, url, type); },
diff --git a/program/lib/Roundcube/rcube.php b/program/lib/Roundcube/rcube.php
index 689823f..547e2b4 100644
--- a/program/lib/Roundcube/rcube.php
+++ b/program/lib/Roundcube/rcube.php
@@ -28,8 +28,14 @@
  */
 class rcube
 {
-    const INIT_WITH_DB = 1;
+    // Init options
+    const INIT_WITH_DB      = 1;
     const INIT_WITH_PLUGINS = 2;
+
+    // Request status
+    const REQUEST_VALID       = 0;
+    const REQUEST_ERROR_URL   = 1;
+    const REQUEST_ERROR_TOKEN = 2;
 
     /**
      * Singleton instace of rcube
@@ -101,6 +107,12 @@
      */
     public $user;
 
+    /**
+     * Request status
+     *
+     * @var int
+     */
+    public $request_status = 0;
 
     /* private/protected vars */
     protected $texts;
@@ -978,6 +990,104 @@
 
 
     /**
+     * Returns session token for secure URLs
+     *
+     * @param bool $generate Generate token if not exists in session yet
+     *
+     * @return string|bool Token string, False when disabled
+     */
+    public function get_secure_url_token($generate = false)
+    {
+        if ($len = $this->config->get('use_secure_urls')) {
+            if (empty($_SESSION['secure_token']) && $generate) {
+                // generate x characters long token
+                $length = $len > 1 ? $len : 16;
+                $token  = openssl_random_pseudo_bytes($length / 2);
+                $token  = bin2hex($token);
+
+                $plugin = $this->plugins->exec_hook('secure_token',
+                    array('value' => $token, 'length' => $length));
+
+                $_SESSION['secure_token'] = $plugin['value'];
+            }
+
+            return $_SESSION['secure_token'];
+        }
+
+        return false;
+    }
+
+
+    /**
+     * Generate a unique token to be used in a form request
+     *
+     * @return string The request token
+     */
+    public function get_request_token()
+    {
+        $sess_id = $_COOKIE[ini_get('session.name')];
+        if (!$sess_id) {
+            $sess_id = session_id();
+        }
+
+        $plugin = $this->plugins->exec_hook('request_token', array(
+            'value' => md5('RT' . $this->get_user_id() . $this->config->get('des_key') . $sess_id)));
+
+        return $plugin['value'];
+    }
+
+
+    /**
+     * Check if the current request contains a valid token.
+     * Empty requests aren't checked until use_secure_urls is set.
+     *
+     * @param int Request method
+     *
+     * @return boolean True if request token is valid false if not
+     */
+    public function check_request($mode = rcube_utils::INPUT_POST)
+    {
+        // check secure token in URL if enabled
+        if ($token = $this->get_secure_url_token()) {
+            foreach (explode('/', preg_replace('/[?#&].*$/', '', $_SERVER['REQUEST_URI'])) as $tok) {
+                if ($tok == $token) {
+                    return true;
+                }
+            }
+
+            $this->request_status = self::REQUEST_ERROR_URL;
+
+            return false;
+        }
+
+        $sess_tok = $this->get_request_token();
+
+        // ajax requests
+        if (rcube_utils::request_header('X-Roundcube-Request') == $sess_tok) {
+            return true;
+        }
+
+        // skip empty requests
+        if (($mode == rcube_utils::INPUT_POST && empty($_POST))
+            || ($mode == rcube_utils::INPUT_GET && empty($_GET))
+        ) {
+            return true;
+        }
+
+        // default method of securing requests
+        $token   = rcube_utils::get_input_value('_token', $mode);
+        $sess_id = $_COOKIE[ini_get('session.name')];
+
+        if (empty($sess_id) || $token != $sess_tok) {
+            $this->request_status = self::REQUEST_ERROR_TOKEN;
+            return false;
+        }
+
+        return true;
+    }
+
+
+    /**
      * Build a valid URL to this instance of Roundcube
      *
      * @param mixed Either a string with the action or url parameters as key-value pairs
diff --git a/program/steps/addressbook/delete.inc b/program/steps/addressbook/delete.inc
index f5b8e4e..9a23c59 100644
--- a/program/steps/addressbook/delete.inc
+++ b/program/steps/addressbook/delete.inc
@@ -20,10 +20,11 @@
 */
 
 // process ajax requests only
-if (!$OUTPUT->ajax_call)
+if (!$OUTPUT->ajax_call) {
     return;
+}
 
-$cids   = rcmail_get_cids();
+$cids   = rcmail_get_cids(null, rcube_utils::INPUT_POST);
 $delcnt = 0;
 
 // remove previous deletes
diff --git a/program/steps/addressbook/func.inc b/program/steps/addressbook/func.inc
index 008d201..c40b517 100644
--- a/program/steps/addressbook/func.inc
+++ b/program/steps/addressbook/func.inc
@@ -785,11 +785,12 @@
     if ($result = $CONTACTS->get_result())
         $record = $result->first();
 
-    $photo_img = $attrib['placeholder'] ? $RCMAIL->output->get_skin_file($attrib['placeholder']) : 'program/resources/blank.gif';
+    $photo_img = $attrib['placeholder'] ? $RCMAIL->output->abs_url($attrib['placeholder'], true) : 'program/resources/blank.gif';
     if ($record['_type'] == 'group' && $attrib['placeholdergroup'])
-        $photo_img = $RCMAIL->output->get_skin_file($attrib['placeholdergroup']);
+        $photo_img = $RCMAIL->output->abs_url($attrib['placeholdergroup'], true);
 
-    $RCMAIL->output->set_env('photo_placeholder', $photo_img);
+    $RCMAIL->output->set_env('photo_placeholder', $RCMAIL->output->asset_url($photo_img));
+
     unset($attrib['placeholder']);
 
     $plugin = $RCMAIL->plugins->exec_hook('contact_photo', array('record' => $record, 'data' => $record['photo']));
@@ -896,13 +897,13 @@
  *
  * @return array List of contact IDs per-source
  */
-function rcmail_get_cids($filter = null)
+function rcmail_get_cids($filter = null, $request_type = rcube_utils::INPUT_GPC)
 {
     // contact ID (or comma-separated list of IDs) is provided in two
     // forms. If _source is an empty string then the ID is a string
     // containing contact ID and source name in form: <ID>-<SOURCE>
 
-    $cid    = rcube_utils::get_input_value('_cid', rcube_utils::INPUT_GPC);
+    $cid    = rcube_utils::get_input_value('_cid', $request_type);
     $source = (string) rcube_utils::get_input_value('_source', rcube_utils::INPUT_GPC);
 
     if (is_array($cid)) {
diff --git a/program/steps/addressbook/photo.inc b/program/steps/addressbook/photo.inc
index 30d09ff..962ca31 100644
--- a/program/steps/addressbook/photo.inc
+++ b/program/steps/addressbook/photo.inc
@@ -90,6 +90,12 @@
     $RCMAIL->output->future_expire_header(86400);
 }
 
-header('Content-Type: ' . rcube_mime::image_content_type($data));
-echo $data ? $data : file_get_contents('program/resources/blank.gif');
+if ($data) {
+    header('Content-Type: ' . rcube_mime::image_content_type($data));
+    echo $data;
+}
+else {
+    header('Content-Type: image/gif');
+    echo base64_decode(rcmail_output::BLANK_GIF);
+}
 exit;
diff --git a/program/steps/mail/compose.inc b/program/steps/mail/compose.inc
index 5492f39..fd25cf4 100644
--- a/program/steps/mail/compose.inc
+++ b/program/steps/mail/compose.inc
@@ -951,7 +951,7 @@
             "googie.setCurrentLanguage('%s');\n".
             "googie.setDecoration(false);\n".
             "googie.decorateTextarea('%s');\n",
-            $RCMAIL->output->get_skin_path(),
+            $RCMAIL->output->asset_url($RCMAIL->output->get_skin_path()),
             $RCMAIL->url(array('_task' => 'utils', '_action' => 'spell', '_remote' => 1)),
                 !empty($dictionary) ? 'true' : 'false',
             rcube::JQ(rcube::Q($RCMAIL->gettext('checkspelling'))),
diff --git a/program/steps/mail/show.inc b/program/steps/mail/show.inc
index 5adc979..d9233a9 100644
--- a/program/steps/mail/show.inc
+++ b/program/steps/mail/show.inc
@@ -341,20 +341,20 @@
 {
     global $RCMAIL, $MESSAGE;
 
-    $placeholder = $attrib['placeholder'] ? $RCMAIL->config->get('skin_path') . $attrib['placeholder'] : null;
+    $placeholder = $attrib['placeholder'] ? $RCMAIL->output->abs_url($attrib['placeholder'], true) : null;
+    $placeholder = $RCMAIL->output->asset_url($placeholder ? $placeholder : 'program/resources/blank.gif');
 
     if ($MESSAGE->sender) {
         $photo_img = $RCMAIL->url(array(
             '_task'   => 'addressbook',
             '_action' => 'photo',
             '_email'  => $MESSAGE->sender['mailto'],
-            '_alt'    => $placeholder,
         ));
 
-        $attrib['onerror'] = "this.src = '" . ($placeholder ? $placeholder : 'program/resources/blank.gif') . "'";
+        $attrib['onerror'] = "this.src = '$placeholder'";
     }
     else {
-        $photo_img = $placeholder ? $placeholder : 'program/resources/blank.gif';
+        $photo_img = $placeholder;
     }
 
     return html::img(array('src' => $photo_img, 'alt' => $RCMAIL->gettext('contactphoto')) + $attrib);
diff --git a/program/steps/settings/func.inc b/program/steps/settings/func.inc
index f700e4f..486e679 100644
--- a/program/steps/settings/func.inc
+++ b/program/steps/settings/func.inc
@@ -334,13 +334,10 @@
                     $input    = new html_radiobutton(array('name'=>'_skin'));
 
                     foreach ($skins as $skin) {
-                        $thumbnail = "./skins/$skin/thumbnail.png";
-                        if (!is_file($thumbnail))
-                            $thumbnail = './program/resources/blank.gif';
-
+                        $thumbnail   = "skins/$skin/thumbnail.png";
                         $skinname    = ucfirst($skin);
                         $author_link = $license_link = '';
-                        $meta        = @json_decode(@file_get_contents("./skins/$skin/meta.json"), true);
+                        $meta        = @json_decode(@file_get_contents(INSTALL_PATH . "skins/$skin/meta.json"), true);
 
                         if (is_array($meta) && $meta['name']) {
                             $skinname     = $meta['name'];
@@ -348,10 +345,19 @@
                             $license_link = $meta['license-url'] ? html::a(array('href' => $meta['license-url'], 'target' => '_blank', 'tabindex' => '-1'), rcube::Q($meta['license'])) : rcube::Q($meta['license']);
                         }
 
+                        $img = html::img(array(
+                                'src'     => $thumbnail,
+                                'class'   => 'skinthumbnail',
+                                'alt'     => $skin,
+                                'width'   => 64,
+                                'height'  => 64,
+                                'onerror' => "this.src = rcmail.assets_path('program/resources/blank.gif')",
+                        ));
+
                         $skinnames[] = mb_strtolower($skinname);
                         $blocks['skin']['options'][$skin]['content'] = html::label(array('class' => 'skinselection'),
                             html::span('skinitem', $input->show($config['skin'], array('value' => $skin, 'id' => $field_id.$skin))) .
-                            html::span('skinitem', html::img(array('src' => $thumbnail, 'class' => 'skinthumbnail', 'alt' => $skin, 'width' => 64, 'height' => 64))) .
+                            html::span('skinitem', $img) .
                             html::span('skinitem', html::span('skinname', rcube::Q($skinname)) . html::br() .
                                 html::span('skinauthor', $author_link ? 'by ' . $author_link : '') . html::br() .
                                 html::span('skinlicense', $license_link ? $RCMAIL->gettext('license').':&nbsp;' . $license_link : ''))
diff --git a/program/steps/utils/error.inc b/program/steps/utils/error.inc
index ec0d038..6bbc57f 100644
--- a/program/steps/utils/error.inc
+++ b/program/steps/utils/error.inc
@@ -50,9 +50,17 @@
 
 // forbidden due to request check
 else if ($ERROR_CODE == 403) {
+    if ($_SERVER['REQUEST_METHOD'] == 'GET' && $rcmail->request_status == rcube::REQUEST_ERROR_URL) {
+        parse_str($_SERVER['QUERY_STRING'], $url);
+        $url = $rcmail->url($url, true, false, true);
+        $add = "<br /><a href=\"$url\">Click here to try again.<a/>";
+    }
+    else {
+        $add = "Please contact your server-administrator.";
+    }
+
     $__error_title = "REQUEST CHECK FAILED";
-    $__error_text  = "Access to this service was denied due to failing security checks!<br />\n"
-        . "Please contact your server-administrator.";
+    $__error_text  = "Access to this service was denied due to failing security checks!<br />\n$add";
 }
 
 // failed request (wrong step in URL)

--
Gitblit v1.9.1