diff options
author | Aleksander Machniak <alec@alec.pl> | 2014-12-16 13:28:48 +0100 |
---|---|---|
committer | Aleksander Machniak <alec@alec.pl> | 2014-12-16 13:28:48 +0100 |
commit | 681ba6fc3c296cd6cd11050531b8f4e785141786 (patch) | |
tree | 77cd99edc9536c1e85e5ee057d231aa3aa5e0aba /program/include/rcmail_output_html.php | |
parent | 53b7421d4419ce12c62d47e5b1231240cefdc3d5 (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/rcmail_output_html.php')
-rw-r--r-- | program/include/rcmail_output_html.php | 137 |
1 files changed, 122 insertions, 15 deletions
diff --git a/program/include/rcmail_output_html.php b/program/include/rcmail_output_html.php index 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++) { |