Skip to content

Commit 739c6a6

Browse files
committed
Brute force protection for admin and user login pages, vulnerability reported by @0xHamy
1 parent ea1c950 commit 739c6a6

17 files changed

+1690
-1
lines changed

admin/controller/user/login.php

+21-1
Original file line numberDiff line numberDiff line change
@@ -25,12 +25,18 @@
2525
use \Vvveb\System\Functions\Str;
2626
use function Vvveb\__;
2727
use function Vvveb\setLanguage;
28+
use Vvveb\Sql\Admin_Failed_LoginSQL;
2829
use Vvveb\System\Event;
2930
use Vvveb\System\User\Admin;
3031
use Vvveb\System\Validator;
3132

3233
#[\AllowDynamicProperties]
3334
class Login {
35+
//failed attemps per minute Y-m-d H:i:00
36+
protected $failedTimeInterval = 'Y-m-d H:00:00'; //failed attemps per hour
37+
38+
protected $failedCount = 10; //the number of failures before account is locked
39+
3440
protected function redirect($url = '/', $parameters = []) {
3541
$redirect = \Vvveb\url($url, $parameters);
3642

@@ -109,7 +115,19 @@ function index() {
109115
if (strpos($user, '@')) {
110116
$loginData['email'] = $user;
111117
} else {
112-
$loginData['user'] = $user;
118+
$loginData['username'] = $user;
119+
}
120+
121+
$failedLogin = new Admin_Failed_LoginSQL();
122+
$date = date($this->failedTimeInterval);
123+
$lastIp = $_SERVER['REMOTE_ADDR'] ?? '';
124+
$failedAttemps = $failedLogin->get(['updated_at' => $date, 'status' => 1] + $loginData);
125+
126+
if ($failedAttemps && ($failedAttemps['count'] > $this->failedCount)) {
127+
$this->view->errors = [__('Too many login attempts, try again in one hour!')];
128+
$failedLogin->logFailed(['last_ip' => $lastIp, 'updated_at' => $date] + $loginData);
129+
130+
return;
113131
}
114132

115133
$loginData['password'] = $this->request->post['password'];
@@ -135,6 +153,8 @@ function index() {
135153
//user not found or wrong password
136154
$this->view->errors = [__('Authentication failed, wrong email or password!')];
137155
$this->session->set('csrf', Str::random());
156+
//increment failed attempts
157+
$failedLogin->logFailed(['last_ip' => $lastIp, 'updated_at' => $date] + $loginData);
138158
}
139159
}
140160
} else {
+263
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,263 @@
1+
-- admin_failed_login
2+
3+
-- get all entries
4+
5+
CREATE PROCEDURE getAll(
6+
IN start INT,
7+
IN limit INT,
8+
IN user_id INT,
9+
IN count INT,
10+
IN updated_at CHAR,
11+
12+
-- return array of admin_failed_login
13+
OUT fetch_all,
14+
-- return admin_failed_login count for count query
15+
OUT fetch_one
16+
)
17+
BEGIN
18+
19+
SELECT * FROM admin_failed_login WHERE 1 = 1
20+
21+
@IF isset(:user_id) AND !empty(:user_id)
22+
THEN
23+
AND admin_failed_login.user_id = :user_id
24+
END @IF
25+
26+
@IF isset(:count) AND !empty(:count)
27+
THEN
28+
AND admin_failed_login.count > :count
29+
END @IF
30+
31+
@IF isset(:updated_at) AND !empty(:updated_at)
32+
THEN
33+
AND admin_failed_login.updated_at = :updated_at
34+
END @IF
35+
36+
ORDER BY admin_failed_login.admin_id, admin_failed_login.updated_at
37+
38+
-- limit
39+
@IF isset(:limit)
40+
THEN
41+
@SQL_LIMIT(:start, :limit)
42+
END @IF;
43+
44+
-- SELECT FOUND_ROWS() as count;
45+
SELECT count(*) FROM (
46+
47+
@SQL_COUNT(admin_failed_login.user_id, user) -- this takes previous query removes limit and replaces select columns with parameter user_id
48+
49+
) as count;
50+
51+
END
52+
53+
-- get user information
54+
55+
CREATE PROCEDURE get(
56+
IN admin_id INT,
57+
IN updated_at CHAR,
58+
IN count INT,
59+
IN username CHAR,
60+
IN email CHAR,
61+
IN status INT,
62+
IN role_id INT,
63+
64+
OUT fetch_row
65+
)
66+
BEGIN
67+
68+
SELECT _.* FROM admin_failed_login AS _
69+
LEFT JOIN admin ON (admin.admin_id = _.admin_id)
70+
71+
WHERE 1 = 1
72+
73+
74+
@IF isset(:admin_id) AND !empty(:admin_id)
75+
THEN
76+
AND _.admin_id = :admin_id
77+
END @IF
78+
79+
@IF isset(:count) AND !empty(:count)
80+
THEN
81+
AND _.count > :count
82+
END @IF
83+
84+
@IF isset(:updated_at) AND !empty(:updated_at)
85+
THEN
86+
AND _.updated_at = :updated_at
87+
END @IF
88+
89+
@IF isset(:username)
90+
THEN
91+
AND admin.username = :username
92+
END @IF
93+
94+
@IF isset(:email)
95+
THEN
96+
AND admin.email = :email
97+
END @IF
98+
99+
@IF isset(:status)
100+
THEN
101+
AND admin.status = :status
102+
END @IF
103+
104+
@IF isset(:role_id)
105+
THEN
106+
AND admin.role_id = :role_id
107+
END @IF
108+
109+
LIMIT 1;
110+
111+
END
112+
113+
114+
115+
-- Add new failed attempt
116+
117+
CREATE PROCEDURE logFailed(
118+
IN admin_id INT,
119+
IN username CHAR,
120+
IN updated_at CHAR,
121+
IN last_ip INT,
122+
OUT fetch_one,
123+
OUT insert_id
124+
)
125+
BEGIN
126+
127+
SELECT admin_id FROM admin WHERE admin.status = 1
128+
129+
@IF isset(:admin_id) AND !empty(:admin_id)
130+
THEN
131+
AND admin_id = :admin_id
132+
END @IF
133+
134+
@IF isset(:username)
135+
THEN
136+
AND username = :username
137+
END @IF
138+
139+
@IF isset(:email)
140+
THEN
141+
AND email = :email
142+
END @IF
143+
144+
@IF isset(:admin_id)
145+
THEN
146+
AND admin_id = :admin_id
147+
END @IF
148+
149+
LIMIT 1;
150+
151+
@IF isset(@result.admin)
152+
THEN
153+
154+
INSERT INTO admin_failed_login
155+
156+
( `admin_id`, `updated_at`, `last_ip`)
157+
158+
VALUES ( @result.admin, :updated_at, :last_ip )
159+
160+
DUPLICATE KEY UPDATE count = count + 1, last_ip = :last_ip
161+
162+
END @IF;
163+
END
164+
165+
-- Add new admin_failed_login
166+
167+
CREATE PROCEDURE add(
168+
IN admin_failed_login ARRAY,
169+
OUT insert_id
170+
)
171+
BEGIN
172+
173+
-- allow only table fields and set defaults for missing values
174+
@FILTER(:admin_failed_login, admin_failed_login)
175+
176+
INSERT INTO admin_failed_login
177+
178+
( @KEYS(:admin_failed_login) )
179+
180+
VALUES ( :admin_failed_login )
181+
END
182+
183+
184+
-- Update admin_failed_login
185+
186+
CREATE PROCEDURE edit(
187+
IN user CHAR,
188+
IN email CHAR,
189+
IN admin_id INT,
190+
IN admin_failed_login ARRAY,
191+
IN role_id INT,
192+
OUT affected_rows
193+
)
194+
BEGIN
195+
-- allow only table fields and set defaults for missing values
196+
@FILTER(:admin_failed_login, admin_failed_login)
197+
198+
UPDATE admin_failed_login
199+
200+
SET @LIST(:admin_failed_login)
201+
202+
WHERE
203+
204+
@IF isset(:email)
205+
THEN
206+
email = :email
207+
END @IF
208+
209+
@IF isset(:admin_failed_login_id)
210+
THEN
211+
admin_failed_login_id = :admin_failed_login_id
212+
END @IF
213+
214+
@IF isset(:username)
215+
THEN
216+
username = :username
217+
END @IF
218+
END
219+
220+
-- delete admin_failed_login
221+
222+
PROCEDURE delete(
223+
IN user_id INT,
224+
IN updated_at CHAR,
225+
IN count INT,
226+
227+
OUT affected_rows
228+
)
229+
BEGIN
230+
231+
DELETE FROM admin_failed_login WHERE admin_failed_login_id IN (:admin_failed_login_id);
232+
233+
END
234+
235+
-- set role
236+
237+
CREATE PROCEDURE setRole(
238+
IN admin_failed_login_id INT,
239+
IN role CHAR,
240+
IN role_id INT
241+
OUT insert_id
242+
)
243+
BEGIN
244+
245+
246+
UPDATE admin_failed_login
247+
248+
SET
249+
250+
@IF isset(:role_id)
251+
THEN
252+
role_id = :role_id
253+
END @IF
254+
255+
256+
@IF isset(:role)
257+
THEN
258+
role_id = (SELECT role_id FROM roles WHERE name = :role)
259+
END @IF
260+
261+
262+
WHERE admin_failed_login_id = :admin_failed_login_id
263+
END

0 commit comments

Comments
 (0)