From ead753dc3d3d141900782c2a42d946077859b073 Mon Sep 17 00:00:00 2001
From: Graham Cole <chckens@sucs.org>
Date: Wed, 16 Jan 2008 20:37:27 +0000
Subject: [PATCH] Attempt to fix some issues with the session library,
 including:

- Begin to stop it being so logout happy for ordinary users who aren't doing anything particularly sensitive on the site by keeping track of when a user was last asked for credentials
- Don't continue to use the same session identifier once a user is logged in; it's likely been sent insecurely
- Mark session cookies as "SSL only" once logged in
- Automatically bump users from HTTP to HTTPS for all requests whilst they're logged in
---
 lib/session.php | 100 ++++++++++++++++++++++++++++++++++--------------
 1 file changed, 72 insertions(+), 28 deletions(-)

diff --git a/lib/session.php b/lib/session.php
index 5e98f6a..f8bd2e9 100644
--- a/lib/session.php
+++ b/lib/session.php
@@ -8,7 +8,7 @@
 // other attributes are :
 // username   - the username they logged in with
 // fullname   - whatever full name we know for them
-// last   - unix timestamp for their previous page access
+// lastseen   - unix timestamp for their previous page access
 // data   - var/array for persistant data, commit by calling the 'save' method
 
 
@@ -22,35 +22,54 @@ public  $email_forward;		// Email forwarded?
 public  $groups  =array();	// users groups
 public	$data='';		// Var/array for session persistant data
 public	$token='';		// session identifier
-public	$last='';		// Time of last page request
-private	$timeout = 300;		// Idle timeout limit in minutes
+public  $logintime='';  // Time which user last gave us credentials
+public	$lastseen='';		// Time of last page request
+private	$timeout = 2880;	// Idle timeout limit in minutes (session deleted)
+private $secure_timeout = 30;   // Idle timeout limit in minutes (consider session less secure, require reauth for sensitive ops)
 private	$table = "session";	// session storage table (const)
 private $datahash='';		// hash of data field
 
-	// Create a new session id
+
+
+	// Create a new (insecure) session
 	private function newsession()
 	{
 		global $DB;
-		$try = 0;
 
-		do {
-			$tt=date("D M d H:i:s Y");
-			$ip = $_SERVER['REMOTE_ADDR'];
-			$token = md5("$ip$tt$try");
-			$old = $DB->GetAll("select hash from session where hash=?", array($token));
-		}while ($old);
-		$DB->Execute("insert into session (hash, time, ip) values (?,NOW(),?)", array($token, $ip));
-		setcookie("session", $token, NULL, "/");
+		$token = $this->genSessionID(); 
+		$DB->Execute("insert into {$this->table} (hash, lastseen, ip) values (?,NOW(),?)", array($token, $_SERVER['REMOTE_ADDR']));
+		setcookie("sucssite_session", $token, NULL, "/");
 		$this->token = $token;
 		return;
 	}
 
