Skip to content

Commit

Permalink
show user account statistics on dashboard (last login, last failed lo…
Browse files Browse the repository at this point in the history
…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
matthiasmullie committed Mar 5, 2012
2 parents d6a8a86 + 918681a commit ba3a3e1
Show file tree
Hide file tree
Showing 32 changed files with 519 additions and 69 deletions.
16 changes: 16 additions & 0 deletions .htaccess
Expand Up @@ -11,6 +11,22 @@ FileETag MTime Size
RewriteEngine On RewriteEngine On
RewriteBase / 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 # backend is an existing folder, but it should be handled as all urls
RewriteCond %{REQUEST_URI} ^/backend/$ RewriteCond %{REQUEST_URI} ^/backend/$
RewriteRule . index.php [NC,L] RewriteRule . index.php [NC,L]
Expand Down
3 changes: 3 additions & 0 deletions CHANGELOG.md
Expand Up @@ -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: 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: 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: improved existing translations.
* Locale: added translations for Spanish (by Yéred Zabdiel) * 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 Swedish (by Erik Holmquist - http://www.holmquist.de & Peter Mayertz - http://www.mayertz.se)
* Locale: added translations for Ukrainian (by Манжела Борис) * Locale: added translations for Ukrainian (by Манжела Борис)
* Locale: added translations for Lithuanian (by Rolanda Naujasdizainas - http://www.naujasdizainas.lt) * Locale: added translations for Lithuanian (by Rolanda Naujasdizainas - http://www.naujasdizainas.lt)
* Location: revised Location-module, added some functionality. * Location: revised Location-module, added some functionality.
* Pages: added widget for previous/parent/next navigation. * 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: Bugfixes:


Expand Down
52 changes: 52 additions & 0 deletions backend/core/engine/authentication.php
Expand Up @@ -13,6 +13,7 @@
* @author Tijs Verkoyen <tijs@sumocoders.be> * @author Tijs Verkoyen <tijs@sumocoders.be>
* @author Davy Hellemans <davy.hellemans@netlash.com> * @author Davy Hellemans <davy.hellemans@netlash.com>
* @author Sam Tubbax <sam@sumocoders.be> * @author Sam Tubbax <sam@sumocoders.be>
* @author Annelies Van Extergem <annelies.vanextergem@netlash.com>
*/ */
class BackendAuthentication class BackendAuthentication
{ {
Expand All @@ -37,6 +38,57 @@ class BackendAuthentication
*/ */
private static $user; 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 * Cleanup sessions for the current user and sessions that are invalid
*/ */
Expand Down
2 changes: 1 addition & 1 deletion backend/core/engine/language.php
Expand Up @@ -296,7 +296,7 @@ public static function setLocale($language)
try try
{ {
// store in cookie // store in cookie
SpoonCookie::set('interface_language', $language); CommonCookie::set('interface_language', $language);
} }


// catch exceptions // catch exceptions
Expand Down
18 changes: 16 additions & 2 deletions backend/core/engine/url.php
Expand Up @@ -38,6 +38,20 @@ public function __construct()
$this->processQueryString(); $this->processQueryString();
} }


/**
* 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 * Get the host
* *
Expand Down Expand Up @@ -288,9 +302,9 @@ private function setLocale()
} }


// no authenticated user, but available from a cookie // 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 // validate if the requested locale is possible
Expand Down
17 changes: 0 additions & 17 deletions backend/core/engine/user.php
Expand Up @@ -156,23 +156,6 @@ public function getSetting($key, $defaultValue = null)
return $this->settings[$key]; return $this->settings[$key];
} }


/**
* 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 * Get all settings at once
* *
Expand Down
32 changes: 32 additions & 0 deletions backend/core/installer/data/locale.xml
Expand Up @@ -3872,6 +3872,10 @@
<translation language="pl"><![CDATA[avatar]]></translation> <translation language="pl"><![CDATA[avatar]]></translation>
<translation language="tr"><![CDATA[Avatar]]></translation> <translation language="tr"><![CDATA[Avatar]]></translation>
</item> </item>
<item type="label" name="Average">
<translation language="nl"><![CDATA[gemiddeld]]></translation>
<translation language="en"><![CDATA[average]]></translation>
</item>
<item type="label" name="Back"> <item type="label" name="Back">
<translation language="nl"><![CDATA[terug]]></translation> <translation language="nl"><![CDATA[terug]]></translation>
<translation language="en"><![CDATA[back]]></translation> <translation language="en"><![CDATA[back]]></translation>
Expand Down Expand Up @@ -4467,6 +4471,10 @@
<translation language="sv"><![CDATA[CSV]]></translation> <translation language="sv"><![CDATA[CSV]]></translation>
<translation language="tr"><![CDATA[CSV]]></translation> <translation language="tr"><![CDATA[CSV]]></translation>
</item> </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"> <item type="label" name="CustomURL">
<translation language="nl"><![CDATA[aangepaste URL]]></translation> <translation language="nl"><![CDATA[aangepaste URL]]></translation>
<translation language="en"><![CDATA[custom URL]]></translation> <translation language="en"><![CDATA[custom URL]]></translation>
Expand Down Expand Up @@ -5645,6 +5653,18 @@
<translation language="uk"><![CDATA[Останнє редагування]]></translation> <translation language="uk"><![CDATA[Останнє редагування]]></translation>
<translation language="sv"><![CDATA[Senast redigerad den ]]></translation> <translation language="sv"><![CDATA[Senast redigerad den ]]></translation>
</item> </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"> <item type="label" name="LastSaved">
<translation language="nl"><![CDATA[laatst opgeslagen]]></translation> <translation language="nl"><![CDATA[laatst opgeslagen]]></translation>
<translation language="en"><![CDATA[last saved]]></translation> <translation language="en"><![CDATA[last saved]]></translation>
Expand Down Expand Up @@ -6223,6 +6243,10 @@
<translation language="uk"><![CDATA[Заголовок]]></translation> <translation language="uk"><![CDATA[Заголовок]]></translation>
<translation language="sv"><![CDATA[navigation title]]></translation> <translation language="sv"><![CDATA[navigation title]]></translation>
</item> </item>
<item type="label" name="Never">
<translation language="nl"><![CDATA[nooit]]></translation>
<translation language="en"><![CDATA[never]]></translation>
</item>
<item type="label" name="NewPassword"> <item type="label" name="NewPassword">
<translation language="nl"><![CDATA[nieuw wachtwoord]]></translation> <translation language="nl"><![CDATA[nieuw wachtwoord]]></translation>
<translation language="en"><![CDATA[new password]]></translation> <translation language="en"><![CDATA[new password]]></translation>
Expand Down Expand Up @@ -6337,6 +6361,10 @@
<translation language="uk"><![CDATA[Не використовувати]]></translation> <translation language="uk"><![CDATA[Не використовувати]]></translation>
<translation language="sv"><![CDATA[inget]]></translation> <translation language="sv"><![CDATA[inget]]></translation>
</item> </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"> <item type="label" name="NoTheme">
<translation language="nl"><![CDATA[geen thema]]></translation> <translation language="nl"><![CDATA[geen thema]]></translation>
<translation language="en"><![CDATA[no theme]]></translation> <translation language="en"><![CDATA[no theme]]></translation>
Expand Down Expand Up @@ -6554,6 +6582,10 @@
<translation language="sv"><![CDATA[lösenord]]></translation> <translation language="sv"><![CDATA[lösenord]]></translation>
<translation language="tr"><![CDATA[şifre]]></translation> <translation language="tr"><![CDATA[şifre]]></translation>
</item> </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"> <item type="label" name="PerDay">
<translation language="nl"><![CDATA[per dag]]></translation> <translation language="nl"><![CDATA[per dag]]></translation>
<translation language="en"><![CDATA[per day]]></translation> <translation language="en"><![CDATA[per day]]></translation>
Expand Down
4 changes: 2 additions & 2 deletions backend/core/js/backend.js
Expand Up @@ -1082,8 +1082,8 @@ jsBackend.controls =
// strong password // strong password
if(score >= 4) return 'strong'; if(score >= 4) return 'strong';


// ok // average
if(score >= 2) return 'ok'; if(score >= 2) return 'average';


// fallback // fallback
return 'weak'; return 'weak';
Expand Down
3 changes: 2 additions & 1 deletion backend/core/layout/css/imports/core_modules.css
Expand Up @@ -573,7 +573,8 @@
margin: 0 0 0 12px; margin: 0 0 0 12px;
} }


#users.usersAddEdit .settingsUserInfo .infoGrid th { #users.usersAddEdit .settingsUserInfo .infoGrid th,
#widgetUsersStatistics .settingsUserInfo .infoGrid th {
white-space: nowrap; white-space: nowrap;
} }


Expand Down
2 changes: 1 addition & 1 deletion backend/core/layout/css/imports/password_strength.css
Expand Up @@ -49,7 +49,7 @@
} }


#passwordStrength .weak { background: #f84a24} #passwordStrength .weak { background: #f84a24}
#passwordStrength .ok { background: #00b244 } #passwordStrength .average { background: #00b244 }
#passwordStrength .strong { background: #007b37 } #passwordStrength .strong { background: #007b37 }


#passwordStrengthMeter td { #passwordStrengthMeter td {
Expand Down
11 changes: 10 additions & 1 deletion backend/init.php
Expand Up @@ -122,10 +122,19 @@ public static function autoLoader($className)
// split in parts // split in parts
if(!preg_match_all('/[A-Z][a-z0-9]*/', $className, $parts)) return; if(!preg_match_all('/[A-Z][a-z0-9]*/', $className, $parts)) return;



// the real matches // the real matches
$parts = $parts[0]; $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 // get root path constant and see if it exists
$rootPath = strtoupper(array_shift($parts)) . '_PATH'; $rootPath = strtoupper(array_shift($parts)) . '_PATH';
if(!defined($rootPath)) return; if(!defined($rootPath)) return;
Expand Down
12 changes: 12 additions & 0 deletions backend/modules/authentication/actions/index.php
Expand Up @@ -11,6 +11,7 @@
* This is the index-action (default), it will display the login screen * This is the index-action (default), it will display the login screen
* *
* @author Tijs Verkoyen <tijs@sumocoders.be> * @author Tijs Verkoyen <tijs@sumocoders.be>
* @author Annelies Van Extergem <annelies.vanextergem@netlash.com>
*/ */
class BackendAuthenticationIndex extends BackendBaseActionIndex class BackendAuthenticationIndex extends BackendBaseActionIndex
{ {
Expand Down Expand Up @@ -93,6 +94,9 @@ private function validateForm()
if(!headers_sent()) header('400 Bad Request', true, 400); if(!headers_sent()) header('400 Bad Request', true, 400);
} }


// get the user's id
$userId = BackendUsersModel::getIdByEmail($txtEmail->getValue());

// all fields are ok? // all fields are ok?
if($txtEmail->isFilled() && $txtPassword->isFilled() && $this->frm->getToken() == $this->frm->getField('form_token')->getValue()) if($txtEmail->isFilled() && $txtPassword->isFilled() && $this->frm->getToken() == $this->frm->getField('form_token')->getValue())
{ {
Expand All @@ -108,6 +112,9 @@ private function validateForm()
// increment and store // increment and store
SpoonSession::set('backend_login_attempts', ++$current); 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 // show error
$this->tpl->assign('hasError', true); $this->tpl->assign('hasError', true);
} }
Expand Down Expand Up @@ -153,6 +160,11 @@ private function validateForm()
SpoonSession::delete('backend_login_attempts'); SpoonSession::delete('backend_login_attempts');
SpoonSession::delete('backend_last_attempt'); 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 // create filter with modules which may not be displayed
$filter = array('authentication', 'error', 'core'); $filter = array('authentication', 'error', 'core');


Expand Down
6 changes: 3 additions & 3 deletions backend/modules/groups/actions/edit.php
Expand Up @@ -341,9 +341,9 @@ private function loadDataGrids()
$this->dataGridUsers->setColumnsSequence('nickname', 'surname', 'name', 'email'); $this->dataGridUsers->setColumnsSequence('nickname', 'surname', 'name', 'email');


// show users's name, surname and nickname // show users's name, surname and nickname
$this->dataGridUsers->setColumnFunction(array('BackendUser', 'getSettingByUserId'), array('[id]', 'surname'), 'surname', false); $this->dataGridUsers->setColumnFunction(array('BackendUsersModel', 'getSetting'), array('[id]', 'surname'), 'surname', false);
$this->dataGridUsers->setColumnFunction(array('BackendUser', 'getSettingByUserId'), array('[id]', 'name'), 'name', false); $this->dataGridUsers->setColumnFunction(array('BackendUsersModel', 'getSetting'), array('[id]', 'name'), 'name', false);
$this->dataGridUsers->setColumnFunction(array('BackendUser', 'getSettingByUserId'), array('[id]', 'nickname'), 'nickname', false); $this->dataGridUsers->setColumnFunction(array('BackendUsersModel', 'getSetting'), array('[id]', 'nickname'), 'nickname', false);
} }
} }


