Verified Commit 9461f4ab authored by Lee Rowlands's avatar Lee Rowlands
Browse files

Issue #2983395 by mcdruid, narendra.rajwar27, andypost, yogeshmpawar,...

Issue #2983395 by mcdruid, narendra.rajwar27, andypost, yogeshmpawar, vijaycs85, prabha1997, swatichouhan012, amjad1233, SunnyGambino, larowlan, borisson_, anavarre, kim.pepper, alexpott: user module's flood controls should do better logging
parent 13d0584f
Loading
Loading
Loading
Loading
+18 −14
Original line number Diff line number Diff line
@@ -5,9 +5,9 @@
use Drupal\Core\Access\CsrfTokenGenerator;
use Drupal\Core\Controller\ControllerBase;
use Drupal\Core\DependencyInjection\ContainerInjectionInterface;
use Drupal\Core\Flood\FloodInterface;
use Drupal\Core\Routing\RouteProviderInterface;
use Drupal\user\UserAuthInterface;
use Drupal\user\UserFloodControlInterface;
use Drupal\user\UserInterface;
use Drupal\user\UserStorageInterface;
use Psr\Log\LoggerInterface;
@@ -39,11 +39,11 @@ class UserAuthenticationController extends ControllerBase implements ContainerIn
  const LOGGED_OUT = 0;

  /**
   * The flood controller.
   * The user flood control service.
   *
   * @var \Drupal\Core\Flood\FloodInterface
   * @var \Drupal\user\UserFloodControl
   */
  protected $flood;
  protected $userFloodControl;

  /**
   * The user storage.
@@ -97,8 +97,8 @@ class UserAuthenticationController extends ControllerBase implements ContainerIn
  /**
   * Constructs a new UserAuthenticationController object.
   *
   * @param \Drupal\Core\Flood\FloodInterface $flood
   *   The flood controller.
   * @param \Drupal\user\UserFloodControlInterface $user_flood_control
   *   The user flood control service.
   * @param \Drupal\user\UserStorageInterface $user_storage
   *   The user storage.
   * @param \Drupal\Core\Access\CsrfTokenGenerator $csrf_token
@@ -114,8 +114,12 @@ class UserAuthenticationController extends ControllerBase implements ContainerIn
   * @param \Psr\Log\LoggerInterface $logger
   *   A logger instance.
   */
  public function __construct(FloodInterface $flood, UserStorageInterface $user_storage, CsrfTokenGenerator $csrf_token, UserAuthInterface $user_auth, RouteProviderInterface $route_provider, Serializer $serializer, array $serializer_formats, LoggerInterface $logger) {
    $this->flood = $flood;
  public function __construct($user_flood_control, UserStorageInterface $user_storage, CsrfTokenGenerator $csrf_token, UserAuthInterface $user_auth, RouteProviderInterface $route_provider, Serializer $serializer, array $serializer_formats, LoggerInterface $logger) {
    if (!$user_flood_control instanceof UserFloodControlInterface) {
      @trigger_error('Passing the flood service to ' . __METHOD__ . ' is deprecated in drupal:9.1.0 and is replaced by user.flood_control in drupal:10.0.0. See https://www.drupal.org/node/3067148', E_USER_DEPRECATED);
      $user_flood_control = \Drupal::service('user.flood_control');
    }
    $this->userFloodControl = $user_flood_control;
    $this->userStorage = $user_storage;
    $this->csrfToken = $csrf_token;
    $this->userAuth = $user_auth;
@@ -140,7 +144,7 @@ public static function create(ContainerInterface $container) {
    }

    return new static(
      $container->get('flood'),
      $container->get('user.flood_control'),
      $container->get('entity_type.manager')->getStorage('user'),
      $container->get('csrf_token'),
      $container->get('user.auth'),
@@ -183,7 +187,7 @@ public function login(Request $request) {
    }

    if ($uid = $this->userAuth->authenticate($credentials['name'], $credentials['pass'])) {
      $this->flood->clear('user.http_login', $this->getLoginFloodIdentifier($request, $credentials['name']));
      $this->userFloodControl->clear('user.http_login', $this->getLoginFloodIdentifier($request, $credentials['name']));
      /** @var \Drupal\user\UserInterface $user */
      $user = $this->userStorage->load($uid);
      $this->userLoginFinalize($user);
@@ -212,10 +216,10 @@ public function login(Request $request) {

    $flood_config = $this->config('user.flood');
    if ($identifier = $this->getLoginFloodIdentifier($request, $credentials['name'])) {
      $this->flood->register('user.http_login', $flood_config->get('user_window'), $identifier);
      $this->userFloodControl->register('user.http_login', $flood_config->get('user_window'), $identifier);
    }
    // Always register an IP-based failed login event.
    $this->flood->register('user.failed_login_ip', $flood_config->get('ip_window'));
    $this->userFloodControl->register('user.failed_login_ip', $flood_config->get('ip_window'));
    throw new BadRequestHttpException('Sorry, unrecognized username or password.');
  }

@@ -354,14 +358,14 @@ protected function getRequestFormat(Request $request) {
   */
  protected function floodControl(Request $request, $username) {
    $flood_config = $this->config('user.flood');
    if (!$this->flood->isAllowed('user.failed_login_ip', $flood_config->get('ip_limit'), $flood_config->get('ip_window'))) {
    if (!$this->userFloodControl->isAllowed('user.failed_login_ip', $flood_config->get('ip_limit'), $flood_config->get('ip_window'))) {
      throw new AccessDeniedHttpException('Access is blocked because of IP based flood prevention.', NULL, Response::HTTP_TOO_MANY_REQUESTS);
    }

    if ($identifier = $this->getLoginFloodIdentifier($request, $username)) {
      // Don't allow login if the limit for this user has been reached.
      // Default is to allow 5 failed attempts every 6 hours.
      if (!$this->flood->isAllowed('user.http_login', $flood_config->get('user_limit'), $flood_config->get('user_window'), $identifier)) {
      if (!$this->userFloodControl->isAllowed('user.http_login', $flood_config->get('user_limit'), $flood_config->get('user_window'), $identifier)) {
        if ($flood_config->get('uid_only')) {
          $error_message = sprintf('There have been more than %s failed login attempts for this account. It is temporarily blocked. Try again later or request a new password.', $flood_config->get('user_limit'));
        }
+43 −0
Original line number Diff line number Diff line
<?php

namespace Drupal\user\Event;

/**
 * Defines events for the user module.
 */
final class UserEvents {

  /**
   * The name of the event fired when a login is blocked by flood control.
   *
   * This event allows modules to perform an action whenever flood control has
   * been triggered by excessive login attempts for a particular user account.
   * The event listener method receives a \Drupal\user\Event\UserFloodEvent
   * instance.
   *
   * @Event
   *
   * @see: \Drupal\user\UserFloodControl::isAllowed
   * @see: \Drupal\user\EventSubscriber\UserFloodSubscriber
   *
   * @var string
   */
  const FLOOD_BLOCKED_USER = 'user.flood_blocked_user';

  /**
   * The name of the event fired when a login is blocked by flood control.
   *
   * This event allows modules to perform an action whenever flood control has
   * been triggered by excessive login attempts from a particular IP. The event
   * listener method receives a \Drupal\user\Event\UserFloodEvent instance.
   *
   * @Event
   *
   * @see: \Drupal\user\UserFloodControl::isAllowed
   * @see: \Drupal\user\EventSubscriber\UserFloodSubscriber
   *
   * @var string
   */
  const FLOOD_BLOCKED_IP = 'user.flood_blocked_ip';

}
+165 −0
Original line number Diff line number Diff line
<?php

namespace Drupal\user\Event;

use Symfony\Component\EventDispatcher\Event;

/**
 * Provides a user flood event for event listeners.
 */
class UserFloodEvent extends Event {

  /**
   * Flood event name.
   *
   * @var string
   */
  protected $name;

  /**
   * Flood event threshold.
   *
   * @var int
   */
  protected $threshold;

  /**
   * Flood event window.
   *
   * @var int
   */
  protected $window;

  /**
   * Flood event identifier.
   *
   * @var string
   */
  protected $identifier;

  /**
   * Flood event uid.
   *
   * @var int
   */
  protected $uid = NULL;

  /**
   * Flood event IP.
   *
   * @var string
   */
  protected $ip = NULL;

  /**
   * Constructs a user flood event object.
   *
   * @param string $name
   *   The name of the flood event.
   * @param int $threshold
   *   The threshold for the flood event.
   * @param int $window
   *   The window for the flood event.
   * @param string $identifier
   *   The identifier of the flood event.
   */
  public function __construct($name, $threshold, $window, $identifier) {
    $this->name = $name;
    $this->threshold = $threshold;
    $this->window = $window;
    $this->identifier = $identifier;
    // The identifier could be a uid or an IP, or a composite of both.
    if (is_numeric($identifier)) {
      $this->uid = $identifier;
      return;
    }
    if (strpos($identifier, '-') !== FALSE) {
      list($uid, $ip) = explode('-', $identifier);
      $this->uid = $uid;
      $this->ip = $ip;
      return;
    }
    $this->ip = $identifier;
  }

  /**
   * Gets the name of the user flood event object.
   *
   * @return string
   *   The name of the flood event.
   */
  public function getName() {
    return $this->name;
  }

  /**
   * Gets the threshold for the user flood event object.
   *
   * @return int
   *   The threshold for the flood event.
   */
  public function getThreshold() {
    return $this->threshold;
  }

  /**
   * Gets the window for the user flood event object.
   *
   * @return int
   *   The window for the flood event.
   */
  public function getWindow() {
    return $this->window;
  }

  /**
   * Gets the identifier of the user flood event object.
   *
   * @return string
   *   The identifier of the flood event.
   */
  public function getIdentifier() {
    return $this->identifier;
  }

  /**
   * Gets the IP of the user flood event object.
   *
   * @return string
   *   The IP of the flood event.
   */
  public function getIp() {
    return $this->ip;
  }

  /**
   * Gets the uid of the user flood event object.
   *
   * @return int
   *   The uid of the flood event.
   */
  public function getUid() {
    return $this->uid;
  }

  /**
   * Is the user flood event associated with an IP?
   *
   * @return bool
   *   Whether the event has an IP.
   */
  public function hasIp() {
    return !empty($this->ip);
  }

  /**
   * Is the user flood event associated with a uid?
   *
   * @return bool
   *   Whether the event has a uid.
   */
  public function hasUid() {
    return !empty($this->uid);
  }

}
+72 −0
Original line number Diff line number Diff line
<?php

namespace Drupal\user\EventSubscriber;

use Drupal\user\Event\UserEvents;
use Drupal\user\Event\UserFloodEvent;
use Drupal\Core\Site\Settings;
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
use Psr\Log\LoggerInterface;

/**
 * Logs details of User Flood Control events.
 */
class UserFloodSubscriber implements EventSubscriberInterface {

  /**
   * The default logger service.
   *
   * @var \Psr\Log\LoggerInterface
   */
  protected $logger;

  /**
   * Constructs a UserFloodSubscriber.
   *
   * @param \Psr\Log\LoggerInterface $logger
   *   A logger instance.
   */
  public function __construct(LoggerInterface $logger = NULL) {
    $this->logger = $logger;
  }

  /**
   * {@inheritdoc}
   */
  public static function getSubscribedEvents() {
    $events[UserEvents::FLOOD_BLOCKED_USER][] = ['blockedUser'];
    $events[UserEvents::FLOOD_BLOCKED_IP][] = ['blockedIp'];
    return $events;
  }

  /**
   * An attempt to login has been blocked based on user name.
   *
   * @param \Drupal\user\Event\UserFloodEvent $floodEvent
   *   The flood event.
   */
  public function blockedUser(UserFloodEvent $floodEvent) {
    if (Settings::get('log_user_flood', TRUE)) {
      $uid = $floodEvent->getUid();
      if ($floodEvent->hasIp()) {
        $ip = $floodEvent->getIp();
        $this->logger->notice('Flood control blocked login attempt for uid %uid from %ip', ['%uid' => $uid, '%ip' => $ip]);
        return;
      }
      $this->logger->notice('Flood control blocked login attempt for uid %uid', ['%uid' => $uid]);
    }
  }

  /**
   * An attempt to login has been blocked based on IP.
   *
   * @param \Drupal\user\Event\UserFloodEvent $floodEvent
   *   The flood event.
   */
  public function blockedIp(UserFloodEvent $floodEvent) {
    if (Settings::get('log_user_flood', TRUE)) {
      $this->logger->notice('Flood control blocked login attempt from %ip', ['%ip' => $floodEvent->getIp()]);
    }
  }

}
+28 −18
Original line number Diff line number Diff line
@@ -2,7 +2,6 @@

namespace Drupal\user\Form;

use Drupal\Core\Flood\FloodInterface;
use Drupal\Core\Form\FormBase;
use Drupal\Core\Form\FormStateInterface;
use Drupal\Core\Render\RendererInterface;
@@ -10,7 +9,9 @@
use Drupal\user\UserAuthInterface;
use Drupal\user\UserInterface;
use Drupal\user\UserStorageInterface;
use Drupal\user\UserFloodControlInterface;
use Symfony\Component\DependencyInjection\ContainerInterface;
use Symfony\Component\HttpFoundation\Response;

/**
 * Provides a user login form.
@@ -20,11 +21,11 @@
class UserLoginForm extends FormBase {

  /**
   * The flood service.
   * The user flood control service.
   *
   * @var \Drupal\Core\Flood\FloodInterface
   * @var \Drupal\user\UserFloodControl
   */
  protected $flood;
  protected $userFloodControl;

  /**
   * The user storage.
@@ -50,8 +51,8 @@ class UserLoginForm extends FormBase {
  /**
   * Constructs a new UserLoginForm.
   *
   * @param \Drupal\Core\Flood\FloodInterface $flood
   *   The flood service.
   * @param \Drupal\user\UserFloodControlInterface $user_flood_control
   *   The user flood control service.
   * @param \Drupal\user\UserStorageInterface $user_storage
   *   The user storage.
   * @param \Drupal\user\UserAuthInterface $user_auth
@@ -59,8 +60,12 @@ class UserLoginForm extends FormBase {
   * @param \Drupal\Core\Render\RendererInterface $renderer
   *   The renderer.
   */
  public function __construct(FloodInterface $flood, UserStorageInterface $user_storage, UserAuthInterface $user_auth, RendererInterface $renderer) {
    $this->flood = $flood;
  public function __construct($user_flood_control, UserStorageInterface $user_storage, UserAuthInterface $user_auth, RendererInterface $renderer) {
    if (!$user_flood_control instanceof UserFloodControlInterface) {
      @trigger_error('Passing the flood service to ' . __METHOD__ . ' is deprecated in drupal:9.1.0 and is replaced by user.flood_control in drupal:10.0.0. See https://www.drupal.org/node/3067148', E_USER_DEPRECATED);
      $user_flood_control = \Drupal::service('user.flood_control');
    }
    $this->userFloodControl = $user_flood_control;
    $this->userStorage = $user_storage;
    $this->userAuth = $user_auth;
    $this->renderer = $renderer;
@@ -71,7 +76,7 @@ public function __construct(FloodInterface $flood, UserStorageInterface $user_st
   */
  public static function create(ContainerInterface $container) {
    return new static(
      $container->get('flood'),
      $container->get('user.flood_control'),
      $container->get('entity_type.manager')->getStorage('user'),
      $container->get('user.auth'),
      $container->get('renderer')
@@ -131,9 +136,13 @@ public function buildForm(array $form, FormStateInterface $form_state) {
   * {@inheritdoc}
   */
  public function submitForm(array &$form, FormStateInterface $form_state) {
    $account = $this->userStorage->load($form_state->get('uid'));

    // A destination was set, probably on an exception controller,
    if (empty($uid = $form_state->get('uid'))) {
      return;
    }
    $account = $this->userStorage->load($uid);

    // A destination was set, probably on an exception controller.
    if (!$this->getRequest()->request->has('destination')) {
      $form_state->setRedirect(
        'entity.user.canonical',
@@ -171,7 +180,7 @@ public function validateAuthentication(array &$form, FormStateInterface $form_st
      // independent of the per-user limit to catch attempts from one IP to log
      // in to many different user accounts.  We have a reasonably high limit
      // since there may be only one apparent IP for all users at an institution.
      if (!$this->flood->isAllowed('user.failed_login_ip', $flood_config->get('ip_limit'), $flood_config->get('ip_window'))) {
      if (!$this->userFloodControl->isAllowed('user.failed_login_ip', $flood_config->get('ip_limit'), $flood_config->get('ip_window'))) {
        $form_state->set('flood_control_triggered', 'ip');
        return;
      }
@@ -193,7 +202,7 @@ public function validateAuthentication(array &$form, FormStateInterface $form_st

        // Don't allow login if the limit for this user has been reached.
        // Default is to allow 5 failed attempts every 6 hours.
        if (!$this->flood->isAllowed('user.failed_login_user', $flood_config->get('user_limit'), $flood_config->get('user_window'), $identifier)) {
        if (!$this->userFloodControl->isAllowed('user.failed_login_user', $flood_config->get('user_limit'), $flood_config->get('user_window'), $identifier)) {
          $form_state->set('flood_control_triggered', 'user');
          return;
        }
@@ -214,20 +223,21 @@ public function validateFinal(array &$form, FormStateInterface $form_state) {
    $flood_config = $this->config('user.flood');
    if (!$form_state->get('uid')) {
      // Always register an IP-based failed login event.
      $this->flood->register('user.failed_login_ip', $flood_config->get('ip_window'));
      $this->userFloodControl->register('user.failed_login_ip', $flood_config->get('ip_window'));
      // Register a per-user failed login event.
      if ($flood_control_user_identifier = $form_state->get('flood_control_user_identifier')) {
        $this->flood->register('user.failed_login_user', $flood_config->get('user_window'), $flood_control_user_identifier);
        $this->userFloodControl->register('user.failed_login_user', $flood_config->get('user_window'), $flood_control_user_identifier);
      }

      if ($flood_control_triggered = $form_state->get('flood_control_triggered')) {
        if ($flood_control_triggered == 'user') {
          $form_state->setErrorByName('name', $this->formatPlural($flood_config->get('user_limit'), 'There has been more than one failed login attempt for this account. It is temporarily blocked. Try again later or <a href=":url">request a new password</a>.', 'There have been more than @count failed login attempts for this account. It is temporarily blocked. Try again later or <a href=":url">request a new password</a>.', [':url' => Url::fromRoute('user.pass')->toString()]));
          $message = $this->formatPlural($flood_config->get('user_limit'), 'There has been more than one failed login attempt for this account. It is temporarily blocked. Try again later or <a href=":url">request a new password</a>.', 'There have been more than @count failed login attempts for this account. It is temporarily blocked. Try again later or <a href=":url">request a new password</a>.', [':url' => Url::fromRoute('user.pass')->toString()]);
        }
        else {
          // We did not find a uid, so the limit is IP-based.
          $form_state->setErrorByName('name', $this->t('Too many failed login attempts from your IP address. This IP address is temporarily blocked. Try again later or <a href=":url">request a new password</a>.', [':url' => Url::fromRoute('user.pass')->toString()]));
          $message = $this->t('Too many failed login attempts from your IP address. This IP address is temporarily blocked. Try again later or <a href=":url">request a new password</a>.', [':url' => Url::fromRoute('user.pass')->toString()]);
        }
        $form_state->setResponse(new Response($message, 403));
      }
      else {
        // Use $form_state->getUserInput() in the error message to guarantee
@@ -251,7 +261,7 @@ public function validateFinal(array &$form, FormStateInterface $form_state) {
    elseif ($flood_control_user_identifier = $form_state->get('flood_control_user_identifier')) {
      // Clear past failures for this user so as not to block a user who might
      // log in and out more than once in an hour.
      $this->flood->clear('user.failed_login_user', $flood_control_user_identifier);
      $this->userFloodControl->clear('user.failed_login_user', $flood_control_user_identifier);
    }
  }

Loading