source: github/program/include/rcube_session.php @ 784a425

HEADcourier-fixdev-browser-capabilitiespdorelease-0.6release-0.7release-0.8
Last change on this file since 784a425 was 784a425, checked in by thomascube <thomas@…>, 2 years ago

protect login form submission from CSRF using a request token

  • Property mode set to 100644
File size: 11.4 KB
Line 
1<?php
2
3/*
4 +-----------------------------------------------------------------------+
5 | program/include/rcube_session.php                                     |
6 |                                                                       |
7 | This file is part of the Roundcube Webmail client                     |
8 | Copyright (C) 2005-2011, The Roundcube Dev Team                       |
9 | Licensed under the GNU GPL                                            |
10 |                                                                       |
11 | PURPOSE:                                                              |
12 |   Provide database supported session management                       |
13 |                                                                       |
14 +-----------------------------------------------------------------------+
15 | Author: Thomas Bruederli <roundcube@gmail.com>                        |
16 | Author: Aleksander Machniak <alec@alec.pl>                            |
17 +-----------------------------------------------------------------------+
18
19 $Id: session.inc 2932 2009-09-07 12:51:21Z alec $
20
21*/
22
23/**
24 * Class to provide database supported session storage
25 *
26 * @package    Core
27 * @author     Thomas Bruederli <roundcube@gmail.com>
28 * @author     Aleksander Machniak <alec@alec.pl>
29 */
30class rcube_session
31{
32  private $db;
33  private $ip;
34  private $start;
35  private $changed;
36  private $unsets = array();
37  private $gc_handlers = array();
38  private $cookiename = 'roundcube_sessauth';
39  private $vars = false;
40  private $key;
41  private $now;
42  private $prev;
43  private $secret = '';
44  private $ip_check = false;
45  private $keep_alive = 0;
46
47  /**
48   * Default constructor
49   */
50  public function __construct($db, $lifetime=60)
51  {
52    $this->db = $db;
53    $this->start = microtime(true);
54    $this->ip = $_SERVER['REMOTE_ADDR'];
55
56    $this->set_lifetime($lifetime);
57
58    // set custom functions for PHP session management
59    session_set_save_handler(
60      array($this, 'open'),
61      array($this, 'close'),
62      array($this, 'read'),
63      array($this, 'write'),
64      array($this, 'destroy'),
65      array($this, 'gc'));
66  }
67
68
69  public function open($save_path, $session_name)
70  {
71    return true;
72  }
73
74
75  public function close()
76  {
77    return true;
78  }
79
80
81  // read session data
82  public function read($key)
83  {
84    $sql_result = $this->db->query(
85      sprintf("SELECT vars, ip, %s AS changed FROM %s WHERE sess_id = ?",
86        $this->db->unixtimestamp('changed'), get_table_name('session')),
87      $key);
88
89    if ($sql_arr = $this->db->fetch_assoc($sql_result)) {
90      $this->changed = $sql_arr['changed'];
91      $this->ip      = $sql_arr['ip'];
92      $this->vars    = base64_decode($sql_arr['vars']);
93      $this->key     = $key;
94
95      if (!empty($this->vars))
96        return $this->vars;
97    }
98
99    return false;
100  }
101
102
103  /**
104   * Save session data.
105   * handler for session_read()
106   *
107   * @param string Session ID
108   * @param string Serialized session vars
109   * @return boolean True on success
110   */
111  public function write($key, $vars)
112  {
113    $ts = microtime(true);
114    $now = $this->db->fromunixtime((int)$ts);
115
116    // use internal data from read() for fast requests (up to 0.5 sec.)
117    if ($key == $this->key && $ts - $this->start < 0.5) {
118      $oldvars = $this->vars;
119    } else { // else read data again from DB
120      $oldvars = $this->read($key);
121    }
122
123    if ($oldvars !== false) {
124      $a_oldvars = $this->unserialize($oldvars);
125      if (is_array($a_oldvars)) {
126        foreach ((array)$this->unsets as $k)
127          unset($a_oldvars[$k]);
128
129        $newvars = $this->serialize(array_merge(
130          (array)$a_oldvars, (array)$this->unserialize($vars)));
131      }
132      else
133        $newvars = $vars;
134
135      if ($newvars !== $oldvars) {
136        $this->db->query(
137          sprintf("UPDATE %s SET vars=?, changed=%s WHERE sess_id=?",
138            get_table_name('session'), $now),
139          base64_encode($newvars), $key);
140      }
141      else if ($ts - $this->changed > $this->lifetime / 2) {
142        $this->db->query("UPDATE ".get_table_name('session')." SET changed=$now WHERE sess_id=?", $key);
143      }
144    }
145    else {
146      $this->db->query(
147        sprintf("INSERT INTO %s (sess_id, vars, ip, created, changed) ".
148          "VALUES (?, ?, ?, %s, %s)",
149          get_table_name('session'), $now, $now),
150        $key, base64_encode($vars), (string)$this->ip);
151    }
152
153    $this->unsets = array();
154    return true;
155  }
156
157
158  /**
159   * Handler for session_destroy()
160   *
161   * @param string Session ID
162   * @return boolean True on success
163   */
164  public function destroy($key)
165  {
166    $this->db->query(
167      sprintf("DELETE FROM %s WHERE sess_id = ?", get_table_name('session')),
168      $key);
169
170    return true;
171  }
172
173
174  /**
175   * Garbage collecting function
176   *
177   * @param string Session lifetime in seconds
178   * @return boolean True on success
179   */
180  public function gc($maxlifetime)
181  {
182    // just delete all expired sessions
183    $this->db->query(
184      sprintf("DELETE FROM %s WHERE changed < %s",
185        get_table_name('session'), $this->db->fromunixtime(time() - $maxlifetime)));
186
187    foreach ($this->gc_handlers as $fct)
188      $fct();
189
190    return true;
191  }
192
193
194  /**
195   * Register additional garbage collector functions
196   *
197   * @param mixed Callback function
198   */
199  public function register_gc_handler($func_name)
200  {
201    if ($func_name && !in_array($func_name, $this->gc_handlers))
202      $this->gc_handlers[] = $func_name;
203  }
204
205
206  /**
207   * Generate and set new session id
208   */
209  public function regenerate_id()
210  {
211    // delete old session record
212    $this->destroy(session_id());
213    $this->vars = false;
214
215    $randval = '0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ';
216
217    for ($random = '', $i=1; $i <= 32; $i++) {
218      $random .= substr($randval, mt_rand(0,(strlen($randval) - 1)), 1);
219    }
220
221    // use md5 value for id
222    $this->key = md5($random);
223    session_id($this->key);
224
225    $cookie   = session_get_cookie_params();
226    $lifetime = $cookie['lifetime'] ? time() + $cookie['lifetime'] : 0;
227
228    rcmail::setcookie(session_name(), $this->key, $lifetime);
229
230    return true;
231  }
232
233
234  /**
235   * Unset a session variable
236   *
237   * @param string Varibale name
238   * @return boolean True on success
239   */
240  public function remove($var=null)
241  {
242    if (empty($var))
243      return $this->destroy(session_id());
244
245    $this->unsets[] = $var;
246    unset($_SESSION[$var]);
247
248    return true;
249  }
250 
251  /**
252   * Kill this session
253   */
254  public function kill()
255  {
256    $this->vars = false;
257    $this->destroy(session_id());
258    rcmail::setcookie($this->cookiename, '-del-', time() - 60);
259  }
260
261
262  /**
263   * Serialize session data
264   */
265  private function serialize($vars)
266  {
267    $data = '';
268    if (is_array($vars))
269      foreach ($vars as $var=>$value)
270        $data .= $var.'|'.serialize($value);
271    else
272      $data = 'b:0;';
273    return $data;
274  }
275
276
277  /**
278   * Unserialize session data
279   * http://www.php.net/manual/en/function.session-decode.php#56106
280   */
281  private function unserialize($str)
282  {
283    $str = (string)$str;
284    $endptr = strlen($str);
285    $p = 0;
286
287    $serialized = '';
288    $items = 0;
289    $level = 0;
290
291    while ($p < $endptr) {
292      $q = $p;
293      while ($str[$q] != '|')
294        if (++$q >= $endptr) break 2;
295
296      if ($str[$p] == '!') {
297        $p++;
298        $has_value = false;
299      } else {
300        $has_value = true;
301      }
302
303      $name = substr($str, $p, $q - $p);
304      $q++;
305
306      $serialized .= 's:' . strlen($name) . ':"' . $name . '";';
307
308      if ($has_value) {
309        for (;;) {
310          $p = $q;
311          switch (strtolower($str[$q])) {
312            case 'n': /* null */
313            case 'b': /* boolean */
314            case 'i': /* integer */
315            case 'd': /* decimal */
316              do $q++;
317              while ( ($q < $endptr) && ($str[$q] != ';') );
318              $q++;
319              $serialized .= substr($str, $p, $q - $p);
320              if ($level == 0) break 2;
321              break;
322            case 'r': /* reference  */
323              $q+= 2;
324              for ($id = ''; ($q < $endptr) && ($str[$q] != ';'); $q++) $id .= $str[$q];
325              $q++;
326              $serialized .= 'R:' . ($id + 1) . ';'; /* increment pointer because of outer array */
327              if ($level == 0) break 2;
328              break;
329            case 's': /* string */
330              $q+=2;
331              for ($length=''; ($q < $endptr) && ($str[$q] != ':'); $q++) $length .= $str[$q];
332              $q+=2;
333              $q+= (int)$length + 2;
334              $serialized .= substr($str, $p, $q - $p);
335              if ($level == 0) break 2;
336              break;
337            case 'a': /* array */
338            case 'o': /* object */
339              do $q++;
340              while ( ($q < $endptr) && ($str[$q] != '{') );
341              $q++;
342              $level++;
343              $serialized .= substr($str, $p, $q - $p);
344              break;
345            case '}': /* end of array|object */
346              $q++;
347              $serialized .= substr($str, $p, $q - $p);
348              if (--$level == 0) break 2;
349              break;
350            default:
351              return false;
352          }
353        }
354      } else {
355        $serialized .= 'N;';
356        $q += 2;
357      }
358      $items++;
359      $p = $q;
360    }
361
362    return unserialize( 'a:' . $items . ':{' . $serialized . '}' );
363  }
364
365
366  /**
367   * Setter for session lifetime
368   */
369  public function set_lifetime($lifetime)
370  {
371      $this->lifetime = max(120, $lifetime);
372
373      // valid time range is now - 1/2 lifetime to now + 1/2 lifetime
374      $now = time();
375      $this->now = $now - ($now % ($this->lifetime / 2));
376      $this->prev = $this->now - ($this->lifetime / 2);
377  }
378
379  /**
380   * Setter for keep_alive interval
381   */
382  public function set_keep_alive($keep_alive)
383  {
384    $this->keep_alive = $keep_alive;
385   
386    if ($this->lifetime < $keep_alive)
387        $this->set_lifetime($keep_alive + 30);
388  }
389
390  /**
391   * Getter for keep_alive interval
392   */
393  public function get_keep_alive()
394  {
395    return $this->keep_alive;
396  }
397
398  /**
399   * Getter for remote IP saved with this session
400   */
401  public function get_ip()
402  {
403    return $this->ip;
404  }
405 
406  /**
407   * Setter for cookie encryption secret
408   */
409  function set_secret($secret)
410  {
411    $this->secret = $secret;
412  }
413
414
415  /**
416   * Enable/disable IP check
417   */
418  function set_ip_check($check)
419  {
420    $this->ip_check = $check;
421  }
422 
423  /**
424   * Setter for the cookie name used for session cookie
425   */
426  function set_cookiename($cookiename)
427  {
428    if ($cookiename)
429      $this->cookiename = $cookiename;
430  }
431
432
433  /**
434   * Check session authentication cookie
435   *
436   * @return boolean True if valid, False if not
437   */
438  function check_auth()
439  {
440    $this->cookie = $_COOKIE[$this->cookiename];
441    $result = $this->ip_check ? $_SERVER['REMOTE_ADDR'] == $this->ip : true;
442
443    if ($result && $this->_mkcookie($this->now) != $this->cookie) {
444      // Check if using id from previous time slot
445      if ($this->_mkcookie($this->prev) == $this->cookie)
446        $this->set_auth_cookie();
447      else
448        $result = false;
449    }
450
451    return $result;
452  }
453
454
455  /**
456   * Set session authentication cookie
457   */
458  function set_auth_cookie()
459  {
460    $this->cookie = $this->_mkcookie($this->now);
461    rcmail::setcookie($this->cookiename, $this->cookie, 0);
462    $_COOKIE[$this->cookiename] = $this->cookie;
463  }
464
465
466  /**
467   * Create session cookie from session data
468   *
469   * @param int Time slot to use
470   */
471  function _mkcookie($timeslot)
472  {
473    $auth_string = "$this->key,$this->secret,$timeslot";
474    return "S" . (function_exists('sha1') ? sha1($auth_string) : md5($auth_string));
475  }
476
477}
Note: See TracBrowser for help on using the repository browser.