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
|
<?php
/**
* Matomo - free/libre analytics platform
*
* @link https://matomo.org
* @license http://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later
*
*/
namespace Piwik\Plugins\Login\Security;
use Piwik\Common;
use Piwik\Date;
use Piwik\Db;
use Piwik\Plugins\Login\SystemSettings;
use Piwik\Updater;
use Piwik\Version;
class BruteForceDetection {
private $minutesTimeRange;
private $maxLogAttempts;
private $table = 'brute_force_log';
private $tablePrefixed = '';
/**
* @var SystemSettings
*/
private $settings;
/**
* @var Updater
*/
private $updater;
public function __construct(SystemSettings $systemSettings)
{
$this->tablePrefixed = Common::prefixTable($this->table);
$this->settings = $systemSettings;
$this->minutesTimeRange = $systemSettings->loginAttemptsTimeRange->getValue();
$this->maxLogAttempts = $systemSettings->maxFailedLoginsPerMinutes->getValue();
$this->updater = new Updater();
}
public function isEnabled()
{
$dbSchemaVersion = $this->updater->getCurrentComponentVersion('core');
if ($dbSchemaVersion && version_compare($dbSchemaVersion, '3.8.0') == -1) {
return false; // do not enable brute force detection before the tables exist
}
return $this->settings->enableBruteForceDetection->getValue();
}
public function addFailedAttempt($ipAddress)
{
$now = $this->getNow()->getDatetime();
$db = Db::get();
$db->query('INSERT INTO '.$this->tablePrefixed.' (ip_address, attempted_at) VALUES(?,?)', array($ipAddress, $now));
}
public function isAllowedToLogin($ipAddress)
{
if ($this->settings->isBlacklistedIp($ipAddress)) {
return false;
}
if ($this->settings->isWhitelistedIp($ipAddress)) {
return true;
}
$db = Db::get();
$startTime = $this->getStartTimeRange();
$sql = 'SELECT count(*) as numLogins FROM '.$this->tablePrefixed.' WHERE ip_address = ? AND attempted_at > ?';
$numLogins = $db->fetchOne($sql, array($ipAddress, $startTime));
return empty($numLogins) || $numLogins <= $this->maxLogAttempts;
}
public function getCurrentlyBlockedIps()
{
$sql = 'SELECT ip_address
FROM ' . $this->tablePrefixed . '
WHERE attempted_at > ?
GROUP BY ip_address
HAVING count(*) > ' . (int) $this->maxLogAttempts;
$rows = Db::get()->fetchAll($sql, array($this->getStartTimeRange()));
$ips = array();
foreach ($rows as $row) {
if ($this->settings->isWhitelistedIp($row['ip_address'])) {
continue;
}
$ips[] = $row['ip_address'];
}
return $ips;
}
public function unblockIp($ip)
{
// we only delete where attempted_at was recent and keep other IPs for history purposes
Db::get()->query('DELETE FROM '.$this->tablePrefixed.' WHERE ip_address = ? and attempted_at > ?', array($ip, $this->getStartTimeRange()));
}
public function cleanupOldEntries()
{
// we delete all entries older than 7 days (or more if more attempts are logged)
$minutesAutoDelete = 10080;
$minutes = max($minutesAutoDelete, $this->minutesTimeRange);
$deleteOlderDate = $this->getDateTimeSubMinutes($minutes);
Db::get()->query('DELETE FROM '.$this->tablePrefixed.' WHERE attempted_at < ?', array($deleteOlderDate));
}
/**
* @internal tests only
*/
public function deleteAll()
{
return Db::query('DELETE FROM ' . $this->tablePrefixed);
}
/**
* @internal tests only
*/
public function getAll()
{
return Db::get()->fetchAll('SELECT * FROM ' . $this->tablePrefixed);
}
protected function getNow()
{
return Date::now();
}
private function getStartTimeRange()
{
return $this->getDateTimeSubMinutes($this->minutesTimeRange);
}
private function getDateTimeSubMinutes($minutes)
{
return $this->getNow()->subPeriod($minutes, 'minute')->getDatetime();
}
}
|