aboutsummaryrefslogtreecommitdiffhomepage
path: root/application/security/BanManager.php
blob: 68190c54ffd6da311382b24f5cd78af9229a9f67 (plain) (blame)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
<?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;
    }
}