Completed
Push — master ( cc22d7...565d52 )
by
unknown
42:16 queued 16s
created
lib/private/Security/Bruteforce/Throttler.php 1 patch
Indentation   +202 added lines, -202 removed lines patch added patch discarded remove patch
@@ -31,206 +31,206 @@
 block discarded – undo
31 31
  * @package OC\Security\Bruteforce
32 32
  */
33 33
 class Throttler implements IThrottler {
34
-	/** @var bool[] */
35
-	private array $hasAttemptsDeleted = [];
36
-
37
-	public function __construct(
38
-		private ITimeFactory $timeFactory,
39
-		private LoggerInterface $logger,
40
-		private IConfig $config,
41
-		private IBackend $backend,
42
-		private BruteforceAllowList $allowList,
43
-	) {
44
-	}
45
-
46
-	/**
47
-	 * {@inheritDoc}
48
-	 */
49
-	public function registerAttempt(string $action,
50
-		string $ip,
51
-		array $metadata = []): void {
52
-		// No need to log if the bruteforce protection is disabled
53
-		if (!$this->config->getSystemValueBool('auth.bruteforce.protection.enabled', true)) {
54
-			return;
55
-		}
56
-
57
-		$ipAddress = new IpAddress($ip);
58
-		if ($this->isBypassListed((string)$ipAddress)) {
59
-			return;
60
-		}
61
-
62
-		$this->logger->notice(
63
-			sprintf(
64
-				'Bruteforce attempt from "%s" detected for action "%s".',
65
-				$ip,
66
-				$action
67
-			),
68
-			[
69
-				'app' => 'core',
70
-			]
71
-		);
72
-
73
-		$this->backend->registerAttempt(
74
-			(string)$ipAddress,
75
-			$ipAddress->getSubnet(),
76
-			$this->timeFactory->getTime(),
77
-			$action,
78
-			$metadata
79
-		);
80
-	}
81
-
82
-	/**
83
-	 * Check if the IP is whitelisted
84
-	 */
85
-	public function isBypassListed(string $ip): bool {
86
-		return $this->allowList->isBypassListed($ip);
87
-	}
88
-
89
-	/**
90
-	 * {@inheritDoc}
91
-	 */
92
-	public function showBruteforceWarning(string $ip, string $action = ''): bool {
93
-		$attempts = $this->getAttempts($ip, $action);
94
-		// 4 failed attempts is the last delay below 5 seconds
95
-		return $attempts >= 4;
96
-	}
97
-
98
-	/**
99
-	 * {@inheritDoc}
100
-	 */
101
-	public function getAttempts(string $ip, string $action = '', float $maxAgeHours = 12): int {
102
-		if ($maxAgeHours > 48) {
103
-			$this->logger->error('Bruteforce has to use less than 48 hours');
104
-			$maxAgeHours = 48;
105
-		}
106
-
107
-		if ($ip === '' || isset($this->hasAttemptsDeleted[$action])) {
108
-			return 0;
109
-		}
110
-
111
-		$ipAddress = new IpAddress($ip);
112
-		if ($this->isBypassListed((string)$ipAddress)) {
113
-			return 0;
114
-		}
115
-
116
-		$maxAgeTimestamp = (int)($this->timeFactory->getTime() - 3600 * $maxAgeHours);
117
-
118
-		return $this->backend->getAttempts(
119
-			$ipAddress->getSubnet(),
120
-			$maxAgeTimestamp,
121
-			$action !== '' ? $action : null,
122
-		);
123
-	}
124
-
125
-	/**
126
-	 * {@inheritDoc}
127
-	 */
128
-	public function getDelay(string $ip, string $action = ''): int {
129
-		$attempts = $this->getAttempts($ip, $action);
130
-		return $this->calculateDelay($attempts);
131
-	}
132
-
133
-	/**
134
-	 * {@inheritDoc}
135
-	 */
136
-	public function calculateDelay(int $attempts): int {
137
-		if ($attempts === 0) {
138
-			return 0;
139
-		}
140
-
141
-		$firstDelay = 0.1;
142
-		if ($attempts > $this->config->getSystemValueInt('auth.bruteforce.max-attempts', self::MAX_ATTEMPTS)) {
143
-			// Don't ever overflow. Just assume the maxDelay time:s
144
-			return self::MAX_DELAY_MS;
145
-		}
146
-
147
-		$delay = $firstDelay * 2 ** $attempts;
148
-		if ($delay > self::MAX_DELAY) {
149
-			return self::MAX_DELAY_MS;
150
-		}
151
-		return (int)\ceil($delay * 1000);
152
-	}
153
-
154
-	/**
155
-	 * {@inheritDoc}
156
-	 */
157
-	public function resetDelay(string $ip, string $action, array $metadata): void {
158
-		// No need to log if the bruteforce protection is disabled
159
-		if (!$this->config->getSystemValueBool('auth.bruteforce.protection.enabled', true)) {
160
-			return;
161
-		}
162
-
163
-		$ipAddress = new IpAddress($ip);
164
-		if ($this->isBypassListed((string)$ipAddress)) {
165
-			return;
166
-		}
167
-
168
-		$this->backend->resetAttempts(
169
-			$ipAddress->getSubnet(),
170
-			$action,
171
-			$metadata,
172
-		);
173
-
174
-		$this->hasAttemptsDeleted[$action] = true;
175
-	}
176
-
177
-	/**
178
-	 * {@inheritDoc}
179
-	 */
180
-	public function resetDelayForIP(string $ip): void {
181
-		// No need to log if the bruteforce protection is disabled
182
-		if (!$this->config->getSystemValueBool('auth.bruteforce.protection.enabled', true)) {
183
-			return;
184
-		}
185
-
186
-		$ipAddress = new IpAddress($ip);
187
-		if ($this->isBypassListed((string)$ipAddress)) {
188
-			return;
189
-		}
190
-
191
-		$this->backend->resetAttempts($ipAddress->getSubnet());
192
-	}
193
-
194
-	/**
195
-	 * {@inheritDoc}
196
-	 */
197
-	public function sleepDelay(string $ip, string $action = ''): int {
198
-		$delay = $this->getDelay($ip, $action);
199
-		if (!$this->config->getSystemValueBool('auth.bruteforce.protection.testing')) {
200
-			usleep($delay * 1000);
201
-		}
202
-		return $delay;
203
-	}
204
-
205
-	/**
206
-	 * {@inheritDoc}
207
-	 */
208
-	public function sleepDelayOrThrowOnMax(string $ip, string $action = ''): int {
209
-		$maxAttempts = $this->config->getSystemValueInt('auth.bruteforce.max-attempts', self::MAX_ATTEMPTS);
210
-		$attempts = $this->getAttempts($ip, $action);
211
-		if ($attempts > $maxAttempts) {
212
-			$attempts30mins = $this->getAttempts($ip, $action, 0.5);
213
-			if ($attempts30mins > $maxAttempts) {
214
-				$this->logger->info('IP address blocked because it reached the maximum failed attempts in the last 30 minutes [action: {action}, attempts: {attempts}, ip: {ip}]', [
215
-					'action' => $action,
216
-					'ip' => $ip,
217
-					'attempts' => $attempts30mins,
218
-				]);
219
-				// If the ip made too many attempts within the last 30 mins we don't execute anymore
220
-				throw new MaxDelayReached('Reached maximum delay');
221
-			}
222
-
223
-			$this->logger->info('IP address throttled because it reached the attempts limit in the last 12 hours [action: {action}, attempts: {attempts}, ip: {ip}]', [
224
-				'action' => $action,
225
-				'ip' => $ip,
226
-				'attempts' => $attempts,
227
-			]);
228
-		}
229
-
230
-		if ($attempts > 0) {
231
-			return $this->calculateDelay($attempts);
232
-		}
233
-
234
-		return 0;
235
-	}
34
+    /** @var bool[] */
35
+    private array $hasAttemptsDeleted = [];
36
+
37
+    public function __construct(
38
+        private ITimeFactory $timeFactory,
39
+        private LoggerInterface $logger,
40
+        private IConfig $config,
41
+        private IBackend $backend,
42
+        private BruteforceAllowList $allowList,
43
+    ) {
44
+    }
45
+
46
+    /**
47
+     * {@inheritDoc}
48
+     */
49
+    public function registerAttempt(string $action,
50
+        string $ip,
51
+        array $metadata = []): void {
52
+        // No need to log if the bruteforce protection is disabled
53
+        if (!$this->config->getSystemValueBool('auth.bruteforce.protection.enabled', true)) {
54
+            return;
55
+        }
56
+
57
+        $ipAddress = new IpAddress($ip);
58
+        if ($this->isBypassListed((string)$ipAddress)) {
59
+            return;
60
+        }
61
+
62
+        $this->logger->notice(
63
+            sprintf(
64
+                'Bruteforce attempt from "%s" detected for action "%s".',
65
+                $ip,
66
+                $action
67
+            ),
68
+            [
69
+                'app' => 'core',
70
+            ]
71
+        );
72
+
73
+        $this->backend->registerAttempt(
74
+            (string)$ipAddress,
75
+            $ipAddress->getSubnet(),
76
+            $this->timeFactory->getTime(),
77
+            $action,
78
+            $metadata
79
+        );
80
+    }
81
+
82
+    /**
83
+     * Check if the IP is whitelisted
84
+     */
85
+    public function isBypassListed(string $ip): bool {
86
+        return $this->allowList->isBypassListed($ip);
87
+    }
88
+
89
+    /**
90
+     * {@inheritDoc}
91
+     */
92
+    public function showBruteforceWarning(string $ip, string $action = ''): bool {
93
+        $attempts = $this->getAttempts($ip, $action);
94
+        // 4 failed attempts is the last delay below 5 seconds
95
+        return $attempts >= 4;
96
+    }
97
+
98
+    /**
99
+     * {@inheritDoc}
100
+     */
101
+    public function getAttempts(string $ip, string $action = '', float $maxAgeHours = 12): int {
102
+        if ($maxAgeHours > 48) {
103
+            $this->logger->error('Bruteforce has to use less than 48 hours');
104
+            $maxAgeHours = 48;
105
+        }
106
+
107
+        if ($ip === '' || isset($this->hasAttemptsDeleted[$action])) {
108
+            return 0;
109
+        }
110
+
111
+        $ipAddress = new IpAddress($ip);
112
+        if ($this->isBypassListed((string)$ipAddress)) {
113
+            return 0;
114
+        }
115
+
116
+        $maxAgeTimestamp = (int)($this->timeFactory->getTime() - 3600 * $maxAgeHours);
117
+
118
+        return $this->backend->getAttempts(
119
+            $ipAddress->getSubnet(),
120
+            $maxAgeTimestamp,
121
+            $action !== '' ? $action : null,
122
+        );
123
+    }
124
+
125
+    /**
126
+     * {@inheritDoc}
127
+     */
128
+    public function getDelay(string $ip, string $action = ''): int {
129
+        $attempts = $this->getAttempts($ip, $action);
130
+        return $this->calculateDelay($attempts);
131
+    }
132
+
133
+    /**
134
+     * {@inheritDoc}
135
+     */
136
+    public function calculateDelay(int $attempts): int {
137
+        if ($attempts === 0) {
138
+            return 0;
139
+        }
140
+
141
+        $firstDelay = 0.1;
142
+        if ($attempts > $this->config->getSystemValueInt('auth.bruteforce.max-attempts', self::MAX_ATTEMPTS)) {
143
+            // Don't ever overflow. Just assume the maxDelay time:s
144
+            return self::MAX_DELAY_MS;
145
+        }
146
+
147
+        $delay = $firstDelay * 2 ** $attempts;
148
+        if ($delay > self::MAX_DELAY) {
149
+            return self::MAX_DELAY_MS;
150
+        }
151
+        return (int)\ceil($delay * 1000);
152
+    }
153
+
154
+    /**
155
+     * {@inheritDoc}
156
+     */
157
+    public function resetDelay(string $ip, string $action, array $metadata): void {
158
+        // No need to log if the bruteforce protection is disabled
159
+        if (!$this->config->getSystemValueBool('auth.bruteforce.protection.enabled', true)) {
160
+            return;
161
+        }
162
+
163
+        $ipAddress = new IpAddress($ip);
164
+        if ($this->isBypassListed((string)$ipAddress)) {
165
+            return;
166
+        }
167
+
168
+        $this->backend->resetAttempts(
169
+            $ipAddress->getSubnet(),
170
+            $action,
171
+            $metadata,
172
+        );
173
+
174
+        $this->hasAttemptsDeleted[$action] = true;
175
+    }
176
+
177
+    /**
178
+     * {@inheritDoc}
179
+     */
180
+    public function resetDelayForIP(string $ip): void {
181
+        // No need to log if the bruteforce protection is disabled
182
+        if (!$this->config->getSystemValueBool('auth.bruteforce.protection.enabled', true)) {
183
+            return;
184
+        }
185
+
186
+        $ipAddress = new IpAddress($ip);
187
+        if ($this->isBypassListed((string)$ipAddress)) {
188
+            return;
189
+        }
190
+
191
+        $this->backend->resetAttempts($ipAddress->getSubnet());
192
+    }
193
+
194
+    /**
195
+     * {@inheritDoc}
196
+     */
197
+    public function sleepDelay(string $ip, string $action = ''): int {
198
+        $delay = $this->getDelay($ip, $action);
199
+        if (!$this->config->getSystemValueBool('auth.bruteforce.protection.testing')) {
200
+            usleep($delay * 1000);
201
+        }
202
+        return $delay;
203
+    }
204
+
205
+    /**
206
+     * {@inheritDoc}
207
+     */
208
+    public function sleepDelayOrThrowOnMax(string $ip, string $action = ''): int {
209
+        $maxAttempts = $this->config->getSystemValueInt('auth.bruteforce.max-attempts', self::MAX_ATTEMPTS);
210
+        $attempts = $this->getAttempts($ip, $action);
211
+        if ($attempts > $maxAttempts) {
212
+            $attempts30mins = $this->getAttempts($ip, $action, 0.5);
213
+            if ($attempts30mins > $maxAttempts) {
214
+                $this->logger->info('IP address blocked because it reached the maximum failed attempts in the last 30 minutes [action: {action}, attempts: {attempts}, ip: {ip}]', [
215
+                    'action' => $action,
216
+                    'ip' => $ip,
217
+                    'attempts' => $attempts30mins,
218
+                ]);
219
+                // If the ip made too many attempts within the last 30 mins we don't execute anymore
220
+                throw new MaxDelayReached('Reached maximum delay');
221
+            }
222
+
223
+            $this->logger->info('IP address throttled because it reached the attempts limit in the last 12 hours [action: {action}, attempts: {attempts}, ip: {ip}]', [
224
+                'action' => $action,
225
+                'ip' => $ip,
226
+                'attempts' => $attempts,
227
+            ]);
228
+        }
229
+
230
+        if ($attempts > 0) {
231
+            return $this->calculateDelay($attempts);
232
+        }
233
+
234
+        return 0;
235
+    }
236 236
 }
Please login to merge, or discard this patch.