From d36b26307d55602e2116ce3fcfa767693c266d7a Mon Sep 17 00:00:00 2001 From: fusion32 Date: Thu, 30 Oct 2025 01:03:01 -0300 Subject: [PATCH] prevent password brute force (#4) Prevent blocked IP addresses and accounts from returning anything other than a blocked error message. Previously, further attempts would first check the password which enabled brute force attacks to use the "invalid account/password" message as an oracle. --- src/query.cc | 42 ++++++++++++++++++++++++------------------ 1 file changed, 24 insertions(+), 18 deletions(-) diff --git a/src/query.cc b/src/query.cc index 827d638..b1863ad 100644 --- a/src/query.cc +++ b/src/query.cc @@ -518,17 +518,19 @@ static void CheckAccountPasswordTx(TDatabase *Database, TQuery *Query, TransactionScope Tx("CheckAccountPassword"); QUERY_STOP_IF(!Tx.Begin(Database)); + // IMPORTANT(fusion): Disallow blocked IP addresses and accounts early to + // prevent error messages from being used to brute force passwords. + int FailedLoginAttempts; + QUERY_STOP_IF(!GetIPAddressFailedLoginAttempts(Database, IPAddress, 30 * 60, &FailedLoginAttempts)); + QUERY_ERROR_IF(FailedLoginAttempts > 20, 4); + QUERY_STOP_IF(!GetAccountFailedLoginAttempts(Database, AccountID, 5 * 60, &FailedLoginAttempts)); + QUERY_ERROR_IF(FailedLoginAttempts > 10, 3); + TAccount Account; QUERY_STOP_IF(!GetAccountData(Database, AccountID, &Account)); QUERY_ERROR_IF(Account.AccountID == 0, 1); QUERY_ERROR_IF(!TestPassword(Account.Auth, sizeof(Account.Auth), Password), 2); - int FailedLoginAttempts; - QUERY_STOP_IF(!GetAccountFailedLoginAttempts(Database, Account.AccountID, 5 * 60, &FailedLoginAttempts)); - QUERY_ERROR_IF(FailedLoginAttempts > 10, 3); - QUERY_STOP_IF(!GetIPAddressFailedLoginAttempts(Database, IPAddress, 30 * 60, &FailedLoginAttempts)); - QUERY_ERROR_IF(FailedLoginAttempts > 20, 4); - QUERY_STOP_IF(!Tx.Commit()); QueryOk(Query); } @@ -558,17 +560,19 @@ static void LoginAccountTx(TDatabase *Database, TQuery *Query, TransactionScope Tx("LoginAccount"); QUERY_STOP_IF(!Tx.Begin(Database)); + // IMPORTANT(fusion): Disallow blocked IP addresses and accounts early to + // prevent error messages from being used to brute force passwords. + int FailedLoginAttempts; + QUERY_STOP_IF(!GetIPAddressFailedLoginAttempts(Database, IPAddress, 30 * 60, &FailedLoginAttempts)); + QUERY_ERROR_IF(FailedLoginAttempts > 20, 4); + QUERY_STOP_IF(!GetAccountFailedLoginAttempts(Database, AccountID, 5 * 60, &FailedLoginAttempts)); + QUERY_ERROR_IF(FailedLoginAttempts > 10, 3); + TAccount Account; QUERY_STOP_IF(!GetAccountData(Database, AccountID, &Account)); QUERY_ERROR_IF(Account.AccountID == 0, 1); QUERY_ERROR_IF(!TestPassword(Account.Auth, sizeof(Account.Auth), Password), 2); - int FailedLoginAttempts; - QUERY_STOP_IF(!GetAccountFailedLoginAttempts(Database, Account.AccountID, 5 * 60, &FailedLoginAttempts)); - QUERY_ERROR_IF(FailedLoginAttempts > 10, 3); - QUERY_STOP_IF(!GetIPAddressFailedLoginAttempts(Database, IPAddress, 30 * 60, &FailedLoginAttempts)); - QUERY_ERROR_IF(FailedLoginAttempts > 20, 4); - bool IsBanished; QUERY_STOP_IF(!IsAccountBanished(Database, Account.AccountID, &IsBanished)); QUERY_ERROR_IF(IsBanished, 5); @@ -628,6 +632,14 @@ static void LoginGameTx(TDatabase *Database, TQuery *Query, TransactionScope Tx("LoginGame"); QUERY_STOP_IF(!Tx.Begin(Database)); + // IMPORTANT(fusion): Disallow blocked IP addresses and accounts early to + // prevent error messages from being used to brute force passwords. + int FailedLoginAttempts; + QUERY_STOP_IF(!GetIPAddressFailedLoginAttempts(Database, IPAddress, 30 * 60, &FailedLoginAttempts)); + QUERY_ERROR_IF(FailedLoginAttempts > 20, 9); + QUERY_STOP_IF(!GetAccountFailedLoginAttempts(Database, AccountID, 5 * 60, &FailedLoginAttempts)); + QUERY_ERROR_IF(FailedLoginAttempts > 10, 7); + TCharacterLoginData Character; QUERY_STOP_IF(!GetCharacterLoginData(Database, CharacterName, &Character)); QUERY_ERROR_IF(Character.CharacterID == 0, 1); @@ -646,12 +658,6 @@ static void LoginGameTx(TDatabase *Database, TQuery *Query, QUERY_ERROR_IF(Account.Deleted, 8); QUERY_ERROR_IF(!TestPassword(Account.Auth, sizeof(Account.Auth), Password), 6); - int FailedLoginAttempts; - QUERY_STOP_IF(!GetAccountFailedLoginAttempts(Database, Account.AccountID, 5 * 60, &FailedLoginAttempts)); - QUERY_ERROR_IF(FailedLoginAttempts > 10, 7); - QUERY_STOP_IF(!GetIPAddressFailedLoginAttempts(Database, IPAddress, 30 * 60, &FailedLoginAttempts)); - QUERY_ERROR_IF(FailedLoginAttempts > 20, 9); - bool IsBanished; QUERY_STOP_IF(!IsAccountBanished(Database, Account.AccountID, &IsBanished)); QUERY_ERROR_IF(IsBanished, 10);