Expand Down
11 changes: 11 additions & 0 deletions backend/modules/groups/installer/installer.php
Expand Up @@ -45,6 +45,17 @@ private function insertDashboardSequence()


// insert default dashboard widget // insert default dashboard widget
$this->insertDashboardWidget('settings', 'analyse', $analyse); $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);
} }


/** /**
Expand Down
5 changes: 5 additions & 0 deletions backend/modules/users/actions/add.php
Expand Up @@ -134,6 +134,7 @@ private function validateForm()
$settings['csv_split_character'] = $this->frm->getField('csv_split_character')->getValue(); $settings['csv_split_character'] = $this->frm->getField('csv_split_character')->getValue();
$settings['csv_line_ending'] = $this->frm->getField('csv_line_ending')->getValue(); $settings['csv_line_ending'] = $this->frm->getField('csv_line_ending')->getValue();
$settings['password_key'] = uniqid(); $settings['password_key'] = uniqid();
$settings['current_password_change'] = time();
$settings['avatar'] = 'no-avatar.gif'; $settings['avatar'] = 'no-avatar.gif';
$settings['api_access'] = (bool) $this->frm->getField('api_access')->getChecked(); $settings['api_access'] = (bool) $this->frm->getField('api_access')->getChecked();


Expand Down Expand Up @@ -168,6 +169,10 @@ private function validateForm()
$user['email'] = $this->frm->getField('email')->getValue(); $user['email'] = $this->frm->getField('email')->getValue();
$user['password'] = BackendAuthentication::getEncryptedString($this->frm->getField('password')->getValue(true), $settings['password_key']); $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 // save changes
$user['id'] = (int) BackendUsersModel::insert($user, $settings); $user['id'] = (int) BackendUsersModel::insert($user, $settings);


Expand Down

0 comments on commit ba3a3e1

Please sign in to comment.