summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorAleksander Machniak <alec@alec.pl>2014-12-16 13:28:48 +0100
committerAleksander Machniak <alec@alec.pl>2014-12-16 13:28:48 +0100
commit681ba6fc3c296cd6cd11050531b8f4e785141786 (patch)
tree77cd99edc9536c1e85e5ee057d231aa3aa5e0aba
parent53b7421d4419ce12c62d47e5b1231240cefdc3d5 (diff)
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
-rw-r--r--.htaccess2
-rw-r--r--CHANGELOG3
-rw-r--r--config/defaults.inc.php22
-rw-r--r--index.php41
-rw-r--r--plugins/acl/acl.js27
-rw-r--r--plugins/acl/acl.php12
-rw-r--r--plugins/legacy_browser/js/iehacks.js2
-rw-r--r--plugins/managesieve/lib/Roundcube/rcube_sieve_engine.php10
-rw-r--r--program/include/rcmail.php83
-rw-r--r--program/include/rcmail_output.php1
-rw-r--r--program/include/rcmail_output_html.php137
-rw-r--r--program/include/rcmail_output_json.php5
-rw-r--r--program/js/app.js24
-rw-r--r--program/js/editor.js5
-rw-r--r--program/lib/Roundcube/rcube.php112
-rw-r--r--program/steps/addressbook/delete.inc5
-rw-r--r--program/steps/addressbook/func.inc11
-rw-r--r--program/steps/addressbook/photo.inc10
-rw-r--r--program/steps/mail/compose.inc2
-rw-r--r--program/steps/mail/show.inc8
-rw-r--r--program/steps/settings/func.inc18
-rw-r--r--program/steps/utils/error.inc12
22 files changed, 418 insertions, 134 deletions
diff --git a/.htaccess b/.htaccess
index a9cc5c3f3..32c47f01e 100644
--- a/.htaccess
+++ b/.htaccess
@@ -30,7 +30,7 @@ RewriteRule ^favicon\.ico$ skins/larry/images/favicon.ico
# 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 60ec32467..5b541cda7 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 e369608a2..5a5bffb5e 100644
--- a/config/defaults.inc.php
+++ b/config/defaults.inc.php
@@ -534,6 +534,28 @@ $config['email_dns_check'] = false;
// 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 5ca2af608..e19d81a26 100644
--- a/index.php
+++ b/index.php
@@ -90,9 +90,9 @@ $RCMAIL->action = $startup['action'];
// 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 @@ if ($RCMAIL->task == 'login' && $RCMAIL->action == 'login') {
unset($redir['abort'], $redir['_err']);
// send redirect
- $OUTPUT->redirect($redir);
+ $OUTPUT->redirect($redir, 0, true);
}
else {
if (!$auth['valid']) {
@@ -171,10 +171,10 @@ if ($RCMAIL->task == 'login' && $RCMAIL->action == 'login') {
}
}
-// 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 @@ if (empty($RCMAIL->user->ID)) {
$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 e59ac72a2..14634534e 100644
--- a/plugins/acl/acl.js
+++ b/plugins/acl/acl.js
@@ -58,8 +58,11 @@ rcube_webmail.prototype.acl_delete = function()
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 @@ rcube_webmail.prototype.acl_delete = function()
// 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 @@ rcube_webmail.prototype.acl_save = function()
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 349f7e518..35a92bb1c 100644
--- a/plugins/acl/acl.php
+++ b/plugins/acl/acl.php
@@ -454,10 +454,10 @@ class acl extends rcube_plugin
*/
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 @@ class acl extends rcube_plugin
*/
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 8f88e6f57..105b7dabc 100644
--- a/plugins/legacy_browser/js/iehacks.js
+++ b/plugins/legacy_browser/js/iehacks.js
@@ -102,7 +102,7 @@ rcube_webmail.prototype.get_input_selection = function(obj)
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 8d0dca4d0..25016c878 100644
--- a/plugins/managesieve/lib/Roundcube/rcube_sieve_engine.php
+++ b/plugins/managesieve/lib/Roundcube/rcube_sieve_engine.php
@@ -349,7 +349,7 @@ class rcube_sieve_engine
}
}
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 @@ class rcube_sieve_engine
}
}
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 @@ class rcube_sieve_engine
}
}
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 @@ class rcube_sieve_engine
$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 27ec831e6..8b47a8d6c 100644
--- a/program/include/rcmail.php
+++ b/program/include/rcmail.php
@@ -760,49 +760,16 @@ class rcmail extends rcube
}
/**
- * 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 @@ class rcmail extends rcube
}
}
+ $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 @@ class rcmail extends rcube
}
/**
+ * 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 0f7aaf966..76ff4e75e 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 026e9f869..c6c43b532 100644
--- a/program/include/rcmail_output_html.php
+++ b/program/include/rcmail_output_html.php
@@ -45,6 +45,8 @@ class rcmail_output_html extends rcmail_output
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
@@ -80,6 +82,8 @@ class rcmail_output_html extends rcmail_output
$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);
if ($this->framed || $framed)
@@ -145,6 +149,55 @@ EOF;
}
/**
+ * 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);
+ }
+
+ /**
* Getter for the current page title
*
* @return string The page title
@@ -251,6 +304,7 @@ EOF;
* @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,10 +315,19 @@ EOF;
}
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;
+ }
+ }
}
return false;
@@ -369,14 +432,15 @@ EOF;
/**
* 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 @@ EOF;
// 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 @@ EOF;
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 @@ EOF;
}
/**
- * 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 @@ EOF;
}
/**
+ * 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 @@ EOF;
// 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 @@ EOF;
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 @@ EOF;
$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 @@ EOF;
}
$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 @@ EOF;
{
$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 fa35824db..91262acb3 100644
--- a/program/include/rcmail_output_json.php
+++ b/program/include/rcmail_output_json.php
@@ -181,6 +181,11 @@ class rcmail_output_json extends rcmail_output
*/
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 e0a6d26e3..4e65a9c6d 100644
--- a/program/js/app.js
+++ b/program/js/app.js
@@ -58,7 +58,6 @@ function rcube_webmail()
request_timeout: 180, // seconds
draft_autosave: 0, // seconds
comm_path: './',
- blankpage: 'program/resources/blank.gif',
recipients_separator: ',',
recipients_delimiter: ', ',
popup_width: 1150,
@@ -163,6 +162,9 @@ function rcube_webmail()
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)
this.gui_containers[n] = $('#'+this.gui_containers[n]);
@@ -1406,8 +1408,10 @@ function rcube_webmail()
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 @@ function rcube_webmail()
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 @@ function rcube_webmail()
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 @@ function rcube_webmail()
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 3dac5f371..2fc3429ab 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 @@ function rcube_text_editor(config, id)
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 689823fcb..547e2b4ac 100644
--- a/program/lib/Roundcube/rcube.php
+++ b/program/lib/Roundcube/rcube.php
@@ -28,9 +28,15 @@
*/
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 @@ class rcube
*/
public $user;
+ /**
+ * Request status
+ *
+ * @var int
+ */
+ public $request_status = 0;
/* private/protected vars */
protected $texts;
@@ -978,6 +990,104 @@ class rcube
/**
+ * 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 f5b8e4eb5..9a23c59bb 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 008d20174..c40b517dc 100644
--- a/program/steps/addressbook/func.inc
+++ b/program/steps/addressbook/func.inc
@@ -785,11 +785,12 @@ function rcmail_contact_photo($attrib)
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', $RCMAIL->output->asset_url($photo_img));
- $RCMAIL->output->set_env('photo_placeholder', $photo_img);
unset($attrib['placeholder']);
$plugin = $RCMAIL->plugins->exec_hook('contact_photo', array('record' => $record, 'data' => $record['photo']));
@@ -896,13 +897,13 @@ function rcmail_search_update($return = false)
*
* @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 30d09ffcc..962ca3126 100644
--- a/program/steps/addressbook/photo.inc
+++ b/program/steps/addressbook/photo.inc
@@ -90,6 +90,12 @@ if (!$cid && $email) {
$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 5492f390d..fd25cf402 100644
--- a/program/steps/mail/compose.inc
+++ b/program/steps/mail/compose.inc
@@ -951,7 +951,7 @@ function rcmail_compose_body($attrib)
"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 5adc97900..d9233a923 100644
--- a/program/steps/mail/show.inc
+++ b/program/steps/mail/show.inc
@@ -341,20 +341,20 @@ function rcmail_message_contactphoto($attrib)
{
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 f700e4f77..486e679c9 100644
--- a/program/steps/settings/func.inc
+++ b/program/steps/settings/func.inc
@@ -334,13 +334,10 @@ function rcmail_user_prefs($current = null)
$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 @@ function rcmail_user_prefs($current = null)
$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 ec0d038f4..6bbc57fda 100644
--- a/program/steps/utils/error.inc
+++ b/program/steps/utils/error.inc
@@ -50,9 +50,17 @@ else if ($ERROR_CODE == 401) {
// 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)