3030 * along with this program. If not, see <http://www.gnu.org/licenses/>.
3131 *
3232 */
33+
3334namespace OC \Security \Bruteforce ;
3435
3536use OC \Security \Normalizer \IpAddress ;
@@ -66,10 +67,12 @@ class Throttler implements IThrottler {
6667 /** @var bool[] */
6768 private $ hasAttemptsDeleted = [];
6869
69- public function __construct (IDBConnection $ db ,
70- ITimeFactory $ timeFactory ,
71- LoggerInterface $ logger ,
72- IConfig $ config ) {
70+ public function __construct (
71+ IDBConnection $ db ,
72+ ITimeFactory $ timeFactory ,
73+ LoggerInterface $ logger ,
74+ IConfig $ config
75+ ) {
7376 $ this ->db = $ db ;
7477 $ this ->timeFactory = $ timeFactory ;
7578 $ this ->logger = $ logger ;
@@ -97,7 +100,7 @@ private function getCutoff(int $expire): \DateInterval {
97100 */
98101 private function getCutoffTimestamp (float $ maxAgeHours = 12.0 ): int {
99102 return (new \DateTime ())
100- ->sub ($ this ->getCutoff ((int ) ($ maxAgeHours * 3600 )))
103+ ->sub ($ this ->getCutoff ((int )($ maxAgeHours * 3600 )))
101104 ->getTimestamp ();
102105 }
103106
@@ -108,9 +111,11 @@ private function getCutoffTimestamp(float $maxAgeHours = 12.0): int {
108111 * @param string $ip
109112 * @param array $metadata Optional metadata logged to the database
110113 */
111- public function registerAttempt (string $ action ,
112- string $ ip ,
113- array $ metadata = []): void {
114+ public function registerAttempt (
115+ string $ action ,
116+ string $ ip ,
117+ array $ metadata = []
118+ ): void {
114119 // No need to log if the bruteforce protection is disabled
115120 if (!$ this ->config ->getSystemValueBool ('auth.bruteforce.protection.enabled ' , true )) {
116121 return ;
@@ -159,7 +164,19 @@ private function isIPWhitelisted(string $ip): bool {
159164 $ keys = array_filter ($ keys , function ($ key ) {
160165 return str_starts_with ($ key , 'whitelist_ ' );
161166 });
167+ $ subnets = array_map (function ($ key ) {
168+ return $ this ->config ->getAppValue ('bruteForce ' , $ key , null );
169+ }, $ keys );
170+
171+ return $ this ->inSubnets ($ ip , $ subnets );
172+ }
162173
174+ /**
175+ * @param string $ip
176+ * @param string[] $subnets
177+ * @return bool
178+ */
179+ public function inSubnets (string $ ip , array $ subnets ): bool {
163180 if (filter_var ($ ip , FILTER_VALIDATE_IP , FILTER_FLAG_IPV4 )) {
164181 $ type = 4 ;
165182 } elseif (filter_var ($ ip , FILTER_VALIDATE_IP , FILTER_FLAG_IPV6 )) {
@@ -170,43 +187,44 @@ private function isIPWhitelisted(string $ip): bool {
170187
171188 $ ip = inet_pton ($ ip );
172189
173- foreach ($ keys as $ key ) {
174- $ cidr = $ this ->config ->getAppValue ('bruteForce ' , $ key , null );
175-
176- $ cx = explode ('/ ' , $ cidr );
177- $ addr = $ cx [0 ];
178- $ mask = (int )$ cx [1 ];
179-
180- // Do not compare ipv4 to ipv6
181- if (($ type === 4 && !filter_var ($ addr , FILTER_VALIDATE_IP , FILTER_FLAG_IPV4 )) ||
182- ($ type === 6 && !filter_var ($ addr , FILTER_VALIDATE_IP , FILTER_FLAG_IPV6 ))) {
183- continue ;
190+ foreach ($ subnets as $ cidr ) {
191+ if ($ this ->inSubnet ($ ip , $ cidr , $ type )) {
192+ return true ;
184193 }
194+ }
195+ return false ;
196+ }
185197
186- $ addr = inet_pton ($ addr );
198+ private function inSubnet (string $ ip , string $ cidr , int $ type ): bool {
199+ $ cx = explode ('/ ' , $ cidr );
200+ $ addr = $ cx [0 ];
187201
188- $ valid = true ;
189- for ($ i = 0 ; $ i < $ mask ; $ i ++) {
190- $ part = ord ($ addr [(int )($ i / 8 )]);
191- $ orig = ord ($ ip [(int )($ i / 8 )]);
202+ // Do not compare ipv4 to ipv6
203+ if (($ type === 4 && !filter_var ($ addr , FILTER_VALIDATE_IP , FILTER_FLAG_IPV4 )) ||
204+ ($ type === 6 && !filter_var ($ addr , FILTER_VALIDATE_IP , FILTER_FLAG_IPV6 ))) {
205+ return false ;
206+ }
192207
193- $ bitmask = 1 << (7 - ($ i % 8 ));
208+ $ addr = inet_pton ($ addr );
209+ if (count ($ cx ) === 1 ) {
210+ return $ ip === $ addr ;
211+ }
212+ $ mask = (int )$ cx [1 ];
194213
195- $ part = $ part & $ bitmask ;
196- $ orig = $ orig & $ bitmask ;
214+ for ($ i = 0 ; $ i < $ mask ; $ i ++) {
215+ $ part = ord ($ addr [(int )($ i / 8 )]);
216+ $ orig = ord ($ ip [(int )($ i / 8 )]);
197217
198- if ($ part !== $ orig ) {
199- $ valid = false ;
200- break ;
201- }
202- }
218+ $ bitmask = 1 << (7 - ($ i % 8 ));
203219
204- if ($ valid === true ) {
205- return true ;
220+ $ part = $ part & $ bitmask ;
221+ $ orig = $ orig & $ bitmask ;
222+
223+ if ($ part !== $ orig ) {
224+ return false ;
206225 }
207226 }
208-
209- return false ;
227+ return true ;
210228 }
211229
212230 /**
@@ -248,7 +266,7 @@ public function getAttempts(string $ip, string $action = '', float $maxAgeHours
248266 $ row = $ result ->fetch ();
249267 $ result ->closeCursor ();
250268
251- return (int ) $ row ['attempts ' ];
269+ return (int )$ row ['attempts ' ];
252270 }
253271
254272 /**
@@ -274,7 +292,7 @@ public function getDelay(string $ip, string $action = ''): int {
274292 if ($ delay > self ::MAX_DELAY ) {
275293 return self ::MAX_DELAY_MS ;
276294 }
277- return (int ) \ceil ($ delay * 1000 );
295+ return (int )\ceil ($ delay * 1000 );
278296 }
279297
280298 /**
@@ -362,4 +380,17 @@ public function sleepDelayOrThrowOnMax(string $ip, string $action = ''): int {
362380 usleep ($ delay * 1000 );
363381 return $ delay ;
364382 }
383+
384+ /**
385+ * @return array{'action': string, ip: string, count: int}[]
386+ */
387+ public function summarizeAttempts (): array {
388+ $ query = $ this ->db ->getQueryBuilder ();
389+ $ query ->select (['action ' , 'ip ' ])
390+ ->selectAlias ($ query ->func ()->count ('id ' ), 'count ' )
391+ ->from ('bruteforce_attempts ' )
392+ ->groupBy (['action ' , 'ip ' ])
393+ ->orderBy ($ query ->func ()->count ('id ' ));
394+ return $ query ->executeQuery ()->fetchAll ();
395+ }
365396}
0 commit comments