Skip to content

Commit 5249112

Browse files
committed
Merge pull request #693 from OmarElGabry/develop
[testing] Security Enhancements(Session, Cookie, CSRF) & Encryption.
2 parents 748d4e4 + 2e836fb commit 5249112

File tree

11 files changed

+381
-13
lines changed

11 files changed

+381
-13
lines changed

application/_installation/02-create-table-users.sql

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
CREATE TABLE IF NOT EXISTS `huge`.`users` (
22
`user_id` int(11) NOT NULL AUTO_INCREMENT COMMENT 'auto incrementing user_id of each user, unique index',
3+
`session_id` varchar(48) DEFAULT NULL COMMENT 'stores session cookie id to prevent session concurrency',
34
`user_name` varchar(64) COLLATE utf8_unicode_ci NOT NULL COMMENT 'user''s name, unique',
45
`user_password_hash` varchar(255) COLLATE utf8_unicode_ci DEFAULT NULL COMMENT 'user''s password in salted and hashed format',
56
`user_email` varchar(64) COLLATE utf8_unicode_ci NOT NULL COMMENT 'user''s email, unique',
@@ -22,9 +23,9 @@ CREATE TABLE IF NOT EXISTS `huge`.`users` (
2223
UNIQUE KEY `user_email` (`user_email`)
2324
) ENGINE=InnoDB AUTO_INCREMENT=3 DEFAULT CHARSET=utf8 COLLATE=utf8_unicode_ci COMMENT='user data';
2425

25-
INSERT INTO `huge`.`users` (`user_id`, `user_name`, `user_password_hash`, `user_email`, `user_active`, `user_deleted`, `user_account_type`,
26+
INSERT INTO `huge`.`users` (`user_id`, `session_id`, `user_name`, `user_password_hash`, `user_email`, `user_active`, `user_deleted`, `user_account_type`,
2627
`user_has_avatar`, `user_remember_me_token`, `user_creation_timestamp`, `user_suspension_timestamp`, `user_last_login_timestamp`,
2728
`user_failed_logins`, `user_last_failed_login`, `user_activation_hash`, `user_password_reset_hash`,
2829
`user_password_reset_timestamp`, `user_provider_type`) VALUES
29-
(1, 'demo', '$2y$10$OvprunjvKOOhM1h9bzMPs.vuwGIsOqZbw88rzSyGCTJTcE61g5WXi', '[email protected]', 1, 0, 7, 0, NULL, 1422205178, NULL, 1422209189, 0, NULL, NULL, NULL, NULL, 'DEFAULT'),
30-
(2, 'demo2', '$2y$10$OvprunjvKOOhM1h9bzMPs.vuwGIsOqZbw88rzSyGCTJTcE61g5WXi', '[email protected]', 1, 0, 1, 0, NULL, 1422205178, NULL, 1422209189, 0, NULL, NULL, NULL, NULL, 'DEFAULT');
30+
(1, NULL, 'demo', '$2y$10$OvprunjvKOOhM1h9bzMPs.vuwGIsOqZbw88rzSyGCTJTcE61g5WXi', '[email protected]', 1, 0, 7, 0, NULL, 1422205178, NULL, 1422209189, 0, NULL, NULL, NULL, NULL, 'DEFAULT'),
31+
(2, NULL, 'demo2', '$2y$10$OvprunjvKOOhM1h9bzMPs.vuwGIsOqZbw88rzSyGCTJTcE61g5WXi', '[email protected]', 1, 0, 1, 0, NULL, 1422205178, NULL, 1422209189, 0, NULL, NULL, NULL, NULL, 'DEFAULT');

application/config/config.development.php

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -83,9 +83,22 @@
8383
* COOKIE_PATH is the path the cookie is valid on, usually "/" to make it valid on the whole domain.
8484
* @see http://stackoverflow.com/q/9618217/1114320
8585
* @see php.net/manual/en/function.setcookie.php
86+
*
87+
* COOKIE_DOMAIN: The domain where the cookie is valid for.
88+
* COOKIE_DOMAIN mightn't work with "localhost", ".localhost", "127.0.0.1", or ".127.0.0.1". If so, leave it as empty string, false or null.
89+
* @see http://stackoverflow.com/questions/1134290/cookies-on-localhost-with-explicit-domain
90+
* @see http://php.net/manual/en/function.setcookie.php#73107
91+
*
92+
* COOKIE_SECURE: If the cookie will be transferred through secured connection(SSL). It's highly recommended to set it to true if you have secured connection.
93+
* COOKIE_HTTP: If set to true, Cookies that can't be accessed by JS - Highly recommended!
94+
* SESSION_RUNTIME: How long should a session cookie be valid by seconds, 604800 = 1 week.
8695
*/
8796
'COOKIE_RUNTIME' => 1209600,
8897
'COOKIE_PATH' => '/',
98+
'COOKIE_DOMAIN' => "",
99+
'COOKIE_SECURE' => false,
100+
'COOKIE_HTTP' => true,
101+
'SESSION_RUNTIME' => 604800,
89102
/**
90103
* Configuration for: Avatars/Gravatar support
91104
* Set to true if you want to use "Gravatar(s)", a service that automatically gets avatar pictures via using email
@@ -99,6 +112,12 @@
99112
'AVATAR_SIZE' => 44,
100113
'AVATAR_JPEG_QUALITY' => 85,
101114
'AVATAR_DEFAULT_IMAGE' => 'default.jpg',
115+
/**
116+
* Configuration for: Encryption Keys
117+
*
118+
*/
119+
'ENCRYPTION_KEY' => '6#x0gÊìf^25cL1f$08&',
120+
'HMAC_SALT' => '8qk9c^4L6d#15tM8z7n0%',
102121
/**
103122
* Configuration for: Email server credentials
104123
*

application/controller/LoginController.php

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,12 @@ public function index()
3535
*/
3636
public function login()
3737
{
38+
39+
// check if csrf token is valid
40+
if (!Csrf::isTokenValid()) {
41+
self::logout();
42+
}
43+
3844
// perform the login method, put result (true or false) into $login_successful
3945
$login_successful = LoginModel::login(
4046
Request::post('user_name'), Request::post('user_password'), Request::post('set_remember_me_cookie')
@@ -60,6 +66,7 @@ public function logout()
6066
{
6167
LoginModel::logout();
6268
Redirect::home();
69+
exit();
6370
}
6471

6572
/**
@@ -113,6 +120,12 @@ public function editUsername()
113120
public function editUsername_action()
114121
{
115122
Auth::checkAuthentication();
123+
124+
// check if csrf token is valid
125+
if (!Csrf::isTokenValid()) {
126+
self::logout();
127+
}
128+
116129
UserModel::editUserName(Request::post('user_name'));
117130
Redirect::to('login/index');
118131
}

application/core/Auth.php

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,8 @@ public static function checkAuthentication()
1818
// initialize the session (if not initialized yet)
1919
Session::init();
2020

21+
// self::checkSessionConcurrency();
22+
2123
// if user is NOT logged in...
2224
// (if user IS logged in the application will not run the code below and therefore just go on)
2325
if (!Session::userIsLoggedIn()) {
@@ -45,6 +47,8 @@ public static function checkAdminAuthentication()
4547
// initialize the session (if not initialized yet)
4648
Session::init();
4749

50+
// self::checkSessionConcurrency();
51+
4852
// if user is not logged in or is not an admin (= not role type 7)
4953
if (!Session::userIsLoggedIn() || Session::get("user_account_type") != 7) {
5054
// ... then treat user as "not logged in", destroy session, redirect to login page
@@ -56,4 +60,19 @@ public static function checkAdminAuthentication()
5660
exit();
5761
}
5862
}
63+
64+
/**
65+
* Detects if there is concurrent session(i.e. another user logged in with the same current user credentials),
66+
* If so, then logout.
67+
*
68+
*/
69+
public static function checkSessionConcurrency(){
70+
if(Session::userIsLoggedIn()){
71+
if(Session::isConcurrentSessionExists()){
72+
LoginModel::logout();
73+
Redirect::home();
74+
exit();
75+
}
76+
}
77+
}
5978
}

application/core/Controller.php

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,9 @@ function __construct()
2020
// always initialize a session
2121
Session::init();
2222

23+
// check session concurrency
24+
Auth::checkSessionConcurrency();
25+
2326
// user is not logged in but has remember-me-cookie ? then try to login with cookie ("remember me" feature)
2427
if (!Session::userIsLoggedIn() AND Request::cookie('remember_me')) {
2528
header('location: ' . Config::get('URL') . 'login/loginWithCookie');

application/core/Csrf.php

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
<?php
2+
3+
/**
4+
* Cross Site Request Forgery Class
5+
*
6+
*/
7+
8+
/**
9+
* Instructions:
10+
*
11+
* At your form, before the submit button put:
12+
* <input type="hidden" name="csrf_token" value="<?= Csrf::makeToken(); ?>" />
13+
*
14+
* This validation needed in the controller action method to validate CSRF token submitted with the form:
15+
* if (!Csrf::isTokenValid()) {
16+
* Login::logout();
17+
* }
18+
* And that's all
19+
*/
20+
class Csrf {
21+
22+
/**
23+
* get CSRF token and generate a new one if expired
24+
*
25+
* @access public
26+
* @static static method
27+
* @return string
28+
*/
29+
public static function makeToken() {
30+
31+
$max_time = 60 * 60 * 24; // token is valid for 1 day
32+
$stored_time = Session::get('csrf_token_time');
33+
$csrf_token = Session::get('csrf_token');
34+
35+
if($max_time + $stored_time <= time() || empty($csrf_token)){
36+
Session::set('csrf_token', md5(uniqid(rand(), true)));
37+
Session::set('csrf_token_time', time());
38+
}
39+
40+
return Session::get('csrf_token');
41+
}
42+
43+
/**
44+
* checks if CSRF token in session is same as in the form submitted
45+
*
46+
* @access public
47+
* @static static method
48+
* @return bool
49+
*/
50+
public static function isTokenValid(){
51+
$token = Request::post('csrf_token');
52+
return $token === Session::get('csrf_token') && !empty($token);
53+
}
54+
}

application/core/Encryption.php

Lines changed: 142 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,142 @@
1+
<?php
2+
3+
/**
4+
* Encryption and Decryption Class
5+
*
6+
*/
7+
8+
class Encryption{
9+
10+
/**
11+
* Cipher algorithm
12+
*
13+
* @var string
14+
*/
15+
const CIPHER = 'aes-256-cbc';
16+
17+
/**
18+
* Hash function
19+
*
20+
* @var string
21+
*/
22+
const HASH_FUNCTION = 'sha256';
23+
24+
/**
25+
* constructor for Encryption object.
26+
*
27+
* @access private
28+
*/
29+
private function __construct(){}
30+
31+
/**
32+
* Encrypt a string.
33+
*
34+
* @access public
35+
* @static static method
36+
* @param string $plain
37+
* @return string
38+
* @throws Exception If functions don't exists
39+
*/
40+
public static function encrypt($plain){
41+
42+
if(!function_exists('openssl_cipher_iv_length') ||
43+
!function_exists('openssl_random_pseudo_bytes') ||
44+
!function_exists('openssl_encrypt')){
45+
throw new Exception("Encryption function don't exists");
46+
}
47+
48+
// generate initialization vector,
49+
// this will make $iv different every time,
50+
// so, encrypted string will be also different.
51+
$iv_size = openssl_cipher_iv_length(self::CIPHER);
52+
$iv = openssl_random_pseudo_bytes($iv_size);
53+
54+
// generate key for authentication using ENCRYPTION_KEY & HMAC_SALT
55+
$key = mb_substr(hash(self::HASH_FUNCTION, Config::get('ENCRYPTION_KEY') . Config::get('HMAC_SALT')), 0, 32, '8bit');
56+
57+
// append initialization vector
58+
$encrypted_string = openssl_encrypt($plain, self::CIPHER, $key, OPENSSL_RAW_DATA, $iv);
59+
$ciphertext = $iv . $encrypted_string;
60+
61+
// apply the HMAC
62+
$hmac = hash_hmac('sha256', $ciphertext, $key);
63+
64+
return $hmac . $ciphertext;
65+
}
66+
67+
/**
68+
* Decrypted a string.
69+
*
70+
* @access public
71+
* @static static method
72+
* @param string $ciphertext
73+
* @return string
74+
* @throws Exception If $ciphertext is empty, or If functions don't exists
75+
*/
76+
public static function decrypt($ciphertext){
77+
78+
if(empty($ciphertext)){
79+
throw new Exception("the string to decrypt can't be empty");
80+
}
81+
82+
if(!function_exists('openssl_cipher_iv_length') ||
83+
!function_exists('openssl_decrypt')){
84+
throw new Exception("Encryption function don't exists");
85+
}
86+
87+
// generate key used for authentication using ENCRYPTION_KEY & HMAC_SALT
88+
$key = mb_substr(hash(self::HASH_FUNCTION, Config::get('ENCRYPTION_KEY') . Config::get('HMAC_SALT')), 0, 32, '8bit');
89+
90+
// split cipher into: hmac, cipher & iv
91+
$macSize = 64;
92+
$hmac = mb_substr($ciphertext, 0, $macSize, '8bit');
93+
$iv_cipher = mb_substr($ciphertext, $macSize, null, '8bit');
94+
95+
// generate original hmac & compare it with the one in $ciphertext
96+
$originalHmac = hash_hmac('sha256', $iv_cipher, $key);
97+
if(!self::hashEquals($hmac, $originalHmac)){
98+
return false;
99+
}
100+
101+
// split out the initialization vector and cipher
102+
$iv_size = openssl_cipher_iv_length(self::CIPHER);
103+
$iv = mb_substr($iv_cipher, 0, $iv_size, '8bit');
104+
$cipher = mb_substr($iv_cipher, $iv_size, null, '8bit');
105+
106+
return openssl_decrypt($cipher, self::CIPHER, $key, OPENSSL_RAW_DATA, $iv);
107+
}
108+
109+
/**
110+
* A timing attack resistant comparison.
111+
*
112+
* @access private
113+
* @static static method
114+
* @param string $hmac The hmac from the ciphertext being decrypted.
115+
* @param string $compare The comparison hmac.
116+
* @return bool
117+
* @see https://github.com/sarciszewski/php-future/blob/bd6c91fb924b2b35a3e4f4074a642868bd051baf/src/Security.php#L36
118+
*/
119+
private static function hashEquals($hmac, $compare){
120+
121+
if (function_exists('hash_equals')) {
122+
return hash_equals($hmac, $compare);
123+
}
124+
125+
// if hash_equals() is not available,
126+
// then use the following snippet.
127+
// It's equivalent to hash_equals() in PHP 5.6.
128+
$hashLength = mb_strlen($hmac, '8bit');
129+
$compareLength = mb_strlen($compare, '8bit');
130+
131+
if ($hashLength !== $compareLength) {
132+
return false;
133+
}
134+
135+
$result = 0;
136+
for ($i = 0; $i < $hashLength; $i++) {
137+
$result |= (ord($hmac[$i]) ^ ord($compare[$i]));
138+
}
139+
140+
return $result === 0;
141+
}
142+
}

0 commit comments

Comments
 (0)