check username validity before attempting to find user

This commit is contained in:
Spine
2023-06-11 09:58:30 +00:00
parent ef1f6ecee6
commit 7f2577b616
5 changed files with 52 additions and 50 deletions

View File

@@ -21,8 +21,8 @@ help:
echo ' mysqldump - dump mysql database from docker to misc/gazelle.sql'
echo ' ocelot-reload-conf - signal Ocelot to reload its configuration'
echo ' ocelot-reload-db - signal Ocelot to reload from database'
echo ' phpstan-analyse - run phpstan over the code
echo ' phpstan-baseline - generate a new phpstan baseline
echo ' phpstan-analyse - run phpstan over the code'
echo ' phpstan-baseline - generate a new phpstan baseline'
echo ' test - run all linters and unit test suite'
echo ' twig-flush - purge the Twig cache'
echo ' update - pull from git and run production composer install'

View File

@@ -51,7 +51,7 @@ class Login extends Base {
bool $persistent = false,
string $twofa = '',
): ?User {
$this->username = $username;
$this->username = trim($username);
$this->password = $password;
$this->watch = $watch;
$this->persistent = $persistent;
@@ -90,6 +90,10 @@ class Login extends Base {
protected function attemptLogin(): ?User {
// we have all we need to go forward
$userMan = new Manager\User;
if (!preg_match(USERNAME_REGEXP, $this->username)) {
$this->error = self::ERR_CREDENTIALS;
return null;
}
$user = $userMan->findByUsername($this->username);
if (is_null($user)) {
$this->error = self::ERR_CREDENTIALS;

View File

@@ -3,17 +3,15 @@
namespace Gazelle;
class LoginWatch extends Base {
protected $id;
protected $ipaddr;
protected $userId = 0;
protected int $id;
protected int $userId = 0;
public function __construct(string $ipaddr) {
$this->ipaddr = $ipaddr;
$this->id = self::$db->scalar("
public function __construct(protected string $ipaddr) {
$this->id = (int)self::$db->scalar("
SELECT ID FROM login_attempts WHERE IP = ?
", $this->ipaddr
);
if (is_null($this->id) && $ipaddr != '0.0.0.0') {
if (!$this->id && $ipaddr != '0.0.0.0') {
self::$db->prepared_query("
INSERT INTO login_attempts
(IP, UserID)
@@ -48,7 +46,7 @@ class LoginWatch extends Base {
capture = ?
WHERE ID = ?
', $seen ? 60 : LOGIN_ATTEMPT_BACKOFF[min($this->nrAttempts(), count(LOGIN_ATTEMPT_BACKOFF)-1)],
$this->userId, substr($username, 0, 20), $this->id
$this->userId, substr(urlencode($username), 0, 20), $this->id
);
return self::$db->affected_rows();
}
@@ -74,17 +72,18 @@ class LoginWatch extends Base {
* When does the login ban expire?
*/
public function bannedUntil(): ?string {
return self::$db->scalar("
$until = self::$db->scalar("
SELECT BannedUntil FROM login_attempts WHERE ID = ?
", $this->id
);
return $until ? (string)$until : null;
}
/**
* When does the login ban expire?
*/
public function bannedEpoch(): int {
return strtotime($this->bannedUntil()) ?? 0;
public function bannedEpoch(): int|false {
return strtotime($this->bannedUntil());
}
/**
@@ -138,7 +137,7 @@ class LoginWatch extends Base {
* Get total login failures
*/
public function activeTotal(): int {
return self::$db->scalar("
return (int)self::$db->scalar("
SELECT count(*)
FROM login_attempts w
WHERE (w.BannedUntil > now() OR w.LastAttempt > now() - INTERVAL 6 HOUR)

View File

@@ -895,41 +895,6 @@ parameters:
count: 1
path: ../app/LogfileSummary.php
-
message: "#^Expression on left side of \\?\\? is not nullable\\.$#"
count: 1
path: ../app/LoginWatch.php
-
message: "#^Method Gazelle\\\\LoginWatch\\:\\:activeTotal\\(\\) should return int but returns bool\\|float\\|int\\|string\\|null\\.$#"
count: 1
path: ../app/LoginWatch.php
-
message: "#^Method Gazelle\\\\LoginWatch\\:\\:bannedEpoch\\(\\) should return int but returns int\\|false\\.$#"
count: 1
path: ../app/LoginWatch.php
-
message: "#^Method Gazelle\\\\LoginWatch\\:\\:bannedUntil\\(\\) should return string\\|null but returns bool\\|float\\|int\\|string\\|null\\.$#"
count: 1
path: ../app/LoginWatch.php
-
message: "#^Property Gazelle\\\\LoginWatch\\:\\:\\$id has no type specified\\.$#"
count: 1
path: ../app/LoginWatch.php
-
message: "#^Property Gazelle\\\\LoginWatch\\:\\:\\$ipaddr has no type specified\\.$#"
count: 1
path: ../app/LoginWatch.php
-
message: "#^Property Gazelle\\\\LoginWatch\\:\\:\\$userId has no type specified\\.$#"
count: 1
path: ../app/LoginWatch.php
-
message: "#^Method Gazelle\\\\Manager\\\\Applicant\\:\\:create\\(\\) has no return type specified\\.$#"
count: 1

View File

@@ -238,4 +238,38 @@ class UserTest extends TestCase {
$this->assertEquals(2, $warned->clear(), 'utest-warn-clear');
$this->assertFalse($warned->isWarned(), 'utest-warn-final');
}
public function testLogin(): void {
$ipaddr = implode('.', ['127', random_int(0, 255), random_int(0, 255), random_int(0, 255)]);
$_SERVER['REMOTE_ADDR'] = $ipaddr;
$watch = new \Gazelle\LoginWatch($ipaddr);
$this->assertEquals(1, $watch->nrAttempts(), 'loginwatch-init-attempt');
$this->assertEquals(0, $watch->nrBans(), 'loginwatch-init-ban');
$login = new \Gazelle\Login;
$result = $login->login(
username: 'email@example.com',
password: 'password',
watch: $watch,
);
$this->assertNull($result, 'login-bad-username');
$this->assertEquals($login->error(), \Gazelle\Login::ERR_CREDENTIALS, 'login-error-username');
$this->assertEquals('email@example.com', $login->username(), 'login-username');
$this->assertEquals($ipaddr, $login->ipaddr(), 'login-ipaddr');
$this->assertFalse($login->persistent(), 'login-persistent');
$this->assertEquals(2, $watch->nrAttempts(), 'loginwatch-attempt');
$result = $login->login(
username: $this->user->username(),
password: 'password',
watch: $watch,
);
$this->assertNull($result, 'login-bad-password');
$this->assertEquals($login->error(), \Gazelle\Login::ERR_CREDENTIALS, 'login-error-password');
$this->assertEquals(3, $watch->nrAttempts(), 'loginwatch-more-attempt');
$this->assertGreaterThan(0, count($watch->activeList('1', 'ASC', 10, 0)), 'loginwatch-active-list');
$this->assertGreaterThan(0, $watch->clearAttempts(), 'loginwatch-clear');
$this->assertEquals(0, $watch->nrAttempts(), 'loginwatch-no-attempts');
}
}