summaryrefslogtreecommitdiff
path: root/program/include
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 /program/include
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
Diffstat (limited to 'program/include')
-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
4 files changed, 175 insertions, 51 deletions
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;