CVE-2016-4069: Cross-Site Request Forgery (CSRF) (CWE-352)
value: Cross-site request forgery (CSRF) vulnerability in Roundcube Webmail before 1.1.5 allows remote attackers to hijack the authentication of users for requests that download attachments and cause a denial of service (disk consumption) via unspecified vectors.
CWE description: The web application does not, or can not, sufficiently verify whether a well-formed, valid, consistent request was intentionally provided by the user who submitted the request.
CVSS v3 base score: 8.8
Published: 2016-08-25T18:59Z
Repository: https://github.com/roundcube/roundcubemail
Language: PHP
Files changed: plugins/enigma/enigma.js, plugins/enigma/lib/enigma_ui.php, plugins/managesieve/lib/Roundcube/rcube_sieve_engine.php, plugins/managesieve/managesieve.js, plugins/zipdownload/zipdownload.js, plugins/zipdownload/zipdownload.php, program/include/rcmail.php, program/include/rcmail_output_html.php, program/js/app.js, program/lib/Roundcube/rcube_message.php, program/lib/Roundcube/rcube_output.php, program/steps/addressbook/export.inc, program/steps/mail/get.inc, program/steps/mail/viewsource.inc
Commit date: 2016-03-06 14:31:07 +0100
Commit message: Protect download urls against CSRF using unique request tokens (#1490642)
Send X-Frame-Options headers with every HTTP response

Diff: diff --git a/plugins/enigma/enigma.js b/plugins/enigma/enigma.js
index bd52d047ae..a5497f4b3c 100644
--- a/plugins/enigma/enigma.js
+++ b/plugins/enigma/enigma.js
@@ -155,11 +155,11 @@ rcube_webmail.prototype.enigma_export = function(selected)
     var keys = selected ? this.keys_list.get_selection().join(',') : '*';
 
     if (!keys.length)
         return;
 
-    this.goto_url('plugin.enigmakeys', {_a: 'export', _keys: keys});
+    this.goto_url('plugin.enigmakeys', {_a: 'export', _keys: keys}, false, true);
 };
 
 // Submit key(s) import form
 rcube_webmail.prototype.enigma_import = function()
 {
diff --git a/plugins/enigma/lib/enigma_ui.php b/plugins/enigma/lib/enigma_ui.php
index dfdacc7a65..ca28c321ac 100644
--- a/plugins/enigma/lib/enigma_ui.php
+++ b/plugins/enigma/lib/enigma_ui.php
@@ -457,10 +457,12 @@ class enigma_ui
     /**
      * Key(s) export handler
      */
     private function key_export()
     {
+        $this->rc->request_security_check(rcube_utils::INPUT_GET);
+
         $keys   = rcube_utils::get_input_value('_keys', rcube_utils::INPUT_GPC);
         $engine = $this->enigma->load_engine();
         $list   = $keys == '*' ? $engine->list_keys() : explode(',', $keys);
 
         if (is_array($list)) {
diff --git a/plugins/managesieve/lib/Roundcube/rcube_sieve_engine.php b/plugins/managesieve/lib/Roundcube/rcube_sieve_engine.php
index 67c9211614..3fb1684433 100644
--- a/plugins/managesieve/lib/Roundcube/rcube_sieve_engine.php
+++ b/plugins/managesieve/lib/Roundcube/rcube_sieve_engine.php
@@ -395,10 +395,12 @@ class rcube_sieve_engine
                 } else {
                     $this->rc->output->show_message('managesieve.setdeleteerror', 'error');
                 }
             }
             else if ($action == 'setget') {
+                $this->rc->request_security_check(rcube_utils::INPUT_GET);
+
                 $script_name = rcube_utils::get_input_value('_set', rcube_utils::INPUT_GPC, true);
                 $script      = $this->sieve->get_script($script_name);
 
                 if (is_a($script, 'PEAR_Error')) {
                     exit;
diff --git a/plugins/managesieve/managesieve.js b/plugins/managesieve/managesieve.js
index a69fa5a588..117f01a5a3 100644
--- a/plugins/managesieve/managesieve.js
+++ b/plugins/managesieve/managesieve.js
@@ -179,11 +179,11 @@ rcube_webmail.prototype.managesieve_list = function(script)
 rcube_webmail.prototype.managesieve_setget = function()
 {
   var id = this.filtersets_list.get_single_selection(),
     script = this.env.filtersets[id];
 
-  location.href = this.env.comm_path+'&_action=plugin.managesieve-action&_act=setget&_set='+urlencode(script);
+  this.goto_url('plugin.managesieve-action', {_act: 'setget', _set: script}, false, true);
 };
 
 // Set activate/deactivate request
 rcube_webmail.prototype.managesieve_setact = function()
 {
diff --git a/plugins/zipdownload/zipdownload.js b/plugins/zipdownload/zipdownload.js
index 228b04f8f0..6f918d2986 100644
--- a/plugins/zipdownload/zipdownload.js
+++ b/plugins/zipdownload/zipdownload.js
@@ -52,11 +52,11 @@ window.rcmail && rcmail.addEventListener('init', function(evt) {
 function rcmail_zipdownload(mode)
 {
     // default .eml download of single message
     if (mode == 'eml') {
         var uid = rcmail.get_single_uid();
-        rcmail.goto_url('viewsource', rcmail.params_from_uid(uid, {_save: 1}));
+        rcmail.goto_url('viewsource', rcmail.params_from_uid(uid, {_save: 1}), false, true);
         return;
     }
 
     // multi-message download, use hidden form to POST selection
     if (rcmail.message_list && rcmail.message_list.get_selection().length > 1) {
diff --git a/plugins/zipdownload/zipdownload.php b/plugins/zipdownload/zipdownload.php
index 2928f4978d..241de54893 100644
--- a/plugins/zipdownload/zipdownload.php
+++ b/plugins/zipdownload/zipdownload.php
@@ -61,11 +61,11 @@ class zipdownload extends rcube_plugin
         if (substr_count($p['content'], '<li') > $rcmail->config->get('zipdownload_attachments', 1)) {
             $href = $rcmail->url(array(
                 '_action' => 'plugin.zipdownload.attachments',
                 '_mbox'   => $rcmail->output->env['mailbox'],
                 '_uid'    => $rcmail->output->env['uid'],
-            ));
+            ), false, false, true);
 
             $link = html::a(array('href' => $href, 'class' => 'button zipdownload'),
                 rcube::Q($this->gettext('downloadall'))
             );
 
@@ -118,10 +118,14 @@ class zipdownload extends rcube_plugin
      * Handler for attachment download action
      */
     public function download_attachments()
     {
         $rcmail    = rcmail::get_instance();
+
+        // require CSRF protected request
+        $rcmail->request_security_check(rcube_utils::INPUT_GET);
+
         $imap      = $rcmail->get_storage();
         $temp_dir  = $rcmail->config->get('temp_dir');
         $tmpfname  = tempnam($temp_dir, 'zipdownload');
         $tempfiles = array($tmpfname);
         $message   = new rcube_message(rcube_utils::get_input_value('_uid', rcube_utils::INPUT_GET));
diff --git a/program/include/rcmail.php b/program/include/rcmail.php
index bdf9405dbb..0294392a66 100644
--- a/program/include/rcmail.php
+++ b/program/include/rcmail.php
@@ -838,10 +838,12 @@ class rcmail extends rcube
             $base_path = preg_replace('/\/[a-zA-Z0-9]{' . strlen($token) . '}$/', '', $base_path);
 
             // this need to be full url to make redirects work
             $absolute = true;
         }
+        else if ($secure && ($token = $this->get_request_token()))
+            $url .= $delm . '_token=' . urlencode($token);
 
         if ($absolute || $full) {
             // add base path to this Roundcube installation
             if ($base_path == '') $base_path = '/';
             $prefix = $base_path;
diff --git a/program/include/rcmail_output_html.php b/program/include/rcmail_output_html.php
index f6020107d2..8dda8c3c13 100644
--- a/program/include/rcmail_output_html.php
+++ b/program/include/rcmail_output_html.php
@@ -514,14 +514,14 @@ EOF;
         }
 
         // write all javascript commands
         $this->add_script($commands, 'head_top');
 
-        // send clickjacking protection headers
+        // allow (legal) iframe content to be loaded
         $iframe = $this->framed || $this->env['framed'];
-        if (!headers_sent() && ($xframe = $this->app->config->get('x_frame_options', 'sameorigin'))) {
-            header('X-Frame-Options: ' . ($iframe && $xframe == 'deny' ? 'sameorigin' : $xframe));
+        if (!headers_sent() && $iframe && $this->app->config->get('x_frame_options', 'sameorigin') === 'deny') {
+            header('X-Frame-Options: sameorigin', true);
         }
 
         // call super method
         $this->_write($template, $this->config->get('skin_path'));
     }
diff --git a/program/js/app.js b/program/js/app.js
index 36b6d1d4cf..075600c3d9 100644
--- a/program/js/app.js
+++ b/program/js/app.js
@@ -1022,11 +1022,11 @@ function rcube_webmail()
         if (command != 'download-attachment' && mimetype && this.env.mimetypes && $.inArray(mimetype, this.env.mimetypes) >= 0) {
           if (this.open_window(this.env.comm_path+'&_action=get&'+qstring+'&_frame=1'))
             break;
         }
 
-        this.goto_url('get', qstring+'&_download=1', false);
+        this.goto_url('get', qstring+'&_download=1', false, true);
         break;
 
       case 'select-all':
         this.select_all_mode = props ? false : true;
         this.dummy_select = true; // prevent msg opening if there's only one msg on the list
@@ -1228,14 +1228,14 @@ function rcube_webmail()
           this.open_window(this.url('viewsource', this.params_from_uid(uid)), true, true);
         break;
 
       case 'download':
         if (this.env.action == 'get') {
-          location.href = location.href.replace(/_frame=/, '_download=');
+          location.href = this.secure_url(location.href.replace(/_frame=/, '_download='));
         }
         else if (uid = this.get_single_uid()) {
-          this.goto_url('viewsource', this.params_from_uid(uid, {_save: 1}));
+          this.goto_url('viewsource', this.params_from_uid(uid, {_save: 1}), false, true);
         }
         break;
 
       // quicksearch
       case 'search':
@@ -1319,17 +1319,17 @@ function rcube_webmail()
           this.goto_url('import', (this.env.source ? '_target='+urlencode(this.env.source)+'&' : ''));
         break;
 
       case 'export':
         if (this.contact_list.rowcount > 0) {
-          this.goto_url('export', { _source: this.env.source, _gid: this.env.group, _search: this.env.search_request });
+          this.goto_url('export', { _source: this.env.source, _gid: this.env.group, _search: this.env.search_request }, false, true);
         }
         break;
 
       case 'export-selected':
         if (this.contact_list.rowcount > 0) {
-          this.goto_url('export', { _source: this.env.source, _gid: this.env.group, _cid: this.contact_list.get_selection().join(',') });
+          this.goto_url('export', { _source: this.env.source, _gid: this.env.group, _cid: this.contact_list.get_selection().join(',') }, false, true);
         }
         break;
 
       case 'upload-photo':
         this.upload_contact_photo(props || this.gui_objects.uploadform);
@@ -1440,11 +1440,11 @@ function rcube_webmail()
     var url = this.get_task_url(task);
 
     if (task == 'mail')
       url += '&_mbox=INBOX';
     else if (task == 'logout' && !this.env.server_error) {
-      url += '&_token=' + this.env.request_token;
+      url = this.secure_url(url);
       this.clear_compose_data();
     }
 
     this.redirect(url);
   };
@@ -1489,10 +1489,16 @@ function rcube_webmail()
     }
 
     return url + '?' + name + '=' + value;
   };
 
+  // append CSRF protection token to the given url
+  this.secure_url = function(url)
+  {
+    return this.add_url(url, '_token', this.env.request_token);
+  },
+
   this.is_framed = function()
   {
     return this.env.framed && parent.rcmail && parent.rcmail != this && typeof parent.rcmail.command == 'function';
   };
 
@@ -7897,13 +7903,15 @@ function rcube_webmail()
       }
       this.location_href(url, window);
     }
   };
 
-  this.goto_url = function(action, query, lock)
+  this.goto_url = function(action, query, lock, secure)
   {
-    this.redirect(this.url(action, query), lock);
+    var url = this.url(action, query)
+    if (secure) url = this.secure_url(url);
+    this.redirect(url, lock);
   };
 
   this.location_href = function(url, target, frame)
   {
     if (frame)
diff --git a/program/lib/Roundcube/rcube_message.php b/program/lib/Roundcube/rcube_message.php
index d21de34e64..10ebbf69f8 100644
--- a/program/lib/Roundcube/rcube_message.php
+++ b/program/lib/Roundcube/rcube_message.php
@@ -106,11 +106,12 @@ class rcube_message
             'safe'        => $this->is_safe,
             'prefer_html' => $this->app->config->get('prefer_html'),
             'get_url'     => $this->app->url(array(
                     'action' => 'get',
                     'mbox'   => $this->folder,
-                    'uid'    => $uid))
+                    'uid'    => $uid),
+                false, false, true)
         );
 
         if (!empty($this->headers->structure)) {
             $this->get_mime_numbers($this->headers->structure);
             $this->parse_structure($this->headers->structure);
diff --git a/program/lib/Roundcube/rcube_output.php b/program/lib/Roundcube/rcube_output.php
index 5df6721126..03ff4c1166 100644
--- a/program/lib/Roundcube/rcube_output.php
+++ b/program/lib/Roundcube/rcube_output.php
@@ -188,10 +188,15 @@ abstract class rcube_output
             header('X-UA-Compatible: IE=edge');
         }
 
         // Request browser to disable DNS prefetching (CVE-2010-0464)
         header("X-DNS-Prefetch-Control: off");
