diff --git a/app/User.php b/app/User.php index f945e5ffe..86b662937 100644 --- a/app/User.php +++ b/app/User.php @@ -2,6 +2,7 @@ namespace Gazelle; +use Gazelle\Util\Irc; use Gazelle\Util\Mail; class User extends BaseObject { @@ -926,6 +927,49 @@ public function flush() { ]); } + public function recordEmailChange(string $newEmail, string $ipaddr): int { + $this->db->prepared_query(" + INSERT INTO users_history_emails + (UserID, Email, IP, useragent) + VALUES (?, ?, ?, ?) + ", $this->id, $newEmail, $ipaddr, $_SERVER['HTTP_USER_AGENT'] + ); + Irc::sendRaw("PRIVMSG " . $this->username() + . " :Security alert: Your email address was changed via $ipaddr with {$_SERVER['HTTP_USER_AGENT']}. Not you? Contact staff ASAP."); + (new Mail)->send($this->email(), 'Email address changed information for ' . SITE_NAME, + $this->twig->render('email/email-address-change.twig', [ + 'ipaddr' => $ipaddr, + 'new_email' => $newEmail, + 'now' => Date('Y-m-d H:i:s'), + 'user_agent' => $_SERVER['HTTP_USER_AGENT'], + 'username' => $this->username(), + ]) + ); + $this->cache->delete_value('user_email_count_' . $this->id); + return $this->db->affected_rows(); + } + + public function recordPasswordChange(string $ipaddr): int { + $this->db->prepared_query(" + INSERT INTO users_history_passwords + (UserID, ChangerIP, useragent) + VALUES (?, ?, ?) + ", $this->id, $ipaddr, $_SERVER['HTTP_USER_AGENT'] + ); + Irc::sendRaw("PRIVMSG " . $this->username() + . " :Security alert: Your password was changed via $ipaddr with {$_SERVER['HTTP_USER_AGENT']}. Not you? Contact staff ASAP."); + (new Mail)->send($this->email(), 'Password changed information for ' . SITE_NAME, + $this->twig->render('email/password-change.twig', [ + 'ipaddr' => $ipaddr, + 'now' => Date('Y-m-d H:i:s'), + 'user_agent' => $_SERVER['HTTP_USER_AGENT'], + 'username' => $this->username(), + ]) + ); + $this->cache->delete_value('user_pw_count_' . $this->id); + return $this->db->affected_rows(); + } + public function remove() { $this->db->prepared_query(" DELETE FROM users_main WHERE ID = ? @@ -1148,9 +1192,9 @@ public function updatePassword(string $pw, string $ipaddr): bool { } $this->db->prepared_query(' INSERT INTO users_history_passwords - (UserID, ChangerIP, ChangeTime) - VALUES (?, ?, now()) - ', $this->id, $ipaddr + (UserID, ChangerIP, useragent) + VALUES (?, ?, ?) + ', $this->id, $ipaddr, $_SERVER['HTTP_USER_AGENT'] ); if ($this->db->affected_rows() !== 1) { $this->db->rollback(); @@ -1164,7 +1208,8 @@ public function updatePassword(string $pw, string $ipaddr): bool { public function passwordHistory(): array { $this->db->prepared_query(" SELECT ChangeTime AS date, - ChangerIP AS ipaddr + ChangerIP AS ipaddr, + useragent FROM users_history_passwords WHERE UserID = ? ORDER BY ChangeTime DESC @@ -1265,7 +1310,7 @@ public function resetIpHistory(): int { ); $n += $this->db->affected_rows(); $this->db->prepared_query(" - UPDATE users_history_passwords SET ChangerIP = '' WHERE UserID = ? + UPDATE users_history_passwords SET ChangerIP = '', useragent = 'reset-ip-history' WHERE UserID = ? ", $this->id ); $n += $this->db->affected_rows(); @@ -1291,8 +1336,8 @@ public function resetEmailHistory(string $email, string $ipaddr): bool { ); $this->db->prepared_query(" INSERT INTO users_history_emails - (UserID, Email, IP) - VALUES (?, ?, ?) + (UserID, Email, IP, useragent) + VALUES (?, ?, ?, 'email-reset') ", $this->id, $email, $ipaddr ); $this->db->prepared_query(" @@ -1644,13 +1689,14 @@ public function collageUnreadCount(): int { /** * Email history * - * @return array [email address, ip, date] + * @return array [email address, ip, date, useragent] */ public function emailHistory(): array { $this->db->prepared_query(" SELECT h.Email, h.Time, - h.IP + h.IP, + h.useragent FROM users_history_emails AS h WHERE h.UserID = ? ORDER BY h.Time DESC @@ -1671,7 +1717,8 @@ public function emailDuplicateHistory(): array { Email AS email, UserID AS user_id, Time AS created, - IP AS ipv4 + IP AS ipv4, + useragent FROM users_history_emails AS uhe WHERE uhe.UserID != ? AND uhe.Email in (SELECT DISTINCT Email FROM users_history_emails WHERE UserID = ?) diff --git a/app/UserCreator.php b/app/UserCreator.php index b04d0a037..b9317a57f 100644 --- a/app/UserCreator.php +++ b/app/UserCreator.php @@ -150,9 +150,9 @@ public function create() { foreach ($this->email as $e) { $this->db->prepared_query(' INSERT INTO users_history_emails - (UserID, Email, IP, Time) + (UserID, Email, IP, useragent, Time) VALUES (?, ?, ?, now() - INTERVAL ? SECOND) - ', $this->id, $e, $this->ipaddr, $past-- + ', $this->id, $e, $this->ipaddr, $_SERVER['HTTP_USER_AGENT'], $past-- ); } diff --git a/boris b/boris index c49e400fc..6221d3458 100755 --- a/boris +++ b/boris @@ -22,6 +22,7 @@ */ define('BORIS', 1); +$_SERVER['HTTP_USER_AGENT'] = 'boris'; require_once(__DIR__ . '/classes/config.php'); require_once(__DIR__ . '/vendor/autoload.php'); diff --git a/classes/script_start.php b/classes/script_start.php index e0eec9c16..3c5dd1b6b 100644 --- a/classes/script_start.php +++ b/classes/script_start.php @@ -187,6 +187,7 @@ function log_token_attempt(DB_MYSQL $db, int $userId): void { // Because we <3 our staff if (check_perms('site_disable_ip_history')) { $_SERVER['REMOTE_ADDR'] = '127.0.0.1'; + $_SERVER['HTTP_USER_AGENT'] = 'staff-browser'; } // IP changed diff --git a/db/migrations/20210720085440_users_history_passwords_now.php b/db/migrations/20210720085440_users_history_passwords_now.php new file mode 100644 index 000000000..7b9c91391 --- /dev/null +++ b/db/migrations/20210720085440_users_history_passwords_now.php @@ -0,0 +1,27 @@ +table('users_history_passwords') + ->changeColumn('ChangeTime', 'datetime', ['null' => false, 'default' => 'CURRENT_TIMESTAMP']) + ->addColumn('useragent', 'string', ['limit' => 768, 'encoding' => 'ascii', 'null' => true]) + ->save(); + + $this->execute("UPDATE users_history_passwords SET useragent = 'unknown'"); + + $this->table('users_history_passwords') + ->changeColumn('useragent', 'string', ['limit' => 768, 'encoding' => 'ascii', 'null' => false]) + ->save(); + } + + public function down(): void { + $this->table('users_history_passwords') + ->changeColumn('ChangeTime', 'datetime', ['null' => false]) + ->removeColumn('useragent') + ->save(); + } +} diff --git a/db/migrations/20210720093846_users_history_email_useragent.php b/db/migrations/20210720093846_users_history_email_useragent.php new file mode 100644 index 000000000..7538c00d6 --- /dev/null +++ b/db/migrations/20210720093846_users_history_email_useragent.php @@ -0,0 +1,25 @@ +table('users_history_emails') + ->addColumn('useragent', 'string', ['limit' => 768, 'encoding' => 'ascii', 'null' => true]) + ->save(); + + $this->execute("UPDATE users_history_emails SET useragent = 'unknown'"); + + $this->table('users_history_emails') + ->changeColumn('useragent', 'string', ['limit' => 768, 'encoding' => 'ascii', 'null' => false]) + ->save(); + } + + public function down(): void { + $this->table('users_history_emails') + ->removeColumn('useragent') + ->save(); + } +} diff --git a/db/seeds/InitialUserSeeder.php b/db/seeds/InitialUserSeeder.php index c687f3e30..beb45c0a7 100644 --- a/db/seeds/InitialUserSeeder.php +++ b/db/seeds/InitialUserSeeder.php @@ -96,13 +96,15 @@ public function run() { 'UserID' => $adminId, 'Email' => 'admin@example.com', 'Time' => Literal::from('now()'), - 'IP' => '127.0.0.1' + 'IP' => '127.0.0.1', + 'useragent' => 'initial-seed', ], [ 'UserID' => $userId, 'Email' => 'user@example.com', 'Time' => Literal::from('now()'), - 'IP' => '127.0.0.1' + 'IP' => '127.0.0.1', + 'useragent' => 'initial-seed', ] ])->saveData(); diff --git a/sections/user/take_edit.php b/sections/user/take_edit.php index 17c51373e..d03a5f132 100644 --- a/sections/user/take_edit.php +++ b/sections/user/take_edit.php @@ -141,19 +141,12 @@ function ($key) { $userMan->hideDonor($user); } -$CurEmail = $user->email(); -if ($CurEmail != $_POST['email']) { - // Non-admins have to authenticate to change email - if (!check_perms('users_edit_profiles') && !$user->validatePassword($_POST['cur_pass'])) { - error('You did not enter the correct password.'); +$NewEmail = false; +if ($user->email() != $_POST['email']) { + if (!$Viewer->permitted('users_edit_profiles') && !$user->validatePassword($_POST['cur_pass'])) { + error('You must enter your current password when changing your email address.'); } $NewEmail = $_POST['email']; - $DB->prepared_query(" - INSERT INTO users_history_emails - (UserID, Email, IP) - VALUES (?, ?, ?) - ", $userId, $NewEmail, $_SERVER['REMOTE_ADDR'] - ); } $ResetPassword = false; @@ -293,7 +286,6 @@ function ($key) { i.NotifyOnDeleteSeeding = ?, i.NotifyOnDeleteSnatched = ?, i.NotifyOnDeleteDownloaded = ?, - m.Email = ?, m.IRCKey = ?, m.Paranoia = ?, i.NavItems = ? @@ -312,7 +304,6 @@ function ($key) { $NotifyOnDeleteSeeding, $NotifyOnDeleteSnatched, $NotifyOnDeleteDownloaded, - $_POST['email'], $_POST['irckey'], serialize($Paranoia), $UserNavItems @@ -321,18 +312,19 @@ function ($key) { if ($ResetPassword) { $SQL .= ',m.PassHash = ?'; $Params[] = Gazelle\UserCreator::hashPassword($_POST['new_pass_1']); - $DB->prepared_query(' - INSERT INTO users_history_passwords - (UserID, ChangerIP, ChangeTime) - VALUES (?, ?, now()) - ', $userId, $LoggedUser['IP'] - ); + $user->recordPasswordChange($Viewer->ipaddr()); +} + +if ($NewEmail) { + $SQL .= ',m.email = ?'; + $Params[] = $NewEmail; + $user->recordEmailChange($NewEmail, $Viewer->ipaddr()); } if (isset($_POST['resetpasskey'])) { $OldPassKey = $user->announceKey(); $NewPassKey = randomString(); - $ChangerIP = $LoggedUser['IP']; + $ChangerIP = $Viewer->ipaddr(); $SQL .= ',m.torrent_pass = ?'; $Params[] = $NewPassKey; $DB->prepared_query(' diff --git a/sections/user/user.php b/sections/user/user.php index 5d96ccc06..90077718f 100644 --- a/sections/user/user.php +++ b/sections/user/user.php @@ -280,7 +280,7 @@ function display_rank(Gazelle\UserRank $r, string $dimension) { } if (check_perms('users_mod')) { ?> -
  • Passwords: passwordCount())?> View
  • +
  • Password history: passwordCount())?> View
  • Stats: N/A View
  • diff --git a/templates/email/email-address-change.twig b/templates/email/email-address-change.twig new file mode 100644 index 000000000..bb11a4757 --- /dev/null +++ b/templates/email/email-address-change.twig @@ -0,0 +1,21 @@ +Dear {{ username }}, + +At {{ now }} UTC, a request from {{ ipaddr }} was received +to change your email address for {{ constant('SITE_NAME') }}. + +The new address is {{ new_email }} . Please take a moment to +verify that you did not made a mistake when entering it. + +If you made this change then you may safely ignore this message. + +If you did not request this change yourself, or do not recognize the +address, then either someone has guessed your password or you left a +session logged in somewhere. In either case, you should contact us +immediately. Come to {{ constant('BOT_SERVER') }} and join the {{ constant('BOT_DISABLED_CHAN') }} channel. + +If you receive another email saying your password was changed +and you did not request it, you account has almost certainly +been taken over by a third party. + +The useragent string sent by the browser was: +{{ user_agent }} diff --git a/templates/email/password-change.twig b/templates/email/password-change.twig new file mode 100644 index 000000000..3d3de856d --- /dev/null +++ b/templates/email/password-change.twig @@ -0,0 +1,18 @@ +Dear {{ username }}, + +At {{ now }} UTC, a request from {{ ipaddr }} was received +to change your password for {{ constant('SITE_NAME') }}. + +If you made this change then you may safely ignore this message. + +If you did not request this change yourself then either someone has +guessed your existing password or you left a session logged in +somewhere. In either case, you should contact us immediately. +Come to {{ constant('BOT_SERVER') }} and join the {{ constant('BOT_DISABLED_CHAN') }} channel. + +If you receive another email saying your email address was +changed and you did not request it, you account has almost +certainly been taken over by a third party. + +The useragent string sent by the browser was: +{{ user_agent }} diff --git a/templates/user/email-history.twig b/templates/user/email-history.twig index 96466615d..24dbf5269 100644 --- a/templates/user/email-history.twig +++ b/templates/user/email-history.twig @@ -2,12 +2,15 @@

    {{ user.username }} › Email history

    +
    +

    Email History

    +
    - + {% for i in user.emailHistory %} @@ -17,6 +20,7 @@ SWI + {% endfor %}
    Email History
    Address Registered since Registered fromUseragent
    {{ i.3 }}
    @@ -33,6 +37,7 @@ Email Registered since Registered from + Useragent Enabled Donor Warned until @@ -46,6 +51,7 @@ S WI + {{ r.useragent }} {{ r.user.isEnabled ? 'yes' : 'no' }} {{ r.user.isDonor ? 'yes' : 'no' }} {{ r.user.isWarned ? r.user.endWarningDate(0) : 'no' }} diff --git a/templates/user/password-history.twig b/templates/user/password-history.twig index 3345edd93..114584ee7 100644 --- a/templates/user/password-history.twig +++ b/templates/user/password-history.twig @@ -1,16 +1,19 @@

    {{ user.username }} › Password reset history

    +
    + {% for change in list %} + {% endfor %}
    Changed IP HUseragent
    {{ change.date|time_diff }} {{ change.ipaddr }} S
    {{ resolveIpv4(change.ipaddr) }}
    {{ change.useragent }}
    - diff --git a/templates/user/setting/access.twig b/templates/user/setting/access.twig index d8c12946b..d4aa8650f 100644 --- a/templates/user/setting/access.twig +++ b/templates/user/setting/access.twig @@ -71,6 +71,7 @@
  • is 8 characters or longer, contains at least 1 lowercase and uppercase letter, and contains at least a number or symbol
  • Or is 20 characters or longer.
  • + Important! You will be logged out after changing your password. You may then log in using your new password.