Aleksander Machniak
2014-12-16 681ba6fc3c296cd6cd11050531b8f4e785141786
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
22 files modified
552 ■■■■ changed files
.htaccess 2 ●●● patch | view | raw | blame | history
CHANGELOG 3 ●●●●● patch | view | raw | blame | history
config/defaults.inc.php 22 ●●●●● patch | view | raw | blame | history
index.php 41 ●●●● patch | view | raw | blame | history
plugins/acl/acl.js 27 ●●●●● patch | view | raw | blame | history
plugins/acl/acl.php 12 ●●●● patch | view | raw | blame | history
plugins/legacy_browser/js/iehacks.js 2 ●●● patch | view | raw | blame | history
plugins/managesieve/lib/Roundcube/rcube_sieve_engine.php 10 ●●●● patch | view | raw | blame | history
program/include/rcmail.php 83 ●●●●● patch | view | raw | blame | history
program/include/rcmail_output.php 1 ●●●● patch | view | raw | blame | history
program/include/rcmail_output_html.php 137 ●●●● patch | view | raw | blame | history
program/include/rcmail_output_json.php 5 ●●●●● patch | view | raw | blame | history
program/js/app.js 24 ●●●● patch | view | raw | blame | history
program/js/editor.js 5 ●●●●● patch | view | raw | blame | history
program/lib/Roundcube/rcube.php 112 ●●●●● patch | view | raw | blame | history
program/steps/addressbook/delete.inc 5 ●●●●● patch | view | raw | blame | history
program/steps/addressbook/func.inc 11 ●●●● patch | view | raw | blame | history
program/steps/addressbook/photo.inc 10 ●●●● patch | view | raw | blame | history
program/steps/mail/compose.inc 2 ●●● patch | view | raw | blame | history
program/steps/mail/show.inc 8 ●●●● patch | view | raw | blame | history
program/steps/settings/func.inc 18 ●●●●● patch | view | raw | blame | history
program/steps/utils/error.inc 12 ●●●● patch | view | raw | blame | history
.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
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
----------------
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
// ----------------------------------
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');
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
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);
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 + '"]');
};
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);
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
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;
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++) {
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;
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)
  {
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); },
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
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
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)) {
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;
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'))),
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);
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 : ''))
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)