Skip to content

Commit

Permalink
issue an alert on change of email or password
Browse files Browse the repository at this point in the history
  • Loading branch information
Spine authored and itismadness committed Aug 15, 2021
1 parent 2ea52b8 commit e981041
Show file tree
Hide file tree
Showing 14 changed files with 181 additions and 37 deletions.
67 changes: 57 additions & 10 deletions app/User.php
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

namespace Gazelle;

use Gazelle\Util\Irc;
use Gazelle\Util\Mail;

class User extends BaseObject {
Expand Down Expand Up @@ -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 = ?
Expand Down Expand Up @@ -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();
Expand All @@ -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
Expand Down Expand Up @@ -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();
Expand All @@ -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("
Expand Down Expand Up @@ -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
Expand All @@ -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 = ?)
Expand Down
4 changes: 2 additions & 2 deletions app/UserCreator.php
Original file line number Diff line number Diff line change
Expand Up @@ -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--
);
}

Expand Down
1 change: 1 addition & 0 deletions boris
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@
*/

define('BORIS', 1);
$_SERVER['HTTP_USER_AGENT'] = 'boris';

require_once(__DIR__ . '/classes/config.php');
require_once(__DIR__ . '/vendor/autoload.php');
Expand Down
1 change: 1 addition & 0 deletions classes/script_start.php
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
27 changes: 27 additions & 0 deletions db/migrations/20210720085440_users_history_passwords_now.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
<?php
declare(strict_types=1);

use Phinx\Migration\AbstractMigration;

final class UsersHistoryPasswordsNow extends AbstractMigration
{
public function up(): void {
$this->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();
}
}
25 changes: 25 additions & 0 deletions db/migrations/20210720093846_users_history_email_useragent.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
<?php
declare(strict_types=1);

use Phinx\Migration\AbstractMigration;

final class UsersHistoryEmailUseragent extends AbstractMigration
{
public function up(): void {
$this->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();
}
}
6 changes: 4 additions & 2 deletions db/seeds/InitialUserSeeder.php
Original file line number Diff line number Diff line change
Expand Up @@ -96,13 +96,15 @@ public function run() {
'UserID' => $adminId,
'Email' => '[email protected]',
'Time' => Literal::from('now()'),
'IP' => '127.0.0.1'
'IP' => '127.0.0.1',
'useragent' => 'initial-seed',
],
[
'UserID' => $userId,
'Email' => '[email protected]',
'Time' => Literal::from('now()'),
'IP' => '127.0.0.1'
'IP' => '127.0.0.1',
'useragent' => 'initial-seed',
]
])->saveData();

Expand Down
32 changes: 12 additions & 20 deletions sections/user/take_edit.php
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -293,7 +286,6 @@ function ($key) {
i.NotifyOnDeleteSeeding = ?,
i.NotifyOnDeleteSnatched = ?,
i.NotifyOnDeleteDownloaded = ?,
m.Email = ?,
m.IRCKey = ?,
m.Paranoia = ?,
i.NavItems = ?
Expand All @@ -312,7 +304,6 @@ function ($key) {
$NotifyOnDeleteSeeding,
$NotifyOnDeleteSnatched,
$NotifyOnDeleteDownloaded,
$_POST['email'],
$_POST['irckey'],
serialize($Paranoia),
$UserNavItems
Expand All @@ -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('
Expand Down
2 changes: 1 addition & 1 deletion sections/user/user.php
Original file line number Diff line number Diff line change
Expand Up @@ -280,7 +280,7 @@ function display_rank(Gazelle\UserRank $r, string $dimension) {
}
if (check_perms('users_mod')) {
?>
<li>Passwords: <?=number_format($User->passwordCount())?> <a href="userhistory.php?action=passwords&amp;userid=<?=$UserID?>" class="brackets">View</a></li>
<li>Password history: <?=number_format($User->passwordCount())?> <a href="userhistory.php?action=passwords&amp;userid=<?=$UserID?>" class="brackets">View</a></li>
<li>Stats: N/A <a href="userhistory.php?action=stats&amp;userid=<?=$UserID?>" class="brackets">View</a></li>
<?php } ?>
</ul>
Expand Down
21 changes: 21 additions & 0 deletions templates/email/email-address-change.twig
Original file line number Diff line number Diff line change
@@ -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 }}
18 changes: 18 additions & 0 deletions templates/email/password-change.twig
Original file line number Diff line number Diff line change
@@ -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 }}
8 changes: 7 additions & 1 deletion templates/user/email-history.twig
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,15 @@
<div class="header">
<h2><a href="user.php?id={{ user.id }}">{{ user.username }}</a> &rsaquo; Email history</h2>
</div>
<div class="header">
<h3>Email History</h3>
</div>
<table>
<tr><th colspan="3">Email History</th></tr>
<tr>
<th>Address</th>
<th>Registered since</th>
<th>Registered from</th>
<th>Useragent</th>
</tr>
{% for i in user.emailHistory %}
<tr class="row{{ cycle(['a', 'b'], loop.index0) }}">
Expand All @@ -17,6 +20,7 @@
<a href="user.php?action=search&amp;ip_history=on&amp;ip={{ i.0 }}" class="brackets tooltip" title="Shared with other users?">S</a>
<a href="https://whatismyipaddress.com/ip/{{ i.0 }}" class="brackets tooltip" title="Search WIMIA.com">WI</a>
</td>
<td>{{ i.3 }}</td>
</tr>
{% endfor %}
</table>
Expand All @@ -33,6 +37,7 @@
<th>Email</th>
<th>Registered since</th>
<th>Registered from</th>
<th>Useragent</th>
<th>Enabled</th>
<th>Donor</th>
<th>Warned until</th>
Expand All @@ -46,6 +51,7 @@
<a href="user.php?action=search&amp;ip_history=on&amp;ip={{ r.ipv4 }}" class="brackets tooltip" title="Shared with other users?">S</a>
<a href="https://whatismyipaddress.com/ip/{{ r.ipv4 }}" class="brackets tooltip" title="Search WIMIA.com">WI</a>
</td>
<td>{{ r.useragent }}</td>
<td>{{ r.user.isEnabled ? 'yes' : 'no' }}</td>
<td>{{ r.user.isDonor ? 'yes' : 'no' }}</td>
<td>{{ r.user.isWarned ? r.user.endWarningDate(0) : 'no' }}</td>
Expand Down
Loading

0 comments on commit e981041

Please sign in to comment.