+
+        // send CSRF and clickjacking protection headers
+        if ($xframe = $this->app->config->get('x_frame_options', 'sameorigin')) {
+            header('X-Frame-Options: ' . $xframe);
+        }
     }
 
     /**
      * Show error page and terminate script execution
      *
diff --git a/program/steps/addressbook/export.inc b/program/steps/addressbook/export.inc
index 859d0f0967..b056a3e74a 100644
--- a/program/steps/addressbook/export.inc
+++ b/program/steps/addressbook/export.inc
@@ -19,10 +19,12 @@
  | Author: Thomas Bruederli <roundcube@gmail.com>                        |
  | Author: Aleksander Machniak <machniak@kolabsys.com>                   |
  +-----------------------------------------------------------------------+
 */
 
+$RCMAIL->request_security_check(rcube_utils::INPUT_GET);
+
 // Use search result
 if (!empty($_REQUEST['_search']) && isset($_SESSION['search'][$_REQUEST['_search']])) {
     $sort_col = $RCMAIL->config->get('addressbook_sort_col', 'name');
     $search  = (array)$_SESSION['search'][$_REQUEST['_search']];
     $records = array();
diff --git a/program/steps/mail/get.inc b/program/steps/mail/get.inc
index 6a70315c77..f81bdc5757 100644
--- a/program/steps/mail/get.inc
+++ b/program/steps/mail/get.inc
@@ -127,10 +127,14 @@ else if (strlen($part_id)) {
 
         if ($plugin['abort']) {
             exit;
         }
 
+        // require CSRF protected url for downloads
+        if ($plugin['download'])
+            $RCMAIL->request_security_check(rcube_utils::INPUT_GET);
+
         // overwrite modified vars from plugin
         $mimetype   = $plugin['mimetype'];
         $extensions = rcube_mime::get_mime_extensions($mimetype);
 
         if ($plugin['body']) {
diff --git a/program/steps/mail/viewsource.inc b/program/steps/mail/viewsource.inc
index 1198505abc..1e1fe263e9 100644
--- a/program/steps/mail/viewsource.inc
+++ b/program/steps/mail/viewsource.inc
@@ -17,10 +17,14 @@
  +-----------------------------------------------------------------------+
  | Author: Thomas Bruederli <roundcube@gmail.com>                        |
  +-----------------------------------------------------------------------+
 */
 
+if (!empty($_GET['_save'])) {
+    $RCMAIL->request_security_check(rcube_utils::INPUT_GET);
+}
+
 ob_end_clean();
 
 // similar code as in program/steps/mail/get.inc
 if ($uid = rcube_utils::get_input_value('_uid', rcube_utils::INPUT_GET)) {
     $headers = $RCMAIL->storage->get_message_headers($uid);

Vulnerable code:     this.goto_url('plugin.enigmakeys', {_a: 'export', _keys: keys});
  location.href = this.env.comm_path+'&_action=plugin.managesieve-action&_act=setget&_set='+urlencode(script);
        rcmail.goto_url('viewsource', rcmail.params_from_uid(uid, {_save: 1}));
            ));
        // send clickjacking protection headers
        if (!headers_sent() && ($xframe = $this->app->config->get('x_frame_options', 'sameorigin'))) {
            header('X-Frame-Options: ' . ($iframe && $xframe == 'deny' ? 'sameorigin' : $xframe));
        this.goto_url('get', qstring+'&_download=1', false);
          location.href = location.href.replace(/_frame=/, '_download=');
          this.goto_url('viewsource', this.params_from_uid(uid, {_save: 1}));
          this.goto_url('export', { _source: this.env.source, _gid: this.env.group, _search: this.env.search_request });
          this.goto_url('export', { _source: this.env.source, _gid: this.env.group, _cid: this.contact_list.get_selection().join(',') });
      url += '&_token=' + this.env.request_token;
  this.goto_url = function(action, query, lock)
    this.redirect(this.url(action, query), lock);
                    'uid'    => $uid))
Fixed code:     this.goto_url('plugin.enigmakeys', {_a: 'export', _keys: keys}, false, true);
        $this->rc->request_security_check(rcube_utils::INPUT_GET);

                $this->rc->request_security_check(rcube_utils::INPUT_GET);

  this.goto_url('plugin.managesieve-action', {_act: 'setget', _set: script}, false, true);
        rcmail.goto_url('viewsource', rcmail.params_from_uid(uid, {_save: 1}), false, true);
            ), false, false, true);

        // require CSRF protected request
        $rcmail->request_security_check(rcube_utils::INPUT_GET);

        else if ($secure && ($token = $this->get_request_token()))
            $url .= $delm . '_token=' . urlencode($token);
        // allow (legal) iframe content to be loaded
        if (!headers_sent() && $iframe && $this->app->config->get('x_frame_options', 'sameorigin') === 'deny') {
            header('X-Frame-Options: sameorigin', true);
        this.goto_url('get', qstring+'&_download=1', false, true);
          location.href = this.secure_url(location.href.replace(/_frame=/, '_download='));
          this.goto_url('viewsource', this.params_from_uid(uid, {_save: 1}), false, true);
          this.goto_url('export', { _source: this.env.source, _gid: this.env.group, _search: this.env.search_request }, false, true);
          this.goto_url('export', { _source: this.env.source, _gid: this.env.group, _cid: this.contact_list.get_selection().join(',') }, false, true);
      url = this.secure_url(url);
  // append CSRF protection token to the given url
  this.secure_url = function(url)
  {
    return this.add_url(url, '_token', this.env.request_token);
  },

  this.goto_url = function(action, query, lock, secure)
    var url = this.url(action, query)
    if (secure) url = this.secure_url(url);
    this.redirect(url, lock);
                    'uid'    => $uid),
                false, false, true)

        // send CSRF and clickjacking protection headers
        if ($xframe = $this->app->config->get('x_frame_options', 'sameorigin')) {
            header('X-Frame-Options: ' . $xframe);
        }
$RCMAIL->request_security_check(rcube_utils::INPUT_GET);

        // require CSRF protected url for downloads
        if ($plugin['download'])
            $RCMAIL->request_security_check(rcube_utils::INPUT_GET);

if (!empty($_GET['_save'])) {
    $RCMAIL->request_security_check(rcube_utils::INPUT_GET);
}