AntiBrutePlugin.php 3.5 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586
  1. <?php
  2. if (!defined('GNUSOCIAL')) { exit(1); }
  3. class AntiBrutePlugin extends Plugin {
  4. const PLUGIN_VERSION = '2.0.0';
  5. protected $failed_attempts = 0;
  6. protected $unauthed_user = null;
  7. protected $client_ip = null;
  8. const FAILED_LOGIN_IP_SECTION = 'failed_login_ip';
  9. public function initialize()
  10. {
  11. // This probably needs some work. For example with IPv6 you can easily generate new IPs...
  12. $client_ip = common_client_ip();
  13. if (!empty($client_ip)) {
  14. $this->client_ip = $client_ip[0] ?: $client_ip[1]; // [0] is proxy, [1] should be the real IP
  15. }
  16. }
  17. public function onStartCheckPassword($nickname, $password, &$authenticatedUser)
  18. {
  19. if (common_is_email($nickname)) {
  20. $this->unauthed_user = User::getKV('email', common_canonical_email($nickname));
  21. } else {
  22. $this->unauthed_user = User::getKV('nickname', Nickname::normalize($nickname));
  23. }
  24. if (!$this->unauthed_user instanceof User) {
  25. // Unknown username continue processing StartCheckPassword (maybe uninitialized LDAP user etc?)
  26. return true;
  27. }
  28. $this->failed_attempts = (int)$this->unauthed_user->getPref(self::FAILED_LOGIN_IP_SECTION, $this->client_ip);
  29. switch (true) {
  30. case $this->failed_attempts >= 5:
  31. common_log(LOG_WARNING, sprintf('Multiple failed login attempts for user %s from IP %s - brute force attack?',
  32. $this->unauthed_user->getNickname(), $this->client_ip));
  33. // 5 seconds is a good max waiting time anyway...
  34. sleep($this->failed_attempts % 5 + 1);
  35. break;
  36. case $this->failed_attempts > 0:
  37. common_debug(sprintf('Previously failed login on user %s from IP %s - sleeping %u seconds.',
  38. $this->unauthed_user->getNickname(), $this->client_ip, $this->failed_attempts));
  39. sleep($this->failed_attempts);
  40. break;
  41. default:
  42. // No sleeping if it's our first failed attempt.
  43. }
  44. return true;
  45. }
  46. public function onEndCheckPassword($nickname, $password, $authenticatedUser)
  47. {
  48. if ($authenticatedUser instanceof User) {
  49. // We'll trust this IP for this user and remove failed logins for the database..
  50. $authenticatedUser->delPref(self::FAILED_LOGIN_IP_SECTION, $this->client_ip);
  51. return true;
  52. }
  53. // See if we have an unauthed user from before. If not, it might be because the User did
  54. // not exist yet (such as autoregistering with LDAP, OpenID etc.).
  55. if ($this->unauthed_user instanceof User) {
  56. // And if the login failed, we'll increment the attempt count.
  57. common_debug(sprintf('Failed login tests for user %s from IP %s',
  58. $this->unauthed_user->getNickname(), $this->client_ip));
  59. $this->unauthed_user->setPref(self::FAILED_LOGIN_IP_SECTION, $this->client_ip, ++$this->failed_attempts);
  60. }
  61. return true;
  62. }
  63. public function onPluginVersion(array &$versions): bool
  64. {
  65. $versions[] = array('name' => 'AntiBrute',
  66. 'version' => self::PLUGIN_VERSION,
  67. 'author' => 'Mikael Nordfeldth',
  68. 'homepage' => GNUSOCIAL_ENGINE_URL,
  69. 'description' =>
  70. // TRANS: Plugin description.
  71. _m('Anti bruteforce method(s).'));
  72. return true;
  73. }
  74. }