File roundcubemail-1.1-csrf.patch of Package roundcubemail (Revision bf1458aad63086b6e6584b282e39f559)

Currently displaying revision bf1458aad63086b6e6584b282e39f559 , Show latest

812
 
1
diff --git a/.htaccess b/.htaccess
2
index f21cf34..d0efd31 100644
3
--- a/.htaccess
4
+++ b/.htaccess
5
@@ -31,7 +31,7 @@ RewriteRule ^favicon\.ico$ skins/larry/images/favicon.ico
6
 # security rules:
7
 # - deny access to files not containing a dot or starting with a dot
8
 #   in all locations except installer directory
9
-RewriteRule ^(?!installer)(\.?[^\.]+)$ - [F]
10
+RewriteRule ^(?!installer|[a-f0-9]{16})(\.?[^\.]+)$ - [F]
11
 # - deny access to some locations
12
 RewriteRule ^/?(\.git|\.tx|SQL|bin|config|logs|temp|tests|program\/(include|lib|localization|steps)) - [F]
13
 # - deny access to some documentation files
14
diff --git a/config/defaults.inc.php b/config/defaults.inc.php
15
index 56791fb..3297dc2 100644
16
--- a/config/defaults.inc.php
17
+++ b/config/defaults.inc.php
18
@@ -532,6 +532,23 @@ $config['email_dns_check'] = false;
19
 // Note: useful when SMTP server stores sent mail in user mailbox
20
 $config['no_save_sent_messages'] = false;
21
 
22
+// Improve system security by using special URL with security token.
23
+// This can be set to a number defining token length. Default: 16.
24
+// Warning: This requires http server configuration. Sample:
25
+//    RewriteRule ^/roundcubemail/[a-f0-9]{16}/(.*) /roundcubemail/$1 [PT]
26
+//    Alias /roundcubemail /var/www/roundcubemail/
27
+// Note: Use assets_path to not prevent the browser from caching assets
28
+$config['use_secure_urls'] = false;
29
+
30
+// Allows to define separate server/path for image/js/css files
31
+// Warning: If the domain is different cross-domain access to some
32
+// resources need to be allowed
33
+// Sample:
34
+//    <FilesMatch ".(eot|ttf|woff)">
35
+//    Header set Access-Control-Allow-Origin "*"
36
+//    </FilesMatch>
37
+$config['assets_path'] = '';
38
+
39
 // ----------------------------------
40
 // PLUGINS
41
 // ----------------------------------
42
diff --git a/index.php b/index.php
43
index 3154daf..7001918 100644
44
--- a/index.php
45
+++ b/index.php
46
@@ -90,9 +90,9 @@ $RCMAIL->action = $startup['action'];
47
 
48
 // try to log in