+	public function isSecure()
+	{
+		global $DB;
+		// is user coming from the IP address they were when they logged in?
+		if ($detail['ip'] != $_SERVER['REMOTE_ADDR']) {
+			return false;
+		} elseif (time() > ($self->logintime + $secure_timeout)) {
+		// has it been too long since we last asked for credentials?
+			return false;
+		}
+
+	}
+
 	// Public Object constructor
 	function __construct()
 	{
-		global $DB;
+		global $DB, $preferred_hostname, $baseurl;
 		unset($token);
 
+		// if user requests a page via HTTP and claims to be logged in, bump them to HTTPS
+		if (!isset($_SERVER['HTTPS']) && (@$_COOKIE['sucssite_loggedin'] == "true")) {
+			header("HTTP/1.0 307 Temporary redirect");
+			header("Location: https://{$preferred_hostname}{$baseurl}{$_SERVER['PATH_INFO']}"); 
+			return;
+		}
+
+
 		// The possible form elements
 		$submit = @$_POST['Login'];
 		$logout = @$_POST['Logout'];
@@ -61,7 +80,7 @@ private $datahash='';		// hash of data field
 		$this->loggedin = FALSE;
 
 		// Time out any old sessions
-		$DB->Execute("delete from {$this->table} where time < NOW() - '{$this->timeout} minutes'::reltime");
+		$DB->Execute("delete from {$this->table} where lastseen < NOW() - '{$this->timeout} minutes'::reltime");
 
 		// Log them out if they ask
 		if ($logout=="Logout") {
@@ -75,8 +94,8 @@ private $datahash='';		// hash of data field
 
 		// Check if we were handed a specific token identifier
 		// Otherwise use the value from the cookie we gave out
-		if (!isset($token) && isset($_COOKIE['session'])) 
-			$token=@$_COOKIE['session'];
+		if (!isset($token) && isset($_COOKIE['sucssite_session'])) 
+			$token=@$_COOKIE['sucssite_session'];
 
 		if (isset($token)) $this->token = $token;
 
@@ -106,7 +125,8 @@ private $datahash='';		// hash of data field
 		// Extract detail of session for pass-back
 		$detail = $oldsess[0];
 		$this->data = unserialize((string)$detail['data']);
-		$this->last = strtotime($detail['time']);
+		$this->lastseen = strtotime($detail['lastseen']);
+		$this->logintime = strtotime($detail['logintime']);
 		$this->datahash = md5(serialize($this->data));
 
 		// are we actually logged in, fill in more
@@ -117,19 +137,31 @@ private $datahash='';		// hash of data field
 				$this->loggedin = FALSE;
 				return;
 			}
-			// User is valid but they're coming from the wrong IP
-			if ($detail['ip'] != $_SERVER['REMOTE_ADDR']) {
-				$this->errormsg = "Your IP address has changed - you have been logged out";
-				$this->logout();
-				return;
-			}
 			$this->username=$detail['username'];
 			$this->fetch_detail($detail['username']);
 			$this->loggedin = TRUE;
 		}
 
 		// update time stamp
-		$DB->Execute( "update {$this->table} set time=NOW() where hash=?", array($this->token));
+		$DB->Execute( "update {$this->table} set lastseen=NOW() where hash=?", array($this->token));
+	}
+	
+	// generate a string suitable to be used as a session ID
+	private function genSessionID() 
+	{
+		global $DB;
+		$try = 0;
+
+		$tt=date("D M d H:i:s Y");
+		$ip = $_SERVER['REMOTE_ADDR'];
+		$nonce = rand(); // this should stop session IDs being (easily) guessable by someone with the algorithm
+
+		do {
+			$token = md5("$ip$tt$nonce".$try++);
+			$old = $DB->GetAll("select hash from {$this->table}  where hash=?", array($token));
+		} while ($old);
+
+		return $token;
 	}
 
 	// Public function: Store the session data away in the database
@@ -150,9 +182,10 @@ private $datahash='';		// hash of data field
 	public function logout( )
 	{
 		global $DB;
-		$DB->Execute("delete from session where hash=?", array($this->token));
+		$DB->Execute("delete from {$this->table} where hash=?", array($this->token));
 		$this->newsession();
 		$this->loggedin = FALSE;
+		setcookie("sucssite_loggedin", "false");
 	}
 
 	// Fill out any extra details we know about the user
@@ -259,7 +292,7 @@ private $datahash='';		// hash of data field
 	// Private function: process login form
 	private function session_init($user, $pass)
 	{
-		global $DB;
+		global $DB, $preferred_hostname;
 		// Check that this is a valid session start
 		// This prevents replay attacks
 		$sess = $DB->GetAll("select * from {$this->table} where hash=? and username is NULL", array($this->token));
@@ -271,8 +304,19 @@ private $datahash='';		// hash of data field
 		if (!$this->check_pass($user, $pass)) return;
 		$this->username = $user;
 
+		// the token has likely been used on an insecure connection
+		// so generate a new one with the secure flag set
+		$oldtoken = $this->token;
+		$this->token = $this->genSessionID();
+		setcookie("sucssite_session", $this->token, NULL, "/", $preferred_hostname, TRUE);
+
+		// set a cookie as a hint that we're logged in
+		// this can be checked for to allow redirecting to SSL to get the secure cookie
+		setcookie("sucssite_loggedin", "true");
+
 		// Update the session, filling in the blanks
-		$DB->Execute("update {$this->table} set username=?, time='NOW()', ip=? where hash=?", array($this->username, $_SERVER['REMOTE_ADDR'], $this->token));
+		$DB->Execute("update {$this->table} set hash=?, username=?, logintime='NOW()', lastseen='NOW()', ip=? where hash=?", 
+			array($this->token, $this->username, $_SERVER['REMOTE_ADDR'], $oldtoken));
 
 		// Return back to normal session retrieval
 	}
-- 
GitLab