<?php namespace Shaarli\Security; use Shaarli\FileUtils; /** * Class BanManager * * Failed login attempts will store the associated IP address. * After N failed attempts, the IP will be prevented from log in for duration D. * Both N and D can be set in the configuration file. * * @package Shaarli\Security */ class BanManager { /** @var array List of allowed proxies IP */ protected $trustedProxies; /** @var int Number of allowed failed attempt before the ban */ protected $nbAttempts; /** @var int Ban duration in seconds */ protected $banDuration; /** @var string Path to the file containing IP bans and failures */ protected $banFile; /** @var string Path to the log file, used to log bans */ protected $logFile; /** @var array List of IP with their associated number of failed attempts */ protected $failures = []; /** @var array List of banned IP with their associated unban timestamp */ protected $bans = []; /** * BanManager constructor. * * @param array $trustedProxies List of allowed proxies IP * @param int $nbAttempts Number of allowed failed attempt before the ban * @param int $banDuration Ban duration in seconds * @param string $banFile Path to the file containing IP bans and failures * @param string $logFile Path to the log file, used to log bans */ public function __construct($trustedProxies, $nbAttempts, $banDuration, $banFile, $logFile) { $this->trustedProxies = $trustedProxies; $this->nbAttempts = $nbAttempts; $this->banDuration = $banDuration; $this->banFile = $banFile; $this->logFile = $logFile; $this->readBanFile(); } /** * Handle a failed login and ban the IP after too many failed attempts * * @param array $server The $_SERVER array */ public function handleFailedAttempt($server) { $ip = $this->getIp($server); // the IP is behind a trusted forward proxy, but is not forwarded // in the HTTP headers, so we do nothing if (empty($ip)) { return; } // increment the fail count for this IP if (isset($this->failures[$ip])) { $this->failures[$ip]++; } else { $this->failures[$ip] = 1; } if ($this->failures[$ip] >= $this->nbAttempts) { $this->bans[$ip] = time() + $this->banDuration; logm( $this->logFile, $server['REMOTE_ADDR'], 'IP address banned from login: '. $ip ); } $this->writeBanFile(); } /** * Remove failed attempts for the provided client. * * @param array $server $_SERVER */ public function clearFailures($server) { $ip = $this->getIp($server); // the IP is behind a trusted forward proxy, but is not forwarded // in the HTTP headers, so we do nothing if (empty($ip)) { return; } if (isset($this->failures[$ip])) { unset($this->failures[$ip]); } $this->writeBanFile(); } /** * Check whether the client IP is banned or not. * * @param array $server $_SERVER * * @return bool True if the IP is banned, false otherwise */ public function isBanned($server) { $ip = $this->getIp($server); // the IP is behind a trusted forward proxy, but is not forwarded // in the HTTP headers, so we allow the authentication attempt. if (empty($ip)) { return false; } // the user is not banned if (! isset($this->bans[$ip])) { return false; } // the user is still banned if ($this->bans[$ip] > time()) { return true; } // the ban has expired, the user can attempt to log in again if (isset($this->failures[$ip])) { unset($this->failures[$ip]); } unset($this->bans[$ip]); logm($this->logFile, $server['REMOTE_ADDR'], 'Ban lifted for: '. $ip); $this->writeBanFile(); return false; } /** * Retrieve the IP from $_SERVER. * If the actual IP is behind an allowed reverse proxy, * we try to extract the forwarded IP from HTTP headers. * * @param array $server $_SERVER * * @return string|bool The IP or false if none could be extracted */ protected function getIp($server) { $ip = $server['REMOTE_ADDR']; if (! in_array($ip, $this->trustedProxies)) { return $ip; } return getIpAddressFromProxy($server, $this->trustedProxies); } /** * Read a file containing banned IPs */ protected function readBanFile() { $data = FileUtils::readFlatDB($this->banFile); if (isset($data['failures']) && is_array($data['failures'])) { $this->failures = $data['failures']; } if (isset($data['bans']) && is_array($data['bans'])) { $this->bans = $data['bans']; } } /** * Write the banned IPs to a file */ protected function writeBanFile() { return FileUtils::writeFlatDB( $this->banFile, [ 'failures' => $this->failures, 'bans' => $this->bans, ] ); } /** * Get the Failures (for UT purpose). * * @return array */ public function getFailures() { return $this->failures; } /** * Get the Bans (for UT purpose). * * @return array */ public function getBans() { return $this->bans; } }