PHP WebShell
Текущая директория: /var/www/bitcardoApp/models/auth
Просмотр файла: login_process.php.bak
<?php
/**
* models/auth/login_process.php
*
* What this does (carefully, without breaking existing behavior):
* - Loads bootstrap (headers + secure session + serv_config + db_config + settings)
* - Validates reCAPTCHA (server-side)
* - Looks up user by email OR phone (prepared statement)
* - Verifies password (password_hash / password_verify)
* - Sets required session keys (email is needed for /chat/sso)
* - OPTIONAL (only if present):
* - lib/session.php -> create session record + remember-me cookie
* - lib/device.php -> if OTP-on-new-device is enabled & device is new, redirect to /auth/challenge.php
* - lib/rate_limit.php -> throttle attempts (per IP + per login)
* - Redirects to dashboard on success; back to login on failure, with a safe error
*
* Notes:
* - We rely on config/bootstrap.php to send headers & start a hardened session.
* - We never double-include serv_config/db_config/settings. Bootstrap already did that.
* - Every exit path uses a small safe_redirect() helper to avoid partial output issues.
*/
////////////////////////////////////////////
// 0) Bootstrap (required)
////////////////////////////////////////////
$__bootstrap = __DIR__ . '/../../config/bootstrap.php';
if (!file_exists($__bootstrap)) {
// Absolute minimum fallback to avoid fatal if bootstrap isn't deployed yet.
if (session_status() === PHP_SESSION_NONE) { session_start(); }
require_once __DIR__ . '/../../config/serv_config.php';
require_once __DIR__ . '/../../config/db_config.php';
// settings are optional; ignore if not present
$__settings = __DIR__ . '/../../config/settings.php';
if (file_exists($__settings)) { require_once $__settings; }
unset($__settings);
} else {
require_once $__bootstrap;
}
unset($__bootstrap);
////////////////////////////////////////////
// 0.1) Small helpers
////////////////////////////////////////////
/** Safe redirect that tolerates accidental output. */
function safe_redirect(string $location) {
if (!headers_sent()) {
header('Location: ' . $location);
} else {
echo '<script>location.href=' . json_encode($location) . ';</script>';
}
exit;
}
/** Uniform error exit back to login with a message. */
function fail_with(string $msg) {
$_SESSION['error'] = $msg;
safe_redirect('../../auth/login.php');
}
// CSRF check (optional but recommended)
if (!isset($_POST['csrf'], $_SESSION['csrf']) || !hash_equals($_SESSION['csrf'], $_POST['csrf'])) {
$_SESSION['error'] = 'Session expired. Please try again.';
header('Location: ../../auth/login.php'); exit;
}
////////////////////////////////////////////
// 1) Collect + normalize input
////////////////////////////////////////////
$loginRaw = $_POST['login'] ?? '';
$passRaw = $_POST['password'] ?? '';
$login = trim(preg_replace('/\s+/', ' ', (string)$loginRaw)); // collapse internal whitespace
$password = (string)$passRaw;
// Keep the typed login for user convenience on failure
$_SESSION['form_data'] = ['login' => $login];
////////////////////////////////////////////
// 2) Validate reCAPTCHA (server-side)
////////////////////////////////////////////
$recaptchaResponse = $_POST['g-recaptcha-response'] ?? '';
$secret = defined('RECAPTCHA_SECRET') ? RECAPTCHA_SECRET : '';
if ($secret === '') {
// Not configured: fail closed (or you can flip to a warning in dev)
fail_with('Captcha is not configured. Please try again later.');
}
// Build a tiny HTTP context with timeouts to avoid hanging
$ctx = stream_context_create([
'http' => [
'method' => 'POST',
'header' => "Content-type: application/x-www-form-urlencoded\r\n",
'content' => http_build_query([
'secret' => $secret,
'response' => $recaptchaResponse,
// Optionally: 'remoteip' => $_SERVER['REMOTE_ADDR'] ?? null,
]),
'timeout' => 5,
]
]);
$verify = @file_get_contents('https://www.google.com/recaptcha/api/siteverify', false, $ctx);
$captcha = $verify ? json_decode($verify, true) : ['success' => false];
if (empty($captcha['success'])) {
fail_with('Captcha verification failed.');
}
////////////////////////////////////////////
// 3) Quick sanity checks
////////////////////////////////////////////
if (!($conn instanceof mysqli)) {
// DB not ready; do not leak details
fail_with('Service temporarily unavailable. Please try again.');
}
if ($login === '' || $password === '') {
fail_with('Please enter your login and password.');
}
////////////////////////////////////////////
// 3.5) ADDED: rate-limit (feature-flagged & optional)
////////////////////////////////////////////
$rateLib = __DIR__ . '/../../lib/rate_limit.php';
$rlActive = false;
if (file_exists($rateLib)) {
require_once $rateLib;
// Feature toggle via settings.php (lowercase keys); default ON
$rlActive = function_exists('is_enabled') ? is_enabled('rate_limit_enabled', true) : true;
if ($rlActive) {
// Policy (conservative defaults)
$IP_LIMIT = 10; // attempts
$IP_WINDOW_SEC = 900; // 15m
$IP_LOCK_SEC = 900; // 15m
$ID_LIMIT = 7; // attempts per email/phone
$ID_WINDOW_SEC = 900; // 15m
$ID_LOCK_SEC = 900; // 15m
// Tokens
$ipToken = $_SERVER['REMOTE_ADDR'] ?? '0.0.0.0';
$loginTok = function_exists('rl_norm_login') ? rl_norm_login($login) : mb_strtolower(trim($login));
// Check IP bucket
if (function_exists('rl_check_and_inc')) {
$r = rl_check_and_inc($conn, 'ip', $ipToken, $IP_LIMIT, $IP_WINDOW_SEC, $IP_LOCK_SEC);
if (!$r['ok']) {
fail_with("Too many attempts from your network. Try again in {$r['locked_for']}s.");
}
}
// Check identifier bucket
if ($loginTok !== '' && function_exists('rl_check_and_inc')) {
$r = rl_check_and_inc($conn, 'login', $loginTok, $ID_LIMIT, $ID_WINDOW_SEC, $ID_LOCK_SEC);
if (!$r['ok']) {
fail_with("Too many attempts on this account. Try again in {$r['locked_for']}s.");
}
}
}
}
unset($rateLib);
////////////////////////////////////////////
// 4) Find user (by email OR phone) with prepared statement
////////////////////////////////////////////
$sql = "SELECT user_id, email, first_name, last_name, password_hash
FROM users
WHERE email = ? OR phone = ?
LIMIT 1";
$stmt = $conn->prepare($sql);
if (!$stmt) {
// Do not expose SQL errors to the user
fail_with('Service temporarily unavailable. Please try again.');
}
$stmt->bind_param('ss', $login, $login);
$stmt->execute();
$stmt->store_result();
if ($stmt->num_rows !== 1) {
$stmt->close();
fail_with('Invalid login credentials.');
}
$stmt->bind_result($userId, $email, $firstName, $lastName, $hash);
$stmt->fetch();
////////////////////////////////////////////
// 5) Verify password (timing-safe)
////////////////////////////////////////////
if (!is_string($hash) || $hash === '' || !password_verify($password, $hash)) {
$stmt->close();
fail_with('Invalid login credentials.');
}
// Good path → close stmt
$stmt->close();
////////////////////////////////////////////
// 6) Establish session identity
////////////////////////////////////////////
if (session_status() !== PHP_SESSION_ACTIVE) { session_start(); }
// Refresh session ID after successful auth
session_regenerate_id(true);
// Set keys (email is used by /chat/sso to create/auto-login on Grupo)
$_SESSION['user_id'] = (int)$userId;
$_SESSION['email'] = $email;
$_SESSION['first_name'] = $firstName ?? '';
$_SESSION['last_name'] = $lastName ?? '';
$_SESSION['loggedIn'] = true;
// Clear prefill
unset($_SESSION['form_data']);
////////////////////////////////////////////
// 6.5) ADDED: rate-limit reset on success (if active)
////////////////////////////////////////////
if (!empty($rlActive) && function_exists('rl_reset')) {
$ipToken = $_SERVER['REMOTE_ADDR'] ?? '0.0.0.0';
$loginTok = function_exists('rl_norm_login') ? rl_norm_login($login) : mb_strtolower(trim($login));
rl_reset($conn, 'ip', $ipToken);
if ($loginTok !== '') rl_reset($conn, 'login', $loginTok);
}
////////////////////////////////////////////
// 7) Optional Phase 1: session record + remember-me (only if present)
////////////////////////////////////////////
$rememberRequested = !empty($_POST['remember']); // checkbox name="remember"
$libDevice = __DIR__ . '/../../lib/device.php';
$libSession = __DIR__ . '/../../lib/session.php';
if (file_exists($libSession)) {
require_once $libSession;
// Respect DB flag if settings helper exists; default allow
$rememberAllowed = true;
if (function_exists('is_enabled')) {
$rememberAllowed = is_enabled('remember_me', true);
}
if (function_exists('session_record_create')) {
// Writes user_sessions row + sets remember cookie if requested/allowed
session_record_create($conn, (int)$userId, ($rememberAllowed && $rememberRequested));
}
}
unset($libSession);
$hasGrace = !empty($_SESSION['first_session_grace']);
if ($otpEnabled && $requireOnNew && !$trusted && !$hasGrace) {
safe_redirect('/auth/challenge.php');
}
// Consume grace once per session
unset($_SESSION['first_session_grace']);
////////////////////////////////////////////
// 8) Optional Phase 2: device trust + TOTP challenge gate (only if present)
////////////////////////////////////////////
if (file_exists($libDevice)) {
require_once $libDevice;
// Defaults: challenge OFF unless explicitly enabled in DB
$otpEnabled = function_exists('is_enabled') ? is_enabled('otp_enabled', true) : true;
$requireOnNew = function_exists('is_enabled') ? is_enabled('otp_require_on_new_dev', true) : false;
if ($otpEnabled && $requireOnNew && function_exists('device_is_trusted')) {
$trusted = device_is_trusted($conn, (int)$userId);
if (!$trusted) {
// Challenge exists? Redirect there. If you haven't created it yet, this line is never reached unless you enabled the flags.
safe_redirect('/auth/challenge.php');
}
}
}
unset($libDevice);
////////////////////////////////////////////
// 9) Success → Dashboard
////////////////////////////////////////////
safe_redirect('../../user/dashboard/index.php');
Выполнить команду
Для локальной разработки. Не используйте в интернете!