/[drupal]/contributions/modules/login_security/login_security.module
ViewVC logotype

Contents of /contributions/modules/login_security/login_security.module

Parent Directory Parent Directory | Revision Log Revision Log | View Revision Graph Revision Graph


Revision 1.20 - (show annotations) (download) (as text)
Fri Jul 31 16:47:40 2009 UTC (3 months, 4 weeks ago) by deekayen
Branch: MAIN
CVS Tags: HEAD
Changes since 1.19: +46 -37 lines
File MIME type: text/x-php
#496446 by ilo: Administration interface tough love
#501040 by deekayen, ilo: Add link to blocked user accounts for watchdog
1 <?php
2 // $Id: login_security.module,v 1.19 2009/06/24 10:45:03 ilo Exp $
3
4 /**
5 * @file
6 * Login Security
7 *
8 * GPL published.. if you don't have a copy of the license, search for it, it's free
9 * Copyrighted by ilo@reversing.org
10 * Thanks to christefano for the module tips and strings
11 */
12
13 define('LOGIN_SECURITY_TRACK_TIME', 0);
14 define('LOGIN_SECURITY_BASE_TIME', 0);
15 define('LOGIN_SECURITY_DELAY_INCREASE', 0);
16 define('LOGIN_SECURITY_USER_WRONG_COUNT', 0);
17 define('LOGIN_SECURITY_HOST_WRONG_COUNT', 0);
18 define('LOGIN_SECURITY_HOST_WRONG_COUNT_HARD', 0);
19 define('LOGIN_SECURITY_DISABLE_CORE_LOGIN_ERROR', 0);
20 define('LOGIN_SECURITY_NOTICE_ATTEMPTS_AVAILABLE', 0);
21 define('LOGIN_SECURITY_NOTICE_ATTEMPTS_MESSAGE', "You have used %user_current_count out of %user_block_attempts login attempts. After all %user_block_attempts have been used, you will be unable to login.");
22 define('LOGIN_SECURITY_HOST_SOFT_BANNED', "This host is not allowed to log in to %site. Please contact your site administrator.");
23 define('LOGIN_SECURITY_HOST_HARD_BANNED', "The IP address <em>%ip</em> is banned at %site, and will not be able to access any of its content from now on. Please contact the site administrator.");
24 define('LOGIN_SECURITY_USER_BLOCKED', "The user <em>%username</em> has been blocked due to failed login attempts.");
25 define('LOGIN_SECURITY_USER_BLOCKED_EMAIL', FALSE);
26 define('LOGIN_SECURITY_USER_BLOCKED_EMAIL_SUBJECT', "Security action: The user %username has been blocked.");
27 define('LOGIN_SECURITY_USER_BLOCKED_EMAIL_BODY', "The user %username (%edit_uri) has been blocked at %site due to the amount of failed login attempts. Please check the logs for more information.");
28
29 /**
30 * Implementation of hook_cron().
31 */
32 function login_security_cron() {
33 // calc expiring time of login security tracked entries
34 $time = time() - (variable_get('login_security_track_time', LOGIN_SECURITY_TRACK_TIME) * 3600);
35 db_query("DELETE FROM {login_security_track} WHERE timestamp < %d", $time);
36 return;
37 }
38
39 /**
40 * Implementation of hook_user().
41 */
42 function login_security_user($op, &$edit, &$account, $category = NULL) {
43 switch ($op) {
44 case 'login':
45 // On success login remove any temporary protection for the IP address and the username
46 db_query("DELETE FROM {login_security_track} WHERE name = '%s' AND host = '%s'", check_plain($edit['name']), check_plain(ip_address()));
47 break;
48 case 'update':
49 // The update case can be launched by the user or by any user administrator
50 // On update, remove only the unser information tracked
51 db_query("DELETE FROM {login_security_track} WHERE name = '%s'", check_plain($edit['name']));
52 break;
53 // Cron will clean the forgotten tracking entries, including the deleted users.
54 }
55 }
56
57 /**
58 * Implementation of hook_form_alter().
59 */
60 function login_security_form_alter(&$form, $form_state, $form_id) {
61 switch ($form_id) {
62 case 'user_login':
63 case 'user_login_block':
64 // Put login_security first or the capture of the previous login timestamp won't work
65 // and core's validation will update to the current login instance before login_security
66 // can read the old timestamp.
67 $form['#validate'] = array_merge(array('login_security_soft_block_validate', 'login_security_set_login_timestamp'), $form['#validate']);
68 $form['#validate'][] = 'login_security_validate';
69 break;
70 case 'user_admin_settings':
71 if (user_access('administer users')) {
72 $form['login_security'] = array(
73 '#type' => 'fieldset',
74 '#title' => t('Login Security settings'),
75 '#weight' => 0,
76 '#collapsible' => FALSE,
77 );
78 $form['login_security'][] = login_security_build_admin_form();
79 }
80 break;
81 }
82 }
83
84 /**
85 * Build a form body for the configuration settings.
86 */
87 function login_security_build_admin_form() {
88 $form = array();
89
90 $form['login_security_track_time'] = array(
91 '#type' => 'textfield',
92 '#title' => t('Track time'),
93 '#default_value' => variable_get('login_security_track_time', LOGIN_SECURITY_TRACK_TIME),
94 '#size' => 3,
95 '#maxlength' => 3,
96 '#description' => t('Enter the time that each failed login attempt is kept for future computing.'),
97 '#field_suffix' => '<kbd>'. t('Hours') .'</kbd>'
98 );
99 $form['login_security_delay_base_time'] = array(
100 '#type' => 'textfield',
101 '#title' => t('Login delay base time'),
102 '#default_value' => variable_get('login_security_delay_base_time', LOGIN_SECURITY_BASE_TIME),
103 '#size' => 3,
104 '#maxlength' => 3,
105 '#description' => t('Enter the base time for login delay'),
106 '#field_suffix' => '<kbd>'. t('Seconds') .'</kbd>'
107 );
108 $form['login_security_delay_increase'] = array(
109 '#type' => 'radios',
110 '#title' => t('Increase delay for each attempt?'),
111 '#default_value' => variable_get('login_security_delay_increase', LOGIN_SECURITY_DELAY_INCREASE),
112 '#options' => array(1 => 'Yes', 0 => 'No'),
113 '#description' => t('Computed as (base time) x (login attempts) for that user.'),
114 );
115 $form['login_security_user_wrong_count'] = array(
116 '#type' => 'textfield',
117 '#title' => t('Maximum number of login failures before blocking a user'),
118 '#default_value' => variable_get('login_security_user_wrong_count', LOGIN_SECURITY_USER_WRONG_COUNT),
119 '#size' => 3,
120 '#maxlength' => 3,
121 '#description' => t('Enter the number of login failures a user is allowed. After that amount is reached, the user will be blocked, no matter the host attempting to log in. Use this option carefully on public sites, as an attacker may block your site users.'),
122 '#field_suffix' => '<kbd>'. t('Failed attempts') .'</kbd>'
123 );
124 $form['login_security_host_wrong_count'] = array(
125 '#type' => 'textfield',
126 '#title' => t('Maximum number of login failures before soft blocking a host'),
127 '#default_value' => variable_get('login_security_host_wrong_count', LOGIN_SECURITY_HOST_WRONG_COUNT),
128 '#size' => 3,
129 '#maxlength' => 3,
130 '#description' => t('Enter the number of login failures a host is allowed. After that amount is reached, the host will not be able to log in but can still browse the site contents as an anonymous user.'),
131 '#field_suffix' => '<kbd>'. t('Failed attempts') .'</kbd>'
132 );
133 $form['login_security_host_wrong_count_hard'] = array(
134 '#type' => 'textfield',
135 '#title' => t('Maximum number of login failures before blocking a host'),
136 '#default_value' => variable_get('login_security_host_wrong_count_hard', LOGIN_SECURITY_HOST_WRONG_COUNT_HARD),
137 '#size' => 3,
138 '#maxlength' => 3,
139 '#description' => t('Enter the number of login failures a host is allowed. After that number is reached, the host will be blocked, no matter the username attempting to log in.'),
140 '#field_suffix' => '<kbd>'. t('Failed attempts') .'</kbd>'
141 );
142
143 $form['login_messages'] = array(
144 '#type' => 'fieldset',
145 '#title' => t('Notifications'),
146 );
147 $form['login_messages']['login_security_disable_core_login_error'] = array(
148 '#type' => 'checkbox',
149 '#title' => t('Disable login failure error message'),
150 '#description' => t('Sorry, unrecognized username or password. Have you forgotten your password?'),
151 '#default_value' => variable_get('login_security_disable_core_login_error', LOGIN_SECURITY_DISABLE_CORE_LOGIN_ERROR)
152 );
153 $form['login_messages']['login_security_notice_attempts_available'] = array(
154 '#type' => 'checkbox',
155 '#title' => t('Notify the user about the number of remaining login attempts'),
156 '#default_value' => variable_get('login_security_notice_attempts_available', LOGIN_SECURITY_NOTICE_ATTEMPTS_AVAILABLE),
157 '#description' => t('Security tip: If you enable this option, try to not disclose as much of your login policies as possible in the message shown on any failed login attempt.'),
158 );
159 $form['login_messages']['login_security_last_login_timestamp'] = array(
160 '#type' => 'checkbox',
161 '#title' => t('Display last login timestamp'),
162 '#description' => t('The last login timestamp will be displayed as a status message when users login.'),
163 '#default_value' => variable_get('login_security_last_login_timestamp', 0)
164 );
165 $form['login_messages']['login_security_last_access_timestamp'] = array(
166 '#type' => 'checkbox',
167 '#title' => t('Display last access timestamp'),
168 '#description' => t('The last access timestamp will be displayed as a status message when users login.'),
169 '#default_value' => variable_get('login_security_last_access_timestamp', 0)
170 );
171 $form['login_messages']['login_security_user_blocked_email'] = array(
172 '#type' => 'checkbox',
173 '#title' => t('Send email message to the admin (uid 1) when a user is blocked'),
174 '#default_value' => variable_get('login_security_user_blocked_email', LOGIN_SECURITY_USER_BLOCKED_EMAIL),
175 );
176
177
178 $form['login_security']['Notifications'] = array(
179 '#type' => 'fieldset',
180 '#title' => t('Edit notification texts'),
181 '#weight' => 3,
182 '#collapsible' => TRUE,
183 '#collapsed' => TRUE,
184 '#description' => t("Allowed placeholders for notifications include the following: %date, %ip, %username, %email, %uid, %site, %uri, %edit_uri, %hard_block_attempts, %soft_block_attempts, %user_block_attempts, %user_ip_current_count, %ip_current_count, %user_current_count, %tracking_time")
185 );
186
187 $form['login_security']['Notifications']['login_security_notice_attempts_message'] = array(
188 '#type' => 'textarea',
189 '#title' => t('Message to be shown on each failed login attempt'),
190 '#rows' => 2,
191 '#default_value' => variable_get('login_security_notice_attempts_message', LOGIN_SECURITY_NOTICE_ATTEMPTS_MESSAGE),
192 '#description' => t('Enter the message string to be shown if the login fails after the form is submitted. You can use any of the placeholders here.'),
193 );
194 $form['login_security']['Notifications']['login_security_host_soft_banned'] = array(
195 '#type' => 'textarea',
196 '#title' => t('Message for banned host (Soft IP ban)'),
197 '#rows' => 2,
198 '#default_value' => variable_get('login_security_host_soft_banned', LOGIN_SECURITY_HOST_SOFT_BANNED),
199 '#description' => t('Enter the soft IP ban message to be shown when a host attempts to log in too many times.'),
200 );
201 $form['login_security']['Notifications']['login_security_host_hard_banned'] = array(
202 '#type' => 'textarea',
203 '#rows' => 2,
204 '#title' => t('Message for banned host (Hard IP ban)'),
205 '#default_value' => variable_get('login_security_host_hard_banned', LOGIN_SECURITY_HOST_HARD_BANNED),
206 '#description' => t('Enter the hard IP ban message to be shown when a host attempts to log in too many times.'),
207 );
208 $form['login_security']['Notifications']['login_security_user_blocked'] = array(
209 '#type' => 'textarea',
210 '#rows' => 2,
211 '#title' => t('Message when user is blocked by uid'),
212 '#default_value' => variable_get('login_security_user_blocked', LOGIN_SECURITY_USER_BLOCKED),
213 '#description' => t('Enter the message to be shown when a user gets blocked due to enough failed login attempts.'),
214 );
215 $form['login_security']['Notifications']['login_security_user_blocked_email_subject'] = array(
216 '#type' => 'textfield',
217 '#title' => t('Email subject'),
218 '#default_value' => variable_get('login_security_user_blocked_email_subject', LOGIN_SECURITY_USER_BLOCKED_EMAIL_SUBJECT),
219 );
220 $form['login_security']['Notifications']['login_security_user_blocked_email_body'] = array(
221 '#type' => 'textarea',
222 '#title' => t('Email body'),
223 '#default_value' => variable_get('login_security_user_blocked_email_body', LOGIN_SECURITY_USER_BLOCKED_EMAIL_BODY),
224 '#description' => t('Enter the message to be sent to the administrator informing a user has been blocked.'),
225 );
226
227 return $form;
228 }
229
230 /**
231 * Previous incarnations of this code put it in hook_submit or hook_user, but since
232 * Drupal core validation updates the login timestamp, we have to set the message before
233 * it gets updated with the current login instance.
234 */
235 function login_security_set_login_timestamp($form, &$form_state) {
236 $account = user_load(array('name' => $form_state['values']['name'], 'pass' => trim($form_state['values']['pass']), 'status' => 1));
237 if (variable_get('login_security_last_login_timestamp', 0) && $account->login > 0) {
238 drupal_set_message(t('Your last login was !stamp', array('!stamp' => format_date($account->login, 'large'))), 'status');
239 }
240 if (variable_get('login_security_last_access_timestamp', 0) && $account->access > 0) {
241 drupal_set_message(t('Your last page access (site activity) was !stamp', array('!stamp' => format_date($account->access, 'large'))), 'status');
242 }
243 }
244
245 /**
246 * Temprarily deny validation to users with excess invalid login attempts.
247 *
248 * @url http://drupal.org/node/493164
249 */
250 function login_security_soft_block_validate($form, &$form_state) {
251 $variables = _login_security_get_variables_by_name(check_plain($form['name']['#value']));
252 // Check for host login attempts: Soft
253 if ($variables['%soft_block_attempts'] >= 1) {
254 if ($variables['%ip_current_count'] >= $variables['%soft_block_attempts']) {
255 // this loop is instead of doing t() because t() can only translate static strings, not variables.
256 foreach ($variables as $key => $value) {
257 $variables[$key] = theme('placeholder', $value);
258 }
259 form_set_error('submit', strtr(variable_get('login_security_host_soft_banned', LOGIN_SECURITY_HOST_SOFT_BANNED), $variables));
260 }
261 }
262 }
263
264 /**
265 * Implementation of form validate. This functions does more than just validating, but it's main
266 * Intention is to break the login form flow.
267 *
268 * @param $form_item
269 * The status of the name field in the form field after being submitted by the user.
270 *
271 */
272 function login_security_validate($form, &$form_state) {
273 // Sanitize user input
274 $name = check_plain($form_state['values']['name']);
275 // Null username should not be tracked
276 if (!strlen($name)) {
277 return;
278 }
279
280 // Save entry in security log, Username and IP Address
281 _login_security_add_event($name, check_plain(ip_address()));
282
283 // Populate variables to be used in any module message or login operation
284 $variables = _login_security_get_variables_by_name($name);
285
286 // Start with login Delay
287 if ($delay = variable_get('login_security_delay_base_time', LOGIN_SECURITY_BASE_TIME)) {
288 $secs = (variable_get('login_security_delay_increase', LOGIN_SECURITY_DELAY_INCREASE) == 1) ? intval($variables['%user_ip_current_count']-1) * intval($delay) : intval($delay);
289 if ($secs >= ini_get('max_execution_time')) {
290 $secs = ini_get('max_execution_time') - 3;
291 }
292
293 @sleep($secs);
294 }
295
296 // Check for host login attempts: Hard
297 if ($variables['%hard_block_attempts'] >= 1) {
298 if ($variables['%ip_current_count'] > $variables['%hard_block_attempts']) {
299 // block the host check_plain(ip_address())
300 login_user_block_ip($variables);
301 }
302 }
303
304 // Check for user login attempts
305 if ($variables['%user_block_attempts'] >= 1) {
306 if ($variables['%user_current_count'] > $variables['%user_block_attempts']) {
307 // Block the account $name
308 login_user_block_user_name($variables);
309 }
310 }
311
312 // at this point, they're either logged in or not by Drupal core's abuse of the validation hook to login users completely
313 global $user;
314
315 // login failed
316 if ($user->uid == 0) {
317 if (variable_get('login_security_disable_core_login_error', LOGIN_SECURITY_DISABLE_CORE_LOGIN_ERROR)) {
318 // resets the form error status so no form fields are highlighted in red
319 form_set_error(NULL, '', TRUE);
320
321 // removes "Sorry, unrecognized username or password. Have you forgotten your password?"
322 // and any other errors that might be helpful to an attacker
323 // it should not reset the attempts message because it is a warning, not an error
324 unset($_SESSION['messages']['error']);
325 }
326
327 // Should the user be advised about the remaining login attempts?
328 $notice_user = variable_get('login_security_notice_attempts_available', LOGIN_SECURITY_NOTICE_ATTEMPTS_AVAILABLE);
329 if (($notice_user == TRUE) && ($variables['%user_block_attempts'] > 0)) {
330 // this loop is instead of doing t() because t() can only translate static strings, not variables.
331 foreach ($variables as $key => $value) {
332 $variables[$key] = theme('placeholder', $value);
333 }
334 drupal_set_message(strtr(variable_get('login_security_notice_attempts_message', LOGIN_SECURITY_NOTICE_ATTEMPTS_MESSAGE), $variables), 'warning');
335 }
336 }
337 }
338
339 /**
340 * Save the login attempt in the tracking database: user name and ip address.
341 *
342 * @param $name
343 * user name to be tracked.
344 *
345 * @param $ip
346 * IP Address of the pair.
347 */
348 function _login_security_add_event($name, $ip) {
349 //Each attempt is kept for future minning of advanced bruteforcing like multiple
350 //IP or X-Forwarded-for usage and automated track data cleanup
351 $event = new stdClass();
352 $event->host = $ip;
353 $event->name = $name;
354 $event->timestamp = time();
355 drupal_write_record('login_security_track', $event);
356 }
357
358 /**
359 * Create a Deny entry for the IP address. If IP address is not especified then block current IP.
360 *
361 * @param $ip
362 * Optional. Add a deny rule in the access control to this IP Address.
363 */
364 function login_user_block_ip($variables) {
365 // There is no need to check if the host has been banned, we can't get here twice.
366 $block = new stdClass();
367 $block->mask = $variables['%ip'];
368 $block->type = 'host';
369 $block->status = 0;
370 drupal_write_record('access', $block);
371 watchdog('login_security', 'Banned IP address %ip due to security configuration.', $variables, WATCHDOG_NOTICE, l(t('edit rule'), "admin/user/rules/edit/{$block->aid}", array('query' => array('destination' => 'admin/user/rules'))));
372 form_set_error('void', t(variable_get('login_security_host_hard_banned', LOGIN_SECURITY_HOST_HARD_BANNED), $variables));
373 }
374
375 /**
376 * Block a user by user name. If no user id then block current user.
377 *
378 * @param $name
379 * Optional. The unique string identifying the user.
380 *
381 */
382 function login_user_block_user_name($variables) {
383 // If the user exists
384 if ($variables['%uid'] > 1) {
385 // Modifying the user table is not an option so it disables the user hooks. Need to do
386 // firing the hook so user_notifications can be used.
387 // db_query("UPDATE {users} SET status = 0 WHERE uid = %d", $uid);
388 $uid = $variables['%uid'];
389 $account = user_load(array("uid" => $uid));
390
391 // Block account if is active.
392 if ($account->status == 1) {
393 user_save($account, array('status' => 0), NULL);
394 // remove user from site now.
395 sess_destroy_uid($uid);
396 // The watchdog alert is set to 'user' so it will show with other blocked user messages.
397 watchdog('user', 'Blocked user %username due to security configuration.', $variables, WATCHDOG_NOTICE, l(t('edit user'), "user/{$variables['%uid']}/edit", array('query' => array('destination' => 'admin/user/user'))));
398 // Also notify the user that account has been blocked.
399 form_set_error('void', t(variable_get('login_security_user_blocked', LOGIN_SECURITY_USER_BLOCKED), $variables));
400
401 // Send admin email
402 if (variable_get('login_security_user_blocked_email', LOGIN_SECURITY_USER_BLOCKED_EMAIL)) {
403 $from = variable_get('site_mail', ini_get('sendmail_from'));
404 $admin_mail = db_result(db_query("SELECT mail FROM {users} WHERE uid = 1"));
405 $subject = strtr(variable_get('login_security_user_blocked_email_subject', LOGIN_SECURITY_USER_BLOCKED_EMAIL_SUBJECT), $variables);
406 $body = strtr(variable_get('login_security_user_blocked_email_mody', LOGIN_SECURITY_USER_BLOCKED_EMAIL_BODY), $variables);
407
408 return drupal_mail('login_security', 'notify', $admin_mail, language_default(), $variables, $from, TRUE);
409 }
410 }
411
412 }
413 }
414
415
416 /**
417 * Helper function to get the variable array for the messages.
418 */
419 function _login_security_get_variables_by_name($name) {
420 $account = user_load(array("name" => $name));
421 $ipaddress = check_plain(ip_address());
422 global $base_url;
423 $variables = array(
424 '%date' => format_date(time()),
425 '%ip' => $ipaddress,
426 '%username' => $account->name,
427 '%email' => $account->mail,
428 '%uid' => $account->uid,
429 '%site' => variable_get('site_name', 'drupal'),
430 '%uri' => $base_url,
431 '%edit_uri' => url('user/'. $account->uid .'/edit', array('absolute' => TRUE)),
432 '%hard_block_attempts' => variable_get('login_security_host_wrong_count_hard', LOGIN_SECURITY_HOST_WRONG_COUNT_HARD),
433 '%soft_block_attempts' => variable_get('login_security_host_wrong_count', LOGIN_SECURITY_USER_WRONG_COUNT),
434 '%user_block_attempts' => variable_get('login_security_user_wrong_count', LOGIN_SECURITY_USER_WRONG_COUNT),
435 '%user_ip_current_count' => db_result(db_query("SELECT COUNT(id) FROM {login_security_track} WHERE name = '%s' AND host = '%s'", $name, $ipaddress)),
436 '%ip_current_count' => db_result(db_query("SELECT COUNT(id) FROM {login_security_track} WHERE host = '%s'", $ipaddress)),
437 '%user_current_count' => db_result(db_query("SELECT COUNT(id) FROM {login_security_track} WHERE name = '%s'", $name)),
438 '%tracking_time' => variable_get('login_security_track_time', LOGIN_SECURITY_TRACK_TIME),
439 );
440 return $variables;
441 }
442
443 function login_security_mail($key, &$message, $variables) {
444 switch ($key) {
445 case 'notify':
446 $message['subject'] = strtr(variable_get('login_security_user_blocked_email_subject', LOGIN_SECURITY_USER_BLOCKED_EMAIL_SUBJECT), $variables);
447 $message['body'] = strtr(variable_get('login_security_user_blocked_email_mody', LOGIN_SECURITY_USER_BLOCKED_EMAIL_BODY), $variables);
448 break;
449 }
450 }
451

  ViewVC Help
Powered by ViewVC 1.1.2