diff --git a/.gitignore b/.gitignore index 03ef8272..534f0b8d 100644 --- a/.gitignore +++ b/.gitignore @@ -11,6 +11,7 @@ # Logs /cronjobs/logs/*.txt /cronjobs/logs/*.txt.*.gz +/logs/* # Test configs public/include/config/global.inc.scrypt.php diff --git a/.htaccess b/.htaccess new file mode 100644 index 00000000..d71420f8 --- /dev/null +++ b/.htaccess @@ -0,0 +1,3 @@ +ErrorDocument 404 /public/index.php?page=error&action=404 +RedirectMatch 404 /logs(/|$) +Options -Indexes \ No newline at end of file diff --git a/logs/README.md b/logs/README.md new file mode 100644 index 00000000..45b983be --- /dev/null +++ b/logs/README.md @@ -0,0 +1 @@ +hi diff --git a/public/include/admin_checks.php b/public/include/admin_checks.php index 4935047d..3d07eefc 100644 --- a/public/include/admin_checks.php +++ b/public/include/admin_checks.php @@ -15,6 +15,12 @@ if (@$_SESSION['USERDATA']['is_admin'] && $user->isAdmin(@$_SESSION['USERDATA'][ } // setup checks + // logging + if ($config['logging']['enabled']) { + if (!is_writable($config['logging']['path'])) { + $error[] = "Logging is enabled but we can't write in the logging path"; + } + } // check if memcache isn't available but enabled in config -> error if (!class_exists('Memcached') && $config['memcache']['enabled']) { $error[] = "You have memcache enabled in your config and it's not available. Install the package on your system."; diff --git a/public/include/autoloader.inc.php b/public/include/autoloader.inc.php index 6cd887e5..888fca18 100644 --- a/public/include/autoloader.inc.php +++ b/public/include/autoloader.inc.php @@ -12,6 +12,9 @@ if (empty($config['algorithm']) || $config['algorithm'] == 'scrypt') { // Default classes require_once(CLASS_DIR . '/debug.class.php'); require_once(INCLUDE_DIR . '/lib/KLogger.php'); +if ($config['logging']['enabled']) { + $log = new KLogger($config['logging']['path']."/".$config['logging']['file'], $config['logging']['level']); +} if ($config['mysql_filter']) { require_once(CLASS_DIR . '/strict.class.php'); } diff --git a/public/include/classes/base.class.php b/public/include/classes/base.class.php index 385474f2..a7fdd768 100644 --- a/public/include/classes/base.class.php +++ b/public/include/classes/base.class.php @@ -19,6 +19,9 @@ class Base { public function setDebug($debug) { $this->debug = $debug; } + public function setLog($log) { + $this->log = $log; + } public function setMysql($mysqli) { $this->mysqli = $mysqli; } diff --git a/public/include/classes/notification.class.php b/public/include/classes/notification.class.php index 66887e34..16817d65 100644 --- a/public/include/classes/notification.class.php +++ b/public/include/classes/notification.class.php @@ -120,6 +120,9 @@ class Notification extends Mail { $this->setErrorMessage($this->getErrorMsg('E0047', $failed)); return $this->sqlError(); } + if ($this->config['logging']['enabled'] && $this->config['logging']['level'] > 0) { + $this->log->LogInfo("User $account_id updated notification settings from [".$_SERVER['REMOTE_ADDR']."]"); + } return true; } @@ -154,6 +157,7 @@ class Notification extends Mail { $notification = new Notification(); $notification->setDebug($debug); +$notification->setLog($log); $notification->setMysql($mysqli); $notification->setSmarty($smarty); $notification->setConfig($config); diff --git a/public/include/classes/payout.class.php b/public/include/classes/payout.class.php index 5255dbde..0a7c3b34 100644 --- a/public/include/classes/payout.class.php +++ b/public/include/classes/payout.class.php @@ -41,8 +41,20 @@ class Payout Extends Base { if ($this->config['twofactor']['enabled'] && $this->config['twofactor']['options']['withdraw']) { $tValid = $this->token->isTokenValid($account_id, $strToken, 7); if ($tValid) { - $this->token->deleteToken($strToken); + $delete = $this->token->deleteToken($strToken); + if ($delete) { + return true; + } else { + if ($this->config['logging']['enabled'] && $this->config['logging']['level'] > 0) { + $this->log->LogInfo("User $account_id requested manual payout but the token deletion failed from [".$_SERVER['REMOTE_ADDR']."]"); + } + $this->setErrorMessage('Unable to delete token'); + return false; + } } else { + if ($this->config['logging']['enabled'] && $this->config['logging']['level'] > 0) { + $this->log->LogInfo("User $account_id requested manual payout using an invalid token from [".$_SERVER['REMOTE_ADDR']."]"); + } $this->setErrorMessage('Invalid token'); return false; } @@ -67,6 +79,7 @@ class Payout Extends Base { $oPayout = new Payout(); $oPayout->setDebug($debug); +$oPayout->setLog($log); $oPayout->setMysql($mysqli); $oPayout->setConfig($config); $oPayout->setToken($oToken); diff --git a/public/include/classes/user.class.php b/public/include/classes/user.class.php index aa2b0319..70325e16 100644 --- a/public/include/classes/user.class.php +++ b/public/include/classes/user.class.php @@ -69,14 +69,23 @@ class User extends Base { } public function changeNoFee($id) { $field = array('name' => 'no_fees', 'type' => 'i', 'value' => !$this->isNoFee($id)); + if ($this->config['logging']['enabled'] && $this->config['logging']['level'] > 0) { + $this->log->LogWarn($this->getUserName($id)." changed no_fees to ".$this->isNoFee($id)." from [".$_SERVER['REMOTE_ADDR']."]"); + } return $this->updateSingle($id, $field); } public function setLocked($id, $value) { $field = array('name' => 'is_locked', 'type' => 'i', 'value' => $value); + if ($this->config['logging']['enabled'] && $this->config['logging']['level'] > 0) { + $this->log->LogWarn($this->getUserName($id)." changed is_locked to $value from [".$_SERVER['REMOTE_ADDR']."]"); + } return $this->updateSingle($id, $field); } public function changeAdmin($id) { $field = array('name' => 'is_admin', 'type' => 'i', 'value' => !$this->isAdmin($id)); + if ($this->config['logging']['enabled'] && $this->config['logging']['level'] > 0) { + $this->log->LogWarn($this->getUserName($id)." changed is_admin to ".$this->isAdmin($id)." from [".$_SERVER['REMOTE_ADDR']."]"); + } return $this->updateSingle($id, $field); } public function setUserFailed($id, $value) { @@ -145,6 +154,11 @@ class User extends Base { $lastLoginTime = $this->getLastLogin($uid); $this->updateLoginTimestamp($uid); $getIPAddress = $this->getUserIp($uid); + if ($this->config['logging']['enabled'] && $this->config['logging']['level'] > 0) { + if ($getIPAddress !== $_SERVER['REMOTE_ADDR']) { + $this->log->LogWarn("$username has logged in with a different IP [".$_SERVER['REMOTE_ADDR']."] saved is [$getIPAddress]"); + } + } $setIPAddress = $this->setUserIp($uid, $_SERVER['REMOTE_ADDR']); $this->createSession($username, $getIPAddress, $lastLoginTime); if ($setIPAddress) { @@ -172,11 +186,17 @@ class User extends Base { } } $this->setErrorMessage("Invalid username or password"); + if ($this->config['logging']['enabled'] && $this->config['logging']['level'] > 0) { + $this->log->LogInfo("$username failed login from [".$_SERVER['REMOTE_ADDR']."]"); + } if ($id = $this->getUserId($username)) { $this->incUserFailed($id); // Check if this account should be locked if (isset($this->config['maxfailed']['login']) && $this->getUserFailed($id) >= $this->config['maxfailed']['login']) { $this->setLocked($id, 1); + if ($this->config['logging']['enabled'] && $this->config['logging']['level'] > 0) { + $this->log->LogWarn("$username locked via failed logins from [".$_SERVER['REMOTE_ADDR']."] saved is [".$this->getUserIp($this->getUserId($username))."]"); + } if ($token = $this->token->createToken('account_unlock', $id)) { $aData['token'] = $token; $aData['username'] = $username; @@ -203,17 +223,23 @@ class User extends Base { $pin_hash = $this->getHash($pin); if ($stmt->bind_param('is', $userId, $pin_hash) && $stmt->execute() && $stmt->bind_result($row_pin) && $stmt->fetch()) { $this->setUserPinFailed($userId, 0); - return $pin_hash === $row_pin; + return ($pin_hash === $row_pin); + } + if ($this->config['logging']['enabled'] && $this->config['logging']['level'] > 0) { + $this->log->LogInfo($this->getUserName($userId)." incorrect pin from [".$_SERVER['REMOTE_ADDR']."]"); } $this->incUserPinFailed($userId); // Check if this account should be locked if (isset($this->config['maxfailed']['pin']) && $this->getUserPinFailed($userId) >= $this->config['maxfailed']['pin']) { $this->setLocked($userId, 1); + if ($this->config['logging']['enabled'] && $this->config['logging']['level'] > 0) { + $this->log->LogWarn($this->getUserName($userId)." was locked via incorrect pins from [".$_SERVER['REMOTE_ADDR']."]"); + } if ($token = $this->token->createToken('account_unlock', $userId)) { $username = $this->getUserName($userId); $aData['token'] = $token; $aData['username'] = $username; - $aData['email'] = $this->getUserEmail($username);; + $aData['email'] = $this->getUserEmail($username); $aData['subject'] = 'Account auto-locked'; $this->mail->sendMail('notifications/locked', $aData); } @@ -234,17 +260,25 @@ class User extends Base { $newpin = $this->getHash($newpin); $aData['subject'] = 'PIN Reset Request'; $stmt = $this->mysqli->prepare("UPDATE $this->table SET pin = ? WHERE ( id = ? AND pass = ? )"); - if ($this->checkStmt($stmt) && $stmt->bind_param('sis', $newpin, $userID, $current) && $stmt->execute()) { if ($stmt->errno == 0 && $stmt->affected_rows === 1) { if ($this->mail->sendMail('pin/reset', $aData)) { + if ($this->config['logging']['enabled'] && $this->config['logging']['level'] > 0) { + $this->log->LogInfo($this->getUserName($userID)." was sent a pin reset from [".$_SERVER['REMOTE_ADDR']."]"); + } return true; } else { + if ($this->config['logging']['enabled'] && $this->config['logging']['level'] > 0) { + $this->log->LogWarn($this->getUserName($userID)." request a pin reset but the mailing failed from [".$_SERVER['REMOTE_ADDR']."]"); + } $this->setErrorMessage('Unable to send mail to your address'); return false; } } } + if ($this->config['logging']['enabled'] && $this->config['logging']['level'] > 0) { + $this->log->LogWarn($this->getUserName($userID)." incorrect pin reset attempt from [".$_SERVER['REMOTE_ADDR']."]"); + } $this->setErrorMessage( 'Unable to generate PIN, current password incorrect?' ); return false; } @@ -319,14 +353,23 @@ class User extends Base { default: $aData['subject'] = ''; } + if ($this->config['logging']['enabled'] && $this->config['logging']['level'] > 0) { + $this->log->LogInfo($this->getUserName($userID)." was sent a $strType token from [".$_SERVER['REMOTE_ADDR']."]"); + } if ($this->mail->sendMail('notifications/'.$strType, $aData)) { return true; } else { $this->setErrorMessage('Failed to send the notification'); + if ($this->config['logging']['enabled'] && $this->config['logging']['level'] > 0) { + $this->log->LogWarn($this->getUserName($userID)." requested a $strType token but the mailing failed from [".$_SERVER['REMOTE_ADDR']."]"); + } return false; } } - $this->setErrorMessage('A request has already been sent to your e-mail address. Please wait 10 minutes for it to expire.'); + if ($this->config['logging']['enabled'] && $this->config['logging']['level'] > 0) { + $this->log->LogWarn($this->getUserName($userID)." attempted to request multiple $strType tokens from [".$_SERVER['REMOTE_ADDR']."]"); + } + $this->setErrorMessage('A request has already been sent to your e-mail address. Please wait an hour for it to expire.'); return false; } @@ -351,25 +394,44 @@ class User extends Base { } $current = $this->getHash($current); $new = $this->getHash($new1); + if ($this->config['twofactor']['enabled'] && $this->config['twofactor']['options']['changepw']) { + $tValid = $this->token->isTokenValid($userID, $strToken, 6); + if ($tValid) { + if ($this->token->deleteToken($strToken)) { + if ($this->config['logging']['enabled'] && $this->config['logging']['level'] > 0) { + $this->log->LogInfo($this->getUserName($userID)." deleted change password token from [".$_SERVER['REMOTE_ADDR']."]"); + } + // token deleted, continue + } else { + if ($this->config['logging']['enabled'] && $this->config['logging']['level'] > 0) { + $this->log->LogWarn($this->getUserName($userID)." change password token failed to delete from [".$_SERVER['REMOTE_ADDR']."]"); + } + $this->setErrorMessage('Token deletion failed'); + return false; + } + } else { + if ($this->config['logging']['enabled'] && $this->config['logging']['level'] > 0) { + $this->log->LogWarn($this->getUserName($userID)." attempted to use an invalid change password token from [".$_SERVER['REMOTE_ADDR']."]"); + } + $this->setErrorMessage('Invalid token'); + return false; + } + } $stmt = $this->mysqli->prepare("UPDATE $this->table SET pass = ? WHERE ( id = ? AND pass = ? )"); if ($this->checkStmt($stmt)) { $stmt->bind_param('sis', $new, $userID, $current); $stmt->execute(); if ($stmt->errno == 0 && $stmt->affected_rows === 1) { - // twofactor - consume the token if it is enabled and valid - if ($this->config['twofactor']['enabled'] && $this->config['twofactor']['options']['changepw']) { - $tValid = $this->token->isTokenValid($userID, $strToken, 6); - if ($tValid) { - $this->token->deleteToken($strToken); - } else { - $this->setErrorMessage('Invalid token'); - return false; - } + if ($this->config['logging']['enabled'] && $this->config['logging']['level'] > 0) { + $this->log->LogInfo($this->getUserName($userID)." updated password from [".$_SERVER['REMOTE_ADDR']."]"); } return true; } $stmt->close(); } + if ($this->config['logging']['enabled'] && $this->config['logging']['level'] > 0) { + $this->log->LogWarn($this->getUserName($userID)." incorrect password update attempt from [".$_SERVER['REMOTE_ADDR']."]"); + } $this->setErrorMessage( 'Unable to update password, current password wrong?' ); return false; } @@ -434,20 +496,38 @@ class User extends Base { $threshold = min($this->config['ap_threshold']['max'], max(0, floatval($threshold))); $donate = min(100, max(0, floatval($donate))); - // We passed all validation checks so update the account - $stmt = $this->mysqli->prepare("UPDATE $this->table SET coin_address = ?, ap_threshold = ?, donate_percent = ?, email = ?, is_anonymous = ? WHERE id = ?"); - if ($this->checkStmt($stmt) && $stmt->bind_param('sddsii', $address, $threshold, $donate, $email, $is_anonymous, $userID) && $stmt->execute()) - // twofactor - consume the token if it is enabled and valid - if ($this->config['twofactor']['enabled'] && $this->config['twofactor']['options']['details']) { - $tValid = $this->token->isTokenValid($userID, $strToken, 5); - if ($tValid) { - $this->token->deleteToken($strToken); + // twofactor - consume the token if it is enabled and valid + if ($this->config['twofactor']['enabled'] && $this->config['twofactor']['options']['details']) { + $tValid = $this->token->isTokenValid($userID, $strToken, 5); + if ($tValid) { + if ($this->token->deleteToken($strToken)) { + if ($this->config['logging']['enabled'] && $this->config['logging']['level'] > 0) { + $this->log->LogInfo($this->getUserName($userID)." deleted account update token for [".$_SERVER['REMOTE_ADDR']."]"); + } } else { - $this->setErrorMessage('Invalid token'); + $this->setErrorMessage('Token deletion failed'); + if ($this->config['logging']['enabled'] && $this->config['logging']['level'] > 0) { + $this->log->LogWarn($this->getUserName($userID)." updated their account details but token deletion failed from [".$_SERVER['REMOTE_ADDR']."]"); + } return false; } + } else { + $this->setErrorMessage('Invalid token'); + if ($this->config['logging']['enabled'] && $this->config['logging']['level'] > 0) { + $this->log->LogWarn($this->getUserName($userID)." attempted to use an invalid token account update token from [".$_SERVER['REMOTE_ADDR']."]"); + } + return false; + } + } + + // We passed all validation checks so update the account + $stmt = $this->mysqli->prepare("UPDATE $this->table SET coin_address = ?, ap_threshold = ?, donate_percent = ?, email = ?, is_anonymous = ? WHERE id = ?"); + if ($this->checkStmt($stmt) && $stmt->bind_param('sddsii', $address, $threshold, $donate, $email, $is_anonymous, $userID) && $stmt->execute()) { + if ($this->config['logging']['enabled'] && $this->config['logging']['level'] > 0) { + $this->log->LogInfo($this->getUserName($userID)." updated their account details from [".$_SERVER['REMOTE_ADDR']."]"); } return true; + } // Catchall $this->setErrorMessage('Failed to update your account'); $this->debug->append('Account update failed: ' . $this->mysqli->error); @@ -542,7 +622,7 @@ class User extends Base { $port = ($_SERVER["SERVER_PORT"] == "80" || $_SERVER["SERVER_PORT"] == "443") ? "" : (":".$_SERVER["SERVER_PORT"]); $pushto = $_SERVER['SCRIPT_NAME'].'?page=login'; $location = (@$_SERVER['HTTPS'] == 'on') ? 'https://' . $_SERVER['SERVER_NAME'] . $port . $pushto : 'http://' . $_SERVER['SERVER_NAME'] . $port . $pushto; - // if (!headers_sent()) header('Location: ' . $location); + if (!headers_sent()) header('Location: ' . $location); exit(''); } @@ -789,6 +869,13 @@ class User extends Base { } $aData['username'] = $this->getUserName($this->getUserId($username, true)); $aData['subject'] = 'Password Reset Request'; + if ($this->config['logging']['enabled'] && $this->config['logging']['level'] > 0) { + if ($_SERVER['REMOTE_ADDR'] !== $this->getUserIp($this->getUserId($username, true))) { + $this->log->LogWarn("$username requested password reset from [".$_SERVER['REMOTE_ADDR']."] saved is [".$this->getUserIp($this->getUserId($username, true))."]"); + } else { + $this->log->LogInfo("$username requested password reset from [".$_SERVER['REMOTE_ADDR']."] saved is [".$this->getUserIp($this->getUserId($username, true))."]"); + } + } if ($this->mail->sendMail('password/reset', $aData)) { return true; } else { @@ -812,7 +899,10 @@ public function isAuthenticated($logout=true) { $this->getUserIp($_SESSION['USERDATA']['id']) == $_SERVER['REMOTE_ADDR'] ) return true; // Catchall - if ($logout == true) $this->logoutUser($_SERVER['REQUEST_URI']); + if ($this->config['logging']['enabled'] && $this->config['logging']['level'] > 0) { + $this->log->LogWarn("Forcing logout, user is locked or IP changed mid session from [".$_SERVER['REMOTE_ADDR']."]"); + } + if ($logout == true) $this->logoutUser(); return false; } @@ -853,6 +943,7 @@ public function isAuthenticated($logout=true) { // Make our class available automatically $user = new User(); $user->setDebug($debug); +$user->setLog($log); $user->setMysql($mysqli); $user->setSalt($config['SALT']); $user->setSmarty($smarty); diff --git a/public/include/config/security.inc.dist.php b/public/include/config/security.inc.dist.php index 20f4c140..b0d33a08 100644 --- a/public/include/config/security.inc.dist.php +++ b/public/include/config/security.inc.dist.php @@ -4,11 +4,21 @@ $defflip = (!cfip()) ? exit(header('HTTP/1.1 401 Unauthorized')) : 1; /** * Misc * Extra security settings - * + * **/ $config['https_only'] = false; $config['mysql_filter'] = true; +/** + * Logging + * Log security issues - 0 = disabled, 2 = everything, 3 = warnings only + * + */ +$config['logging']['enabled'] = true; +$config['logging']['level'] = 3; +$config['logging']['path'] = realpath(BASEPATH.'../logs'); +$config['logging']['file'] = date('Y-m-d').'.security.log'; + /** * Memcache Rate Limiting * Rate limit requests using Memcache diff --git a/public/include/pages/account/edit.inc.php b/public/include/pages/account/edit.inc.php index 6c728c0a..4e59bea4 100644 --- a/public/include/pages/account/edit.inc.php +++ b/public/include/pages/account/edit.inc.php @@ -72,10 +72,10 @@ if ($user->isAuthenticated()) { } } else { - if ( @$_POST['do'] && (!$checkpin = $user->checkPin($_SESSION['USERDATA']['id'], @$_POST['authPin']))) { + if ( @$_POST['do'] && !$user->checkPin($_SESSION['USERDATA']['id'], @$_POST['authPin'])) { $_SESSION['POPUP'][] = array('CONTENT' => 'Invalid PIN. ' . ($config['maxfailed']['pin'] - $user->getUserPinFailed($_SESSION['USERDATA']['id'])) . ' attempts remaining.', 'TYPE' => 'errormsg'); } else { - if (isset($_POST['unlock']) && isset($_POST['utype']) && $checkpin) { + if (isset($_POST['unlock']) && isset($_POST['utype'])) { $validtypes = array('account_edit','change_pw','withdraw_funds'); $isvalid = in_array($_POST['utype'],$validtypes); if ($isvalid) { @@ -99,6 +99,9 @@ if ($user->isAuthenticated()) { } else { $aBalance = $transaction->getBalance($_SESSION['USERDATA']['id']); $dBalance = $aBalance['confirmed']; + if ($this->config['logging']['enabled'] && $this->config['logging']['level'] > 0) { + $user->log->LogInfo($_SESSION['USERDATA']['username']." requesting manual payout from [".$_SERVER['REMOTE_ADDR']."]"); + } if ($dBalance > $config['txfee_manual']) { if (!$oPayout->isPayoutActive($_SESSION['USERDATA']['id'])) { if (!$config['csrf']['enabled'] || $config['csrf']['enabled'] && $csrftoken->valid) { diff --git a/public/include/pages/admin/settings.inc.php b/public/include/pages/admin/settings.inc.php index 6fb477ea..cd9a5230 100644 --- a/public/include/pages/admin/settings.inc.php +++ b/public/include/pages/admin/settings.inc.php @@ -8,6 +8,9 @@ if (!$user->isAuthenticated() || !$user->isAdmin($_SESSION['USERDATA']['id'])) { } if (@$_REQUEST['do'] == 'save' && !empty($_REQUEST['data'])) { + if ($this->config['logging']['enabled'] && $this->config['logging']['level'] > 0) { + $user->log->LogWarn($_SESSION['USERDATA']['username']." changed admin settings from [".$_SERVER['REMOTE_ADDR']."]"); + } foreach($_REQUEST['data'] as $var => $value) { $setting->setValue($var, $value); } diff --git a/public/index.php b/public/index.php index 811b9b05..df800d64 100644 --- a/public/index.php +++ b/public/index.php @@ -59,12 +59,14 @@ if ($config['memcache']['enabled'] && $config['mc_antidos']['enabled']) { $session_start = @session_start(); session_set_cookie_params(time()+$config['cookie']['duration'], $config['cookie']['path'], $config['cookie']['domain'], $config['cookie']['secure'], $config['cookie']['httponly']); if (!$session_start) { + if ($this->config['logging']['enabled'] && $this->config['logging']['level'] > 0) { + $log->LogInfo("Forcing session id regeneration for ".$_SERVER['REMOTE_ADDR']." [hijack attempt?]"); + } session_destroy(); session_regenerate_id(true); session_start(); } @setcookie(session_name(), session_id(), time()+$config['cookie']['duration'], $config['cookie']['path'], $config['cookie']['domain'], $config['cookie']['secure'], $config['cookie']['httponly']); - // Rate limiting if ($config['memcache']['enabled'] && $config['mc_antidos']['enabled']) { $skip_check = false;