Permalink
Browse files

show user account statistics on dashboard (last login, last failed lo…

…gin, last password change & password strength)

save cookies httponly by default & automatically secure when browsing over https
make cache-files inaccessible over http

Merge branch 'security_audit' of github.com:annelyze/forkcms

Conflicts:
	backend/core/engine/authentication.php
	backend/core/engine/url.php
  • Loading branch information...
2 parents d6a8a86 + 918681a commit ba3a3e1a92062fe30bdd7a28c2772ec1c5770320 @matthiasmullie matthiasmullie committed Mar 5, 2012
View
@@ -11,6 +11,22 @@ FileETag MTime Size
RewriteEngine On
RewriteBase /
+ # forbidden folders
+ RewriteRule .*\.gitignore - [F]
+ RewriteRule frontend/cache/cached_templates - [F]
+ RewriteRule frontend/cache/compiled_templates - [F]
+ RewriteRule frontend/cache/config - [F]
+ RewriteRule frontend/cache/locale - [F]
+ RewriteRule frontend/cache/logs - [F]
+ RewriteRule frontend/cache/navigation/.*\.php - [F]
+ RewriteRule frontend/cache/search - [F]
+ RewriteRule backend/cache/compiled_templates - [F]
+ RewriteRule backend/cache/config - [F]
+ RewriteRule backend/cache/cronjobs - [F]
+ RewriteRule backend/cache/locale - [F]
+ RewriteRule backend/cache/logs - [F]
+ RewriteRule backend/cache/navigation - [F]
+
# backend is an existing folder, but it should be handled as all urls
RewriteCond %{REQUEST_URI} ^/backend/$
RewriteRule . index.php [NC,L]
View
@@ -4,13 +4,16 @@ Improvements:
* Core: added some JS to automatically add a .filled class on all form fields that are being filled out.
* Core: only images that are smaller then 5kb will be included in the CSS-file.
+* Core: save cookies httponly by default & automatically secure when browsing over https.
+* Core: make cache-files inaccessible over http.
* Locale: improved existing translations.
* Locale: added translations for Spanish (by Yéred Zabdiel)
* Locale: added translations for Swedish (by Erik Holmquist - http://www.holmquist.de & Peter Mayertz - http://www.mayertz.se)
* Locale: added translations for Ukrainian (by Манжела Борис)
* Locale: added translations for Lithuanian (by Rolanda Naujasdizainas - http://www.naujasdizainas.lt)
* Location: revised Location-module, added some functionality.
* Pages: added widget for previous/parent/next navigation.
+* Users: show user account statistics on dashboard (last login, last failed login, last password change & password strength).
Bugfixes:
@@ -13,6 +13,7 @@
* @author Tijs Verkoyen <tijs@sumocoders.be>
* @author Davy Hellemans <davy.hellemans@netlash.com>
* @author Sam Tubbax <sam@sumocoders.be>
+ * @author Annelies Van Extergem <annelies.vanextergem@netlash.com>
*/
class BackendAuthentication
{
@@ -38,6 +39,57 @@ class BackendAuthentication
private static $user;
/**
+ * Check the strength of the password
+ *
+ * @param string $password The password.
+ * @return string
+ */
+ public static function checkPassword($password)
+ {
+ // init vars
+ $score = 0;
+ $uniqueChars = array();
+
+ // less then 4 chars is just a weak password
+ if(mb_strlen($password) <= 4) return 'weak';
+
+ // loop chars and add unique chars
+ $passwordChars = str_split($password);
+ foreach($passwordChars as $char)
+ {
+ $uniqueChars[$char] = $char;
+ }
+
+ // less then 3 unique chars is just weak
+ if(count($uniqueChars) < 3) return 'weak';
+
+ // more then 6 chars is good
+ if(mb_strlen($password) >= 6) $score++;
+
+ // more then 8 is beter
+ if(mb_strlen($password) >= 8) $score++;
+
+ // @todo
+ // upper and lowercase?
+ if(preg_match('/[a-z]/', $password) && preg_match('/[A-Z]/', $password)) $score += 2;
+
+ // number?
+ if(preg_match('/\d+/', $password)) $score++;
+
+ // special char?
+ if(preg_match('/.[!,@,#,$,%,^,&,*,?,_,~,-,(,)]/', $password)) $score++;
+
+ // strong password
+ if($score >= 4) return 'strong';
+
+ // ok
+ if($score >= 2) return 'average';
+
+ // fallback
+ return 'weak';
+ }
+
+ /**
* Cleanup sessions for the current user and sessions that are invalid
*/
public static function cleanupOldSessions()
@@ -296,7 +296,7 @@ public static function setLocale($language)
try
{
// store in cookie
- SpoonCookie::set('interface_language', $language);
+ CommonCookie::set('interface_language', $language);
}
// catch exceptions
@@ -39,6 +39,20 @@ public function __construct()
}
/**
+ * Get the domain
+ *
+ * @return string The current domain (without www.)
+ */
+ public function getDomain()
+ {
+ // get host
+ $host = $this->getHost();
+
+ // replace
+ return str_replace('www.', '', $host);
+ }
+
+ /**
* Get the host
*
* @return string
@@ -288,9 +302,9 @@ private function setLocale()
}
// no authenticated user, but available from a cookie
- elseif(SpoonCookie::exists('interface_language'))
+ elseif(CommonCookie::exists('interface_language'))
{
- $locale = SpoonCookie::get('interface_language');
+ $locale = CommonCookie::get('interface_language');
}
// validate if the requested locale is possible
@@ -157,23 +157,6 @@ public function getSetting($key, $defaultValue = null)
}
/**
- * Fetch a user setting for a specific user
- *
- * @param int $userId The id of the user.
- * @param string $setting The name of the setting to get.
- * @return mixed
- */
- public static function getSettingByUserId($userId, $setting)
- {
- return @unserialize(BackendModel::getDB()->getVar(
- 'SELECT value
- FROM users_settings
- WHERE user_id = ? AND name = ?',
- array((int) $userId, (string) $setting)
- ));
- }
-
- /**
* Get all settings at once
*
* @return array
@@ -3872,6 +3872,10 @@
<translation language="pl"><![CDATA[avatar]]></translation>
<translation language="tr"><![CDATA[Avatar]]></translation>
</item>
+ <item type="label" name="Average">
+ <translation language="nl"><![CDATA[gemiddeld]]></translation>
+ <translation language="en"><![CDATA[average]]></translation>
+ </item>
<item type="label" name="Back">
<translation language="nl"><![CDATA[terug]]></translation>
<translation language="en"><![CDATA[back]]></translation>
@@ -4467,6 +4471,10 @@
<translation language="sv"><![CDATA[CSV]]></translation>
<translation language="tr"><![CDATA[CSV]]></translation>
</item>
+ <item type="label" name="CurrentPassword">
+ <translation language="nl"><![CDATA[huidig wachtwoord]]></translation>
+ <translation language="en"><![CDATA[current password]]></translation>
+ </item>
<item type="label" name="CustomURL">
<translation language="nl"><![CDATA[aangepaste URL]]></translation>
<translation language="en"><![CDATA[custom URL]]></translation>
@@ -5645,6 +5653,18 @@
<translation language="uk"><![CDATA[Останнє редагування]]></translation>
<translation language="sv"><![CDATA[Senast redigerad den ]]></translation>
</item>
+ <item type="label" name="LastFailedLoginAttempt">
+ <translation language="nl"><![CDATA[laatste mislukte aanmelding]]></translation>
+ <translation language="en"><![CDATA[last failed login attempt]]></translation>
+ </item>
+ <item type="label" name="LastLogin">
+ <translation language="nl"><![CDATA[laatste aanmelding]]></translation>
+ <translation language="en"><![CDATA[last login]]></translation>
+ </item>
+ <item type="label" name="LastPasswordChange">
+ <translation language="nl"><![CDATA[laatste aanpassing wachtwoord]]></translation>
+ <translation language="en"><![CDATA[last password change]]></translation>
+ </item>
<item type="label" name="LastSaved">
<translation language="nl"><![CDATA[laatst opgeslagen]]></translation>
<translation language="en"><![CDATA[last saved]]></translation>
@@ -6223,6 +6243,10 @@
<translation language="uk"><![CDATA[Заголовок]]></translation>
<translation language="sv"><![CDATA[navigation title]]></translation>
</item>
+ <item type="label" name="Never">
+ <translation language="nl"><![CDATA[nooit]]></translation>
+ <translation language="en"><![CDATA[never]]></translation>
+ </item>
<item type="label" name="NewPassword">
<translation language="nl"><![CDATA[nieuw wachtwoord]]></translation>
<translation language="en"><![CDATA[new password]]></translation>
@@ -6337,6 +6361,10 @@
<translation language="uk"><![CDATA[Не використовувати]]></translation>
<translation language="sv"><![CDATA[inget]]></translation>
</item>
+ <item type="label" name="NoPreviousLogin">
+ <translation language="nl"><![CDATA[geen eerdere aanmelding]]></translation>
+ <translation language="en"><![CDATA[no previous login]]></translation>
+ </item>
<item type="label" name="NoTheme">
<translation language="nl"><![CDATA[geen thema]]></translation>
<translation language="en"><![CDATA[no theme]]></translation>
@@ -6554,6 +6582,10 @@
<translation language="sv"><![CDATA[lösenord]]></translation>
<translation language="tr"><![CDATA[şifre]]></translation>
</item>
+ <item type="label" name="PasswordStrength">
+ <translation language="nl"><![CDATA[wachtwoord sterkte]]></translation>
+ <translation language="en"><![CDATA[password strength]]></translation>
+ </item>
<item type="label" name="PerDay">
<translation language="nl"><![CDATA[per dag]]></translation>
<translation language="en"><![CDATA[per day]]></translation>
@@ -1082,8 +1082,8 @@ jsBackend.controls =
// strong password
if(score >= 4) return 'strong';
- // ok
- if(score >= 2) return 'ok';
+ // average
+ if(score >= 2) return 'average';
// fallback
return 'weak';
@@ -573,7 +573,8 @@
margin: 0 0 0 12px;
}
- #users.usersAddEdit .settingsUserInfo .infoGrid th {
+ #users.usersAddEdit .settingsUserInfo .infoGrid th,
+ #widgetUsersStatistics .settingsUserInfo .infoGrid th {
white-space: nowrap;
}
@@ -49,7 +49,7 @@
}
#passwordStrength .weak { background: #f84a24}
-#passwordStrength .ok { background: #00b244 }
+#passwordStrength .average { background: #00b244 }
#passwordStrength .strong { background: #007b37 }
#passwordStrengthMeter td {
View
@@ -122,10 +122,19 @@ public static function autoLoader($className)
// split in parts
if(!preg_match_all('/[A-Z][a-z0-9]*/', $className, $parts)) return;
-
// the real matches
$parts = $parts[0];
+ // is it an application class?
+ if(isset($parts[0]) && $parts[0] == 'Common')
+ {
+ $chunks = $parts;
+ array_shift($chunks);
+ $pathToLoad = PATH_LIBRARY . '/base/' . strtolower(implode('_', $chunks)) . '.php';
+
+ if(SpoonFile::exists($pathToLoad)) require_once $pathToLoad;
+ }
+
// get root path constant and see if it exists
$rootPath = strtoupper(array_shift($parts)) . '_PATH';
if(!defined($rootPath)) return;
@@ -11,6 +11,7 @@
* This is the index-action (default), it will display the login screen
*
* @author Tijs Verkoyen <tijs@sumocoders.be>
+ * @author Annelies Van Extergem <annelies.vanextergem@netlash.com>
*/
class BackendAuthenticationIndex extends BackendBaseActionIndex
{
@@ -93,6 +94,9 @@ private function validateForm()
if(!headers_sent()) header('400 Bad Request', true, 400);
}
+ // get the user's id
+ $userId = BackendUsersModel::getIdByEmail($txtEmail->getValue());
+
// all fields are ok?
if($txtEmail->isFilled() && $txtPassword->isFilled() && $this->frm->getToken() == $this->frm->getField('form_token')->getValue())
{
@@ -108,6 +112,9 @@ private function validateForm()
// increment and store
SpoonSession::set('backend_login_attempts', ++$current);
+ // save the failed login attempt in the user's settings
+ BackendUsersModel::setSetting($userId, 'last_failed_login_attempt', time());
+
// show error
$this->tpl->assign('hasError', true);
}
@@ -153,6 +160,11 @@ private function validateForm()
SpoonSession::delete('backend_login_attempts');
SpoonSession::delete('backend_last_attempt');
+ // save the login timestamp in the user's settings
+ $lastLogin = BackendUsersModel::getSetting($userId, 'current_login');
+ BackendUsersModel::setSetting($userId, 'current_login', time());
+ if($lastLogin) BackendUsersModel::setSetting($userId, 'last_login', $lastLogin);
+
// create filter with modules which may not be displayed
$filter = array('authentication', 'error', 'core');
@@ -341,9 +341,9 @@ private function loadDataGrids()
$this->dataGridUsers->setColumnsSequence('nickname', 'surname', 'name', 'email');
// show users's name, surname and nickname
- $this->dataGridUsers->setColumnFunction(array('BackendUser', 'getSettingByUserId'), array('[id]', 'surname'), 'surname', false);
- $this->dataGridUsers->setColumnFunction(array('BackendUser', 'getSettingByUserId'), array('[id]', 'name'), 'name', false);
- $this->dataGridUsers->setColumnFunction(array('BackendUser', 'getSettingByUserId'), array('[id]', 'nickname'), 'nickname', false);
+ $this->dataGridUsers->setColumnFunction(array('BackendUsersModel', 'getSetting'), array('[id]', 'surname'), 'surname', false);
+ $this->dataGridUsers->setColumnFunction(array('BackendUsersModel', 'getSetting'), array('[id]', 'name'), 'name', false);
+ $this->dataGridUsers->setColumnFunction(array('BackendUsersModel', 'getSetting'), array('[id]', 'nickname'), 'nickname', false);
}
}
@@ -45,6 +45,17 @@ private function insertDashboardSequence()
// insert default dashboard widget
$this->insertDashboardWidget('settings', 'analyse', $analyse);
+
+ // create default dashboard widget
+ $statistics = array(
+ 'column' => 'left',
+ 'position' => 2,
+ 'hidden' => false,
+ 'present' => true
+ );
+
+ // insert default dashboard widget
+ $this->insertDashboardWidget('users', 'statistics', $statistics);
}
/**
@@ -134,6 +134,7 @@ private function validateForm()
$settings['csv_split_character'] = $this->frm->getField('csv_split_character')->getValue();
$settings['csv_line_ending'] = $this->frm->getField('csv_line_ending')->getValue();
$settings['password_key'] = uniqid();
+ $settings['current_password_change'] = time();
$settings['avatar'] = 'no-avatar.gif';
$settings['api_access'] = (bool) $this->frm->getField('api_access')->getChecked();
@@ -168,6 +169,10 @@ private function validateForm()
$user['email'] = $this->frm->getField('email')->getValue();
$user['password'] = BackendAuthentication::getEncryptedString($this->frm->getField('password')->getValue(true), $settings['password_key']);
+ // save the password strength
+ $passwordStrength = BackendAuthentication::checkPassword($this->frm->getField('password')->getValue(true));
+ $settings['password_strength'] = $passwordStrength;
+
// save changes
$user['id'] = (int) BackendUsersModel::insert($user, $settings);
Oops, something went wrong.

0 comments on commit ba3a3e1

Please sign in to comment.