49
 if ($RCMAIL->task == 'login' && $RCMAIL->action == 'login') {
50
-    $request_valid = $_SESSION['temp'] && $RCMAIL->check_request(rcube_utils::INPUT_POST, 'login');
51
+    $request_valid = $_SESSION['temp'] && $RCMAIL->check_request();
52
 
53
-    // purge the session in case of new login when a session already exists 
54
+    // purge the session in case of new login when a session already exists
55
     $RCMAIL->kill_session();
56
 
57
     $auth = $RCMAIL->plugins->exec_hook('authenticate', array(
58
@@ -140,7 +140,7 @@ if ($RCMAIL->task == 'login' && $RCMAIL->action == 'login') {
59
         unset($redir['abort'], $redir['_err']);
60
 
61
         // send redirect
62
-        $OUTPUT->redirect($redir);
63
+        $OUTPUT->redirect($redir, 0, true);
64
     }
65
     else {
66
         if (!$auth['valid']) {
67
@@ -171,10 +171,10 @@ if ($RCMAIL->task == 'login' && $RCMAIL->action == 'login') {
68
     }
69
 }
70
 
71
-// end session (after optional referer check)
72
-else if ($RCMAIL->task == 'logout' && isset($_SESSION['user_id'])
73
-    && (!$RCMAIL->config->get('referer_check') || rcube_utils::check_referer())
74
-) {
75
+// end session
76
+else if ($RCMAIL->task == 'logout' && isset($_SESSION['user_id'])) {
77
+    $RCMAIL->request_security_check($mode = rcube_utils::INPUT_GET);
78
+
79
     $userdata = array(
80
         'user' => $_SESSION['username'],
81
         'host' => $_SESSION['storage_host'],
82
@@ -234,32 +234,9 @@ if (empty($RCMAIL->user->ID)) {
83
 
84
     $OUTPUT->send($plugin['task']);
85
 }
86
-// CSRF prevention
87
 else {
88
-    // don't check for valid request tokens in these actions
89
-    $request_check_whitelist = array('login'=>1, 'spell'=>1, 'spell_html'=>1);
90
-
91
-    if (!$request_check_whitelist[$RCMAIL->action]) {
92
-        // check client X-header to verify request origin
93
-        if ($OUTPUT->ajax_call) {
94
-            if (rcube_utils::request_header('X-Roundcube-Request') != $RCMAIL->get_request_token()) {
95
-                header('HTTP/1.1 403 Forbidden');
96
-                die("Invalid Request");
97
-            }
98
-        }
99
-        // check request token in POST form submissions
100
-        else if (!empty($_POST) && !$RCMAIL->check_request()) {
101
-            $OUTPUT->show_message('invalidrequest', 'error');
102
-            $OUTPUT->send($RCMAIL->task);
103
-        }
104
-
105
-        // check referer if configured
106
-        if ($RCMAIL->config->get('referer_check') && !rcube_utils::check_referer()) {
107
-            raise_error(array(
108
-                'code' => 403, 'type' => 'php',
109
-                'message' => "Referer check failed"), true, true);
110
-        }
111
-    }
112
+    // CSRF prevention
113
+    $RCMAIL->request_security_check();
114
 
115
     // check access to disabled actions
116
     $disabled_actions = (array) $RCMAIL->config->get('disabled_actions');
117
diff --git a/plugins/acl/acl.js b/plugins/acl/acl.js
118
index e59ac72..1463453 100644
119
--- a/plugins/acl/acl.js
120
+++ b/plugins/acl/acl.js
121
@@ -58,8 +58,11 @@ rcube_webmail.prototype.acl_delete = function()
122
     var users = this.acl_get_usernames();
123
 
124
     if (users && users.length && confirm(this.get_label('acl.deleteconfirm'))) {
125
-        this.http_request('settings/plugin.acl', '_act=delete&_user='+urlencode(users.join(','))
126
-            + '&_mbox='+urlencode(this.env.mailbox),
127
+        this.http_post('settings/plugin.acl', {
128
+                _act: 'delete',
129
+                _user: users.join(','),
130
+                _mbox: this.env.mailbox
131
+            },
132
             this.set_busy(true, 'acl.deleting'));
133
     }
134
 }
135
@@ -67,7 +70,7 @@ rcube_webmail.prototype.acl_delete = function()
136
 // Save ACL data
137
 rcube_webmail.prototype.acl_save = function()
138
 {
139
-    var user = $('#acluser', this.acl_form).val(), rights = '', type;
140
+    var data, type, rights = '', user = $('#acluser', this.acl_form).val();
141
 
142
     $((this.env.acl_advanced ? '#advancedrights :checkbox' : '#simplerights :checkbox'), this.acl_form).map(function() {
143
         if (this.checked)
144
@@ -88,12 +91,18 @@ rcube_webmail.prototype.acl_save = function()
145
         return;
146
     }
147
 
148
-    this.http_request('settings/plugin.acl', '_act=save'
149
-        + '&_user='+urlencode(user)
150
-        + '&_acl=' +rights
151
-        + '&_mbox='+urlencode(this.env.mailbox)
152
-        + (this.acl_id ? '&_old='+this.acl_id : ''),
153
-        this.set_busy(true, 'acl.saving'));
154
+    data = {
155
+        _act: 'save',
156
+        _user: user,
157
+        _acl: rights,
158
+        _mbox: this.env.mailbox
159
+    }
160
+
161
+    if (this.acl_id) {
162
+        data._old = this.acl_id;
163
+    }
164
+
165
+    this.http_post('settings/plugin.acl', data, this.set_busy(true, 'acl.saving'));
166
 }
167
 
168
 // Cancel/Hide form
169
diff --git a/plugins/acl/acl.php b/plugins/acl/acl.php
170
index 33bd91e..cb1a720 100644
171
--- a/plugins/acl/acl.php
172
+++ b/plugins/acl/acl.php
173
@@ -452,10 +452,10 @@ class acl extends rcube_plugin
174
      */
175
     private function action_save()
176
     {
177
-        $mbox  = trim(rcube_utils::get_input_value('_mbox', rcube_utils::INPUT_GPC, true)); // UTF7-IMAP
178
-        $user  = trim(rcube_utils::get_input_value('_user', rcube_utils::INPUT_GPC));
179
-        $acl   = trim(rcube_utils::get_input_value('_acl', rcube_utils::INPUT_GPC));
180
-        $oldid = trim(rcube_utils::get_input_value('_old', rcube_utils::INPUT_GPC));
181
+        $mbox  = trim(rcube_utils::get_input_value('_mbox', rcube_utils::INPUT_POST, true)); // UTF7-IMAP
182
+        $user  = trim(rcube_utils::get_input_value('_user', rcube_utils::INPUT_POST));
183
+        $acl   = trim(rcube_utils::get_input_value('_acl', rcube_utils::INPUT_POST));
184
+        $oldid = trim(rcube_utils::get_input_value('_old', rcube_utils::INPUT_POST));
185
 
186
         $acl    = array_intersect(str_split($acl), $this->rights_supported());
187
         $users  = $oldid ? array($user) : explode(',', $user);
188
@@ -508,8 +508,8 @@ class acl extends rcube_plugin
189
      */
190
     private function action_delete()
191
     {
192
-        $mbox = trim(rcube_utils::get_input_value('_mbox', rcube_utils::INPUT_GPC, true)); //UTF7-IMAP
193
-        $user = trim(rcube_utils::get_input_value('_user', rcube_utils::INPUT_GPC));
194
+        $mbox = trim(rcube_utils::get_input_value('_mbox', rcube_utils::INPUT_POST, true)); //UTF7-IMAP
195
+        $user = trim(rcube_utils::get_input_value('_user', rcube_utils::INPUT_POST));
196
 
197
         $user = explode(',', $user);
198
 
199
diff --git a/plugins/managesieve/lib/Roundcube/rcube_sieve_engine.php b/plugins/managesieve/lib/Roundcube/rcube_sieve_engine.php
200
index 59e116f..8fba018 100644
201
--- a/plugins/managesieve/lib/Roundcube/rcube_sieve_engine.php
202
+++ b/plugins/managesieve/lib/Roundcube/rcube_sieve_engine.php
203
@@ -312,7 +312,7 @@ class rcube_sieve_engine
204
                 }
205
             }
206
             else if ($action == 'setact' && !$error) {
207
-                $script_name = rcube_utils::get_input_value('_set', rcube_utils::INPUT_GPC, true);
208
+                $script_name = rcube_utils::get_input_value('_set', rcube_utils::INPUT_POST, true);
209
                 $result = $this->activate_script($script_name);
210
                 $kep14  = $this->rc->config->get('managesieve_kolab_master');
211
 
212
@@ -326,7 +326,7 @@ class rcube_sieve_engine
213
                 }
214
             }
215
             else if ($action == 'deact' && !$error) {
216
-                $script_name = rcube_utils::get_input_value('_set', rcube_utils::INPUT_GPC, true);
217
+                $script_name = rcube_utils::get_input_value('_set', rcube_utils::INPUT_POST, true);
218
                 $result = $this->deactivate_script($script_name);
219
 
220
                 if ($result === true) {
221
@@ -339,7 +339,7 @@ class rcube_sieve_engine
222
                 }
223
             }
224
             else if ($action == 'setdel' && !$error) {
225
-                $script_name = rcube_utils::get_input_value('_set', rcube_utils::INPUT_GPC, true);
226
+                $script_name = rcube_utils::get_input_value('_set', rcube_utils::INPUT_POST, true);
227
                 $result = $this->remove_script($script_name);
228
 
229
                 if ($result === true) {
230
@@ -382,14 +382,14 @@ class rcube_sieve_engine
231
                 $this->rc->output->command('managesieve_updatelist', 'list', array('list' => $result));
232
             }
233
             else if ($action == 'ruleadd') {
234
-                $rid = rcube_utils::get_input_value('_rid', rcube_utils::INPUT_GPC);
235
+                $rid = rcube_utils::get_input_value('_rid', rcube_utils::INPUT_POST);
236
                 $id = $this->genid();
237
                 $content = $this->rule_div($fid, $id, false);
238
 
239
                 $this->rc->output->command('managesieve_rulefill', $content, $id, $rid);
240
             }
241
             else if ($action == 'actionadd') {
242
-                $aid = rcube_utils::get_input_value('_aid', rcube_utils::INPUT_GPC);
243
+                $aid = rcube_utils::get_input_value('_aid', rcube_utils::INPUT_POST);
244
                 $id = $this->genid();
245
                 $content = $this->action_div($fid, $id, false);
246
 
247
diff --git a/program/include/rcmail.php b/program/include/rcmail.php
248
index ceb369a..fba537e 100644
249
--- a/program/include/rcmail.php
250
+++ b/program/include/rcmail.php
251
@@ -753,47 +753,14 @@ class rcmail extends rcube
252
     }
253
 
254
     /**
255
-     * Generate a unique token to be used in a form request
256
-     *
257
-     * @return string The request token
258
-     */
259
-    public function get_request_token()
260
-    {
261
-        $sess_id = $_COOKIE[ini_get('session.name')];
262
-
263
-        if (!$sess_id) {
264
-            $sess_id = session_id();
265
-        }
266
-
267
-        $plugin = $this->plugins->exec_hook('request_token', array(
268
-            'value' => md5('RT' . $this->get_user_id() . $this->config->get('des_key') . $sess_id)));
269
-
270
-        return $plugin['value'];
271
-    }
272
-
273
-    /**
274
-     * Check if the current request contains a valid token
275
-     *
276
-     * @param int Request method
277
-     *
278
-     * @return boolean True if request token is valid false if not
279
-     */
280
-    public function check_request($mode = rcube_utils::INPUT_POST)
281
-    {
282
-        $token   = rcube_utils::get_input_value('_token', $mode);
283
-        $sess_id = $_COOKIE[ini_get('session.name')];
284
-
285
-        return !empty($sess_id) && $token == $this->get_request_token();
286
-    }
287
-
288
-    /**
289
      * Build a valid URL to this instance of Roundcube
290
      *
291
      * @param mixed Either a string with the action or url parameters as key-value pairs
292
+     * @param bool  Return absolute URL to secure location
293
      *
294
      * @return string Valid application URL
295
      */
296
-    public function url($p)
297
+    public function url($p, $secure = false)
298
     {
299
         if (!is_array($p)) {
300
             if (strpos($p, 'http') === 0) {
301
@@ -807,8 +774,9 @@ class rcmail extends rcube
302
         $p['_task'] = $task;
303
         unset($p['task']);
304
 
305
-        $url  = './' . $this->filename;
306
-        $delm = '?';
307
+        $prefix = './';
308
+        $url    = $this->filename;
309
+        $delm   = '?';
310
 
311
         foreach (array_reverse($p) as $key => $val) {
312
             if ($val !== '' && $val !== null) {
313
@@ -818,7 +786,22 @@ class rcmail extends rcube
314
             }
315
         }
316
 
317
-        return $url;
318
+        // generate absolute URL if URL token exists
319
+        if ($secure && ($token = $this->get_secure_url_token(true))) {
320
+            $uri = preg_replace('/[?#&].*$/', '', $_SERVER['REQUEST_URI']);
321
+            if (strpos($uri, '/' . $token) === false) {
322
+                $uri  = rtrim($uri, '/');
323
+                // remove old token
324
+                $uri  = preg_replace('/\/[a-f0-9]{' . strlen($token) . '}$/', '', $uri);
325
+                // add new token
326
+                $uri .= '/' . $token . '/';
327
+
328
+                $prefix = (rcube_utils::https_check() ? 'https' : 'http') . '://'
329
+                    . $_SERVER['SERVER_NAME'] . $uri;
330
+            }
331
+        }
332
+
333
+        return $prefix . $url;
334
     }
335
 
336
     /**
337
@@ -853,6 +836,36 @@ class rcmail extends rcube
338
     }
339
 
340
     /**
341
+     * CSRF attack prevention code
342
+     *
343
+     * @param int Request mode
344
+     */
345
+    public function request_security_check($mode = rcube_utils::INPUT_POST)
346
+    {
347
+        // don't check for valid request tokens in these actions
348
+        // @TODO: get rid of this
349
+        $request_check_whitelist = array('spell'=>1, 'spell_html'=>1);
350
+
351
+        if ($request_check_whitelist[$this->action]) {
352
+            return;
353
+        }
354
+
355
+        // check request token
356
+        if (!$this->check_request($mode)) {
357
+            self::raise_error(array(
358
+                'code' => 403, 'type' => 'php',
359
+                'message' => "Request security check failed"), false, true);
360
+        }
361
+
362
+        // check referer if configured
363
+        if ($this->config->get('referer_check') && !rcube_utils::check_referer()) {
364
+            self::raise_error(array(
365
+                'code' => 403, 'type' => 'php',
366
+                'message' => "Referer check failed"), true, true);
367
+        }
368
+    }
369
+
370
+    /**
371
      * Registers action aliases for current task
372
      *
373
      * @param array $map Alias-to-filename hash array
374
diff --git a/program/include/rcmail_output_html.php b/program/include/rcmail_output_html.php
375
index a848246..2d8853f 100644
376
--- a/program/include/rcmail_output_html.php
377
+++ b/program/include/rcmail_output_html.php
378
@@ -45,6 +45,7 @@ class rcmail_output_html extends rcmail_output
379
     protected $footer = '';
380
     protected $body = '';
381
     protected $base_path = '';
382
+    protected $assets_path;
383
     protected $devel_mode = false;
384
 
385
     // deprecated names of templates used before 0.5
386
@@ -80,6 +81,8 @@ class rcmail_output_html extends rcmail_output
387
         $this->set_skin($skin);
388
         $this->set_env('skin', $skin);
389
 
390
+        $this->set_assets_path($this->config->get('assets_path'));
391
+
392
         if (!empty($_REQUEST['_extwin']))
393
             $this->set_env('extwin', 1);
394
         if ($this->framed || $framed)
395
@@ -145,6 +148,50 @@ EOF;
396
     }
397
 
398
     /**
399
+     * Parse and set assets path
400
+     *
401
+     * @param string Assets path (relative or absolute URL)
402
+     */
403
+    public function set_assets_path($path)
404
+    {
405
+        $path = trim($path, '/') . '/';
406
+
407
+        if (empty($path)) {
408
+            return;
409
+        }
410
+
411
+        // convert to absolute URL
412
+        if (!preg_match('|^https?://|', $path)) {
413
+            $base = preg_replace('/[?#&].*$/', '', $_SERVER['REQUEST_URI']);
414
+            $base = rtrim($base, '/');
415
+
416
+            // remove url token if exists
417
+            if ($len = intval($this->config->get('use_secure_urls'))) {
418
+                $_base  = explode('/', $base);
419
+                $length = $len > 1 ? $len : 16; // as in rcube::get_secure_url_token()
420
+
421
+                // we can't use real token here because it
422
+                // does not exists in unauthenticated state,
423
+                // hope this will not produce false-positive matches
424
+                foreach ($_base as $idx => $token) {
425
+                    if (preg_match('/^[a-f0-9]{' . $length . '}$/', $token)) {
426
+                        unset($_base[$idx]);
427
+                        break;
428
+                    }
429
+                }
430
+
431
+                $base = implode('/', $_base);
432
+            }
433
+
434
+            $path = (rcube_utils::https_check() ? 'https' : 'http') . '://'
435
+                . $_SERVER['SERVER_NAME'] . $base . '/' . $path;
436
+        }
437
+
438
+        $this->assets_path = $path;
439
+        $this->set_env('assets_path', $path);
440
+    }
441
+
442
+    /**
443
      * Getter for the current page title
444
      *
445
      * @return string The page title
446
@@ -367,14 +414,15 @@ EOF;
447
     /**
448
      * Redirect to a certain url
449
      *
450
-     * @param mixed $p     Either a string with the action or url parameters as key-value pairs
451
-     * @param int   $delay Delay in seconds
452
+     * @param mixed $p      Either a string with the action or url parameters as key-value pairs
453
+     * @param int   $delay  Delay in seconds
454
+     * @param bool  $secure Redirect to secure location (see rcmail::url())
455
      */
456
-    public function redirect($p = array(), $delay = 1)
457
+    public function redirect($p = array(), $delay = 1, $secure = false)
458
     {
459
         if ($this->env['extwin'])
460
             $p['extwin'] = 1;
461
-        $location = $this->app->url($p);
462
+        $location = $this->app->url($p, $secure);
463
         header('Location: ' . $location);
464
         exit;
465
     }
466
@@ -659,6 +707,24 @@ EOF;
467
         exit;
468
     }
469
 
470
+    /**
471
+     * Modify path by adding URL prefix if configured
472
+     */
473
+    public function asset_url($path)
474
+    {
475
+        // iframe content can't be in a different domain
476
+        // @TODO: check if assests are on a different domain
477
+        if (!$this->assets_path || $path == $this->env['blankpage']) {
478
+            return $path;
479
+        }
480
+
481
+        if ($path[0] == '.' && $path[1] == '/') {
482
+            $path = substr($path, 2);
483
+        }
484
+
485
+        return $this->assets_path . $path;
486
+    }
487
+
488
 
489
     /*****  Template parsing methods  *****/
490
 
491
@@ -715,6 +781,8 @@ EOF;
492
             $file = $this->file_mod($file);
493
         }
494
 
495
+        $file = $this->asset_url($file);
496
+
497
         return $matches[1] . '=' . $matches[2] . $file . $matches[4];
498
     }
499
 
500
@@ -1329,6 +1397,7 @@ EOF;
501
     {
502
         if (!preg_match('|^https?://|i', $file) && $file[0] != '/') {
503
             $file = $this->file_mod($this->scripts_path . $file);
504
+            $file = $this->asset_url($file);
505
         }
506
 
507
         if (!is_array($this->script_files[$position])) {
508
@@ -1541,7 +1610,7 @@ EOF;
509
         }
510
 
511
         $attrib['name'] = $attrib['id'];
512
-        $attrib['src'] = $attrib['src'] ? $this->abs_url($attrib['src'], true) : 'program/resources/blank.gif';
513
+        $attrib['src']  = $this->asset_url($attrib['src'] ? $this->abs_url($attrib['src'], true) : 'program/resources/blank.gif');
514
 
515
         // register as 'contentframe' object
516
         if ($is_contentframe || $attrib['contentframe']) {
517
@@ -1758,9 +1827,11 @@ EOF;
518
     {
519
         $images = preg_split('/[\s\t\n,]+/', $attrib['images'], -1, PREG_SPLIT_NO_EMPTY);
520
         $images = array_map(array($this, 'abs_url'), $images);
521
+        $images = array_map(array($this, 'asset_url'), $images);
522
 
523
-        if (empty($images) || $this->app->task == 'logout')
524
+        if (empty($images) || $_REQUEST['_task'] == 'logout') {
525
             return;
526
+        }
527
 
528
         $this->add_script('var images = ' . self::json_serialize($images) .';
529
             for (var i=0; i<images.length; i++) {
530
diff --git a/program/include/rcmail_output_json.php b/program/include/rcmail_output_json.php
531
index fa35824..91262ac 100644
532
--- a/program/include/rcmail_output_json.php
533
+++ b/program/include/rcmail_output_json.php
534
@@ -181,6 +181,11 @@ class rcmail_output_json extends rcmail_output
535
      */
536
     public function raise_error($code, $message)
537
     {
538
+        if ($code == 403) {
539
+            header('HTTP/1.1 403 Forbidden');
540
+            die("Invalid Request");
541
+        }
542
+
543
         $this->show_message("Application Error ($code): $message", 'error');
544
         $this->remote_response();
545
         exit;
546
diff --git a/program/js/app.js b/program/js/app.js
547
index 5a87347..0f51331 100644
548
--- a/program/js/app.js
549
+++ b/program/js/app.js
550
@@ -1404,8 +1404,10 @@ function rcube_webmail()
551
 
552
     if (task == 'mail')
553
       url += '&_mbox=INBOX';
554
-    else if (task == 'logout' && !this.env.server_error)
555
+    else if (task == 'logout' && !this.env.server_error) {
556
+      url += '&_token=' + this.env.request_token;
557
       this.clear_compose_data();
558
+    }
559
 
560
     this.redirect(url);
561
   };
562
@@ -1415,7 +1417,10 @@ function rcube_webmail()
563
     if (!url)
564
       url = this.env.comm_path;
565
 
566
-    return url.replace(/_task=[a-z0-9_-]+/i, '_task='+task);
567
+    if (url.match(/[?&]_task=[a-zA-Z0-9_-]+/))
568
+        return url.replace(/_task=[a-zA-Z0-9_-]+/, '_task=' + task);
569
+    else
570
+        return url.replace(/\?.*$/, '') + '?_task=' + task;
571
   };
572
 
573
   this.reload = function(delay)
574
diff --git a/program/js/editor.js b/program/js/editor.js
575
index b2e96d1..74a3134 100644
576
--- a/program/js/editor.js
577
+++ b/program/js/editor.js
578
@@ -36,11 +36,12 @@
579
 function rcube_text_editor(config, id)
580
 {
581
   var ref = this,
582
+    abs_url = location.href.replace(/[?#].*$/, '').replace(/\/$/, ''),
583
     conf = {
584
       selector: '#' + ($('#' + id).is('.mce_editor') ? id : 'fake-editor-id'),
585
       theme: 'modern',
586
       language: config.lang,
587
-      content_css: 'program/js/tinymce/roundcube/content.css?v1',
588
+      content_css: (rcmail.env.assets_path || '') + 'program/js/tinymce/roundcube/content.css?v1',
589
       menubar: false,
590
       statusbar: false,
591
       toolbar_items_size: 'small',
592
@@ -82,7 +83,7 @@ function rcube_text_editor(config, id)
593
       toolbar: 'bold italic underline | alignleft aligncenter alignright alignjustify'
594
         + ' | bullist numlist outdent indent ltr rtl blockquote | forecolor backcolor | fontselect fontsizeselect'
595
         + ' | link unlink table | emoticons charmap image media | code searchreplace undo redo',
596
-      spellchecker_rpc_url: '../../../../../?_task=utils&_action=spell_html&_remote=1',
597
+      spellchecker_rpc_url: abs_url + '/?_task=utils&_action=spell_html&_remote=1',
598
       spellchecker_language: rcmail.env.spell_lang,
599
       accessibility_focus: false,
600
       file_browser_callback: function(name, url, type, win) { ref.file_browser_callback(name, url, type); },
601
diff --git a/program/lib/Roundcube/rcube.php b/program/lib/Roundcube/rcube.php
602
index eedc46c..e557138 100644
603
--- a/program/lib/Roundcube/rcube.php
604
+++ b/program/lib/Roundcube/rcube.php
605
@@ -28,9 +28,15 @@
606
  */
607
 class rcube
608
 {
609
-    const INIT_WITH_DB = 1;
610
+    // Init options
611
+    const INIT_WITH_DB      = 1;
612
     const INIT_WITH_PLUGINS = 2;
613
 
614
+    // Request status
615
+    const REQUEST_VALID       = 0;
616
+    const REQUEST_ERROR_URL   = 1;
617
+    const REQUEST_ERROR_TOKEN = 2;
618
+
619
     /**
620
      * Singleton instace of rcube
621
      *
622
@@ -101,6 +107,12 @@ class rcube
623
      */
624
     public $user;
625
 
626
+    /**
627
+     * Request status
628
+     *
629
+     * @var int
630
+     */
631
+    public $request_status = 0;
632
 
633
     /* private/protected vars */
634
     protected $texts;
635
@@ -976,6 +988,104 @@ class rcube
636
 
637
 
638
     /**
639
+     * Returns session token for secure URLs
640
+     *
641
+     * @param bool $generate Generate token if not exists in session yet
642
+     *
643
+     * @return string|bool Token string, False when disabled
644
+     */
645
+    public function get_secure_url_token($generate = false)
646
+    {
647
+        if ($len = $this->config->get('use_secure_urls')) {
648
+            if (empty($_SESSION['secure_token']) && $generate) {
649
+                // generate x characters long token
650
+                $length = $len > 1 ? $len : 16;
651
+                $token  = openssl_random_pseudo_bytes($length / 2);
652
+                $token  = bin2hex($token);
653
+
654
+                $plugin = $this->plugins->exec_hook('secure_token',
655
+                    array('value' => $token, 'length' => $length));
656
+
657
+                $_SESSION['secure_token'] = $plugin['value'];
658
+            }
659
+
660
+            return $_SESSION['secure_token'];
661
+        }
662
+
663
+        return false;
664
+    }
665
+
666
+
667
+    /**
668
+     * Generate a unique token to be used in a form request
669
+     *
670
+     * @return string The request token
671
+     */
672
+    public function get_request_token()
673
+    {
674
+        $sess_id = $_COOKIE[ini_get('session.name')];
675
+        if (!$sess_id) {
676
+            $sess_id = session_id();
677
+        }
678
+
679
+        $plugin = $this->plugins->exec_hook('request_token', array(
680
+            'value' => md5('RT' . $this->get_user_id() . $this->config->get('des_key') . $sess_id)));
681
+
682
+        return $plugin['value'];
683
+    }
684
+
685
+
686
+    /**
687
+     * Check if the current request contains a valid token.
688
+     * Empty requests aren't checked until use_secure_urls is set.
689
+     *
690
+     * @param int Request method
691
+     *
692
+     * @return boolean True if request token is valid false if not
693
+     */
694
+    public function check_request($mode = rcube_utils::INPUT_POST)
695
+    {
696
+        // check secure token in URL if enabled
697
+        if ($token = $this->get_secure_url_token()) {
698
+            foreach (explode('/', preg_replace('/[?#&].*$/', '', $_SERVER['REQUEST_URI'])) as $tok) {
699
+                if ($tok == $token) {
700
+                    return true;
701
+                }
702
+            }
703
+
704
+            $this->request_status = self::REQUEST_ERROR_URL;
705
+
706
+            return false;
707
+        }
708
+
709
+        $sess_tok = $this->get_request_token();
710
+
711
+        // ajax requests
712
+        if (rcube_utils::request_header('X-Roundcube-Request') == $sess_tok) {
713
+            return true;
714
+        }
715
+
716
+        // skip empty requests
717
+        if (($mode == rcube_utils::INPUT_POST && empty($_POST))
718
+            || ($mode == rcube_utils::INPUT_GET && empty($_GET))
719
+        ) {
720
+            return true;
721
+        }
722
+
723
+        // default method of securing requests
724
+        $token   = rcube_utils::get_input_value('_token', $mode);
725
+        $sess_id = $_COOKIE[ini_get('session.name')];
726
+
727
+        if (empty($sess_id) || $token != $sess_tok) {
728
+            $this->request_status = self::REQUEST_ERROR_TOKEN;
729
+            return false;
730
+        }
731
+
732
+        return true;
733
+    }
734
+
735
+
736
+    /**
737
      * Build a valid URL to this instance of Roundcube
738
      *
739
      * @param mixed Either a string with the action or url parameters as key-value pairs
740
@@ -1263,7 +1373,7 @@ class rcube
741
 
742
         $cli = php_sapi_name() == 'cli';
743
 
744
-        if (($log || $terminate) && !$cli && $arg['message']) {
745
+        if ($log && !$cli && $arg['message']) {
746
             $arg['fatal'] = $terminate;
747
             self::log_bug($arg);
748
         }
749
diff --git a/program/steps/addressbook/delete.inc b/program/steps/addressbook/delete.inc
750
index 3d57d70..727b597 100644
751
--- a/program/steps/addressbook/delete.inc
752
+++ b/program/steps/addressbook/delete.inc
753
@@ -20,10 +20,11 @@
754
 */
755
 
756
 // process ajax requests only
757
-if (!$OUTPUT->ajax_call)
758
+if (!$OUTPUT->ajax_call) {
759
     return;
760
+}
761
 
762
-$cids   = rcmail_get_cids();
763
+$cids   = rcmail_get_cids(null, rcube_utils::INPUT_POST);
764
 $delcnt = 0;
765
 
766
 // remove previous deletes
767
diff --git a/program/steps/addressbook/func.inc b/program/steps/addressbook/func.inc
768
index 2c22d5a..84dff35 100644
769
--- a/program/steps/addressbook/func.inc
770
+++ b/program/steps/addressbook/func.inc
771
@@ -896,13 +896,13 @@ function rcmail_search_update($return = false)
772
  *
773
  * @return array List of contact IDs per-source
774
  */
775
-function rcmail_get_cids($filter = null)
776
+function rcmail_get_cids($filter = null, $request_type = rcube_utils::INPUT_GPC)
777
 {
778
     // contact ID (or comma-separated list of IDs) is provided in two
779
     // forms. If _source is an empty string then the ID is a string
780
     // containing contact ID and source name in form: <ID>-<SOURCE>
781
 
782
-    $cid    = rcube_utils::get_input_value('_cid', rcube_utils::INPUT_GPC);
783
+    $cid    = rcube_utils::get_input_value('_cid', $request_type);
784
     $source = (string) rcube_utils::get_input_value('_source', rcube_utils::INPUT_GPC);
785
 
786
     if (is_array($cid)) {
787
diff --git a/program/steps/utils/error.inc b/program/steps/utils/error.inc
788
index 2a3a9a6..b7ccf92 100644
789
--- a/program/steps/utils/error.inc
790
+++ b/program/steps/utils/error.inc
791
@@ -50,9 +50,17 @@ else if ($ERROR_CODE == 401) {
792
 
793
 // forbidden due to request check
794
 else if ($ERROR_CODE == 403) {
795
+    if ($_SERVER['REQUEST_METHOD'] == 'GET' && $rcmail->request_status == rcube::REQUEST_ERROR_URL) {
796
+        parse_str($_SERVER['QUERY_STRING'], $url);
797
+        $url = $rcmail->url($url, true);
798
+        $add = "<br /><a href=\"$url\">Click here to try again.<a/>";
799
+    }
800
+    else {
801
+        $add = "Please contact your server-administrator.";
802
+    }
803
+
804
     $__error_title = "REQUEST CHECK FAILED";
805
-    $__error_text  = "Access to this service was denied due to failing security checks!<br />\n"
806
-        . "Please contact your server-administrator.";
807
+    $__error_text  = "Access to this service was denied due to failing security checks!<br />\n$add";
808
 }
809
 
810
 // failed request (wrong step in URL)
811
812