PHP WebShell
Текущая директория: /var/www/bitcardoApp/auth
Просмотр файла: challenge.php
<?php
// auth/challenge.php — Delightful UX: TOTP first, Email OTP fallback, resend cooldown
require_once __DIR__ . '/../config/bootstrap.php';
if (!defined('OTP_DEV_MODE')) {
define('OTP_DEV_MODE', true); // <-- you can omit this entirely; bootstrap already provides it
}
// Optional libs if present
$libDevice = __DIR__ . '/../lib/device.php';
if (file_exists($libDevice)) require_once $libDevice;
$libTotp = __DIR__ . '/../lib/totp.php';
if (file_exists($libTotp)) require_once $libTotp;
// [ADDED: RL BOOTSTRAP] -------------------------------------------
$rateLib = __DIR__ . '/../lib/rate_limit.php';
$rlActive = false;
if (file_exists($rateLib)) {
require_once $rateLib;
$rlActive = function_exists('is_enabled') ? is_enabled('rate_limit_enabled', true) : true;
// tokens for OTP/TOTP throttling
$ipToken = $_SERVER['REMOTE_ADDR'] ?? '0.0.0.0';
// we’ll bucket by user to stop rapid attempts on one account
$usrToken = 'user_' . (int)($_SESSION['user_id'] ?? 0);
// policy (OTP stricter than login)
$OTP_SEND_LIMIT = 5; // sends per 15m per user
$OTP_SEND_WINDOW_SEC = 900;
$OTP_SEND_LOCK_SEC = 900;
$OTP_VERIFY_LIMIT = 8; // wrong OTP attempts per 15m per user
$OTP_VERIFY_WINDOW_SEC= 900;
$OTP_VERIFY_LOCK_SEC = 900;
$TOTP_VERIFY_LIMIT = 10; // totp guesses per 15m per user
$TOTP_VERIFY_WINDOW_SEC = 900;
$TOTP_VERIFY_LOCK_SEC = 900;
}
// ---------------------------------------------------------------
// Guards
if (empty($_SESSION['user_id'])) { header('Location: /auth/login.php'); exit; }
$userId = (int)$_SESSION['user_id'];
$userEmail = $_SESSION['email'] ?? '';
$otpOn = function_exists('is_enabled') ? is_enabled('otp_enabled', true) : true;
$emailOn = function_exists('is_enabled') ? is_enabled('email_otp', true) : true;
$totpOn = function_exists('is_enabled') ? is_enabled('totp_enabled', true) : true;
$trustDays = (int)(function_exists('get_settings') ? get_settings('otp_trust_days', 30) : 30);
if (!$otpOn) { header('Location: /user/dashboard/index.php'); exit; }
// ----- Config: resend cooldown seconds -----
$cooldownSec = 60;
// ----- Helpers -----
function page_redirect($url){ header('Location: '.$url); exit; }
function send_email_simple($to, $subject, $bodyHtml) {
// Try PHPMailer if present, else mail()
if (class_exists('PHPMailer\PHPMailer\PHPMailer')) {
$mail = new PHPMailer\PHPMailer\PHPMailer(true);
try {
if (defined('SMTP_HOST') && SMTP_HOST) {
$mail->isSMTP();
$mail->Host = SMTP_HOST;
$mail->Port = defined('SMTP_PORT') ? SMTP_PORT : 587;
$mail->SMTPAuth = (defined('SMTP_USER') && SMTP_USER) || (defined('SMTP_PASS') && SMTP_PASS);
if ($mail->SMTPAuth) {
$mail->Username = defined('SMTP_USER') ? SMTP_USER : '';
$mail->Password = defined('SMTP_PASS') ? SMTP_PASS : '';
}
if (defined('SMTP_SECURE') && SMTP_SECURE) {
$mail->SMTPSecure = SMTP_SECURE; // 'tls' or 'ssl'
}
}
$from = defined('SMTP_FROM') ? SMTP_FROM : 'no-reply@bitcardo.com';
$fromName = defined('SMTP_FROM_NAME') ? SMTP_FROM_NAME : 'Bitcardo';
$mail->setFrom($from, $fromName);
$mail->addAddress($to);
$mail->isHTML(true);
$mail->Subject = $subject;
$mail->Body = $bodyHtml;
$mail->AltBody = strip_tags($bodyHtml);
$mail->send();
return true;
} catch (\Throwable $e) {
error_log('[EMAIL] PHPMailer send failed: '.$e->getMessage());
}
}
// Fallback: native mail()
$headers = "MIME-Version: 1.0\r\n";
$headers .= "Content-type: text/html; charset=UTF-8\r\n";
$from = defined('SMTP_FROM') ? SMTP_FROM : 'no-reply@bitcardo.com';
$fromName = defined('SMTP_FROM_NAME') ? SMTP_FROM_NAME : 'Bitcardo';
$headers .= "From: {$fromName} <{$from}>\r\n";
$ok = @mail($to, $subject, $bodyHtml, $headers);
if (!$ok) error_log('[EMAIL] mail() returned false. Check server mail config.');
return $ok;
}
function otp_can_send_again(mysqli $conn, int $userId, string $channel='email', int $cooldownSec=60): bool {
$stmt = $conn->prepare("SELECT created_at FROM user_otps WHERE user_id=? AND channel=? AND consumed_at IS NULL ORDER BY uotp_id DESC LIMIT 1");
$stmt->bind_param('is', $userId, $channel);
$stmt->execute();
$stmt->bind_result($createdAt);
if ($stmt->fetch()) {
$stmt->close();
if ($createdAt) {
$last = new DateTimeImmutable($createdAt);
return (time() - $last->getTimestamp()) >= $cooldownSec;
}
} else { $stmt->close(); }
return true;
}
function otp_seconds_until_send(mysqli $conn, int $userId, string $channel='email', int $cooldownSec=60): int {
$stmt = $conn->prepare("SELECT created_at FROM user_otps WHERE user_id=? AND channel=? AND consumed_at IS NULL ORDER BY uotp_id DESC LIMIT 1");
$stmt->bind_param('is', $userId, $channel);
$stmt->execute();
$stmt->bind_result($createdAt);
if ($stmt->fetch()) {
$stmt->close();
if ($createdAt) {
$last = new DateTimeImmutable($createdAt);
$remain = $cooldownSec - (time() - $last->getTimestamp());
return max(0, (int)$remain);
}
} else { $stmt->close(); }
return 0;
}
function otp_issue_and_send(mysqli $conn, int $userId, string $email): bool {
// Invalidate previous unconsumed codes (optional)
$conn->query("UPDATE user_otps SET consumed_at=NOW() WHERE user_id={$userId} AND channel='email' AND consumed_at IS NULL");
$code = str_pad((string)random_int(0, 999999), 6, '0', STR_PAD_LEFT);
$selector = bin2hex(random_bytes(12));
$hash = hash('sha256', $code);
$ip = $_SERVER['REMOTE_ADDR'] ?? '';
$expiry = (new DateTimeImmutable('+10 minutes'))->format('Y-m-d H:i:s');
$stmt = $conn->prepare("INSERT INTO user_otps (user_id, channel, selector, token_hash, expires_at, ip) VALUES (?, 'email', ?, ?, ?, ?)");
if (!$stmt) { error_log('[OTP] prepare failed: '.$conn->error); return false; }
$stmt->bind_param('issss', $userId, $selector, $hash, $expiry, $ip);
$ok = $stmt->execute();
if (!$ok) { error_log('[OTP] execute failed: '.$conn->error); $stmt->close(); return false; }
$stmt->close();
// Polished email template
$brand = defined('SMTP_FROM_NAME') ? SMTP_FROM_NAME : 'Bitcardo';
$year = date('Y');
$codeSpaced = implode(' ', str_split($code, 3));
$html = <<<HTML
<table role="presentation" width="100%" cellpadding="0" cellspacing="0" style="background:#f6f8fb;padding:24px 0;">
<tr>
<td align="center">
<table role="presentation" width="560" cellpadding="0" cellspacing="0" style="background:#ffffff;border-radius:12px;border:1px solid #e8eef3;box-shadow:0 4px 18px rgba(7,98,137,.06);font-family:system-ui,-apple-system,Segoe UI,Roboto,Arial,sans-serif;">
<tr><td style="padding:24px 24px 0 24px;text-align:center;">
<div style="display:inline-block;background:#076289;color:#fff;font-weight:700;padding:8px 14px;border-radius:999px;">$brand Security</div>
</td></tr>
<tr><td style="padding:16px 24px 4px 24px;text-align:center;">
<h1 style="margin:0;font-size:20px;color:#0f172a;">Verify it’s you</h1>
<p style="margin:8px 0 0 0;color:#334155;font-size:14px;line-height:1.5">Use the code below to finish signing in. This keeps your account and funds protected.</p>
</td></tr>
<tr><td style="padding:8px 24px 24px 24px;text-align:center;">
<div style="display:inline-block;border:2px dashed #076289;border-radius:12px;padding:14px 18px;">
<div style="font-size:28px;font-weight:800;letter-spacing:4px;color:#0f172a;">$codeSpaced</div>
<div style="color:#64748b;font-size:12px;margin-top:6px;">Expires in 10 minutes</div>
</div>
</td></tr>
<tr><td style="padding:0 24px 24px 24px;text-align:center;">
<div style="color:#475569;font-size:13px;line-height:1.6">
Didn’t request this? You can safely ignore this email.
</div>
</td></tr>
<tr><td style="padding:16px 24px 24px 24px;border-top:1px solid #e8eef3;text-align:center;color:#94a3b8;font-size:12px;">
© $year $brand
</td></tr>
</table>
</td>
</tr>
</table>
HTML;
$sentOk = send_email_simple($email, "{$brand} Verification Code", $html);
if (!$sentOk && defined('OTP_DEV_MODE') && OTP_DEV_MODE) {
$_SESSION['__DEV_OTP_CODE__'] = $code;
error_log('[OTP] DEV_MODE enabled — exposing OTP on page for testing.');
return true;
}
return $sentOk;
}
function otp_verify(mysqli $conn, int $userId, string $code): bool {
$code = preg_replace('/\D+/', '', $code);
if (strlen($code) !== 6) return false;
$stmt = $conn->prepare("SELECT uotp_id, token_hash, expires_at, attempts FROM user_otps
WHERE user_id=? AND channel='email' AND consumed_at IS NULL
ORDER BY uotp_id DESC LIMIT 1");
$stmt->bind_param('i', $userId);
$stmt->execute();
$stmt->bind_result($id, $tokenHash, $expiresAt, $attempts);
if (!$stmt->fetch()) { $stmt->close(); return false; }
$stmt->close();
if (new DateTimeImmutable($expiresAt) < new DateTimeImmutable()) {
$u = $conn->prepare("UPDATE user_otps SET consumed_at=NOW() WHERE uotp_id=?");
$u->bind_param('i', $id); $u->execute(); $u->close();
return false;
}
$ok = hash_equals($tokenHash ?? '', hash('sha256', $code));
$a = $conn->prepare("UPDATE user_otps SET attempts=LEAST(attempts+1, 250) WHERE uotp_id=?");
$a->bind_param('i', $id); $a->execute(); $a->close();
if ($ok) {
$c = $conn->prepare("UPDATE user_otps SET consumed_at=NOW() WHERE uotp_id=?");
$c->bind_param('i', $id); $c->execute(); $c->close();
return true;
}
return false;
}
// ----- Determine TOTP status -----
$hasTotp = false; $secret = null;
if ($totpOn && file_exists($libTotp)) {
$stmt = $conn->prepare("SELECT secret_base32, enabled FROM user_totp WHERE user_id=? LIMIT 1");
$stmt->bind_param('i', $userId);
$stmt->execute();
$stmt->bind_result($secret, $enabled);
if ($stmt->fetch() && $enabled) $hasTotp = true;
$stmt->close();
}
// ----- Handle POST (TOTP or Email OTP) -----
$error = '';
$sent = false;
// Calculate initial cooldown (if a code was sent recently)
$cooldownRemain = $emailOn && !$hasTotp ? otp_seconds_until_send($conn, $userId, 'email', $cooldownSec) : 0;
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
// TOTP path
if (isset($_POST['totp']) && $hasTotp && file_exists($libTotp)) {
// [ADDED: CSRF + RL]
csrf_verify_or_fail($_POST['csrf'] ?? null, '/auth/login.php');
if ($rlActive && function_exists('rl_check_and_inc')) {
$r = rl_check_and_inc($conn, 'totp_verify', $usrToken, $TOTP_VERIFY_LIMIT, $TOTP_VERIFY_WINDOW_SEC, $TOTP_VERIFY_LOCK_SEC);
if (!$r['ok']) {
$error = "Too many attempts. Try again in {$r['locked_for']}s.";
}
}
if (!$error) {
$code = preg_replace('/\D+/', '', $_POST['totp'] ?? '');
if ($secret && function_exists('totp_verify') && totp_verify($secret, $code, 1)) {
if ($rlActive && function_exists('rl_reset')) rl_reset($conn, 'totp_verify', $usrToken);
if (function_exists('device_mark_trusted')) device_mark_trusted($conn, $userId, $trustDays);
page_redirect('/user/dashboard/index.php');
} else {
$error = 'Invalid authenticator code. Try again.';
}
}
}
// Email OTP send
if (isset($_POST['send_email_code']) && $emailOn && !$hasTotp) {
// [ADDED: CSRF + RL]
csrf_verify_or_fail($_POST['csrf'] ?? null, '/auth/login.php');
if ($rlActive && function_exists('rl_check_and_inc')) {
$r = rl_check_and_inc($conn, 'otp_send', $usrToken, $OTP_SEND_LIMIT, $OTP_SEND_WINDOW_SEC, $OTP_SEND_LOCK_SEC);
if (!$r['ok']) {
$error = "Too many code requests. Try again in {$r['locked_for']}s.";
}
}
if (!$error) {
if (!$userEmail) {
$error = 'No email on file for your account.';
} else if (!otp_can_send_again($conn, $userId, 'email', $cooldownSec)) {
$error = 'Please wait a moment before requesting a new code.';
$cooldownRemain = otp_seconds_until_send($conn, $userId, 'email', $cooldownSec);
} else {
if (otp_issue_and_send($conn, $userId, $userEmail)) {
$sent = true;
$cooldownRemain = otp_seconds_until_send($conn, $userId, 'email', $cooldownSec);
if ($cooldownRemain <= 0) $cooldownRemain = $cooldownSec;
} else {
$error = 'Could not send code right now. Please try again.';
}
}
}
}
// Email OTP verify
if (isset($_POST['email_code']) && $emailOn && !$hasTotp) {
// [ADDED: CSRF + RL]
csrf_verify_or_fail($_POST['csrf'] ?? null, '/auth/login.php');
if ($rlActive && function_exists('rl_check_and_inc')) {
$r = rl_check_and_inc($conn, 'otp_verify', $usrToken, $OTP_VERIFY_LIMIT, $OTP_VERIFY_WINDOW_SEC, $OTP_VERIFY_LOCK_SEC);
if (!$r['ok']) {
$error = "Too many attempts. Try again in {$r['locked_for']}s.";
}
}
if (!$error) {
$code = $_POST['email_code'] ?? '';
if (otp_verify($conn, $userId, $code)) {
if ($rlActive && function_exists('rl_reset')) rl_reset($conn, 'otp_verify', $usrToken);
if (function_exists('device_mark_trusted')) device_mark_trusted($conn, $userId, $trustDays);
page_redirect('/user/dashboard/index.php');
} else {
$error = 'Invalid or expired code. Request a new one and try again.';
$cooldownRemain = otp_seconds_until_send($conn, $userId, 'email', $cooldownSec);
}
}
}
}
// ======================= VIEW (polished & reassuring) =======================
include __DIR__ . '/header.php';
?>
<style>
/* Soft gradient background ribbon */
.secure-bg {
background: radial-gradient(1200px 400px at 50% -150px, rgba(7,98,137,.18), transparent 60%),
linear-gradient(180deg, rgba(7,98,137,.06), transparent 40%);
}
/* Card */
.secure-card {
border: 1px solid rgba(7,98,137,.12);
border-radius: 16px;
box-shadow: 0 10px 30px rgba(7,98,137,.08);
background: #fff;
}
/* Animated shield */
.shield-wrap { width: 84px; height: 84px; margin: 0 auto 8px; position: relative; }
.shield-pulse {
position: absolute; inset: -6px;
border-radius: 50%;
background: radial-gradient(circle, rgba(7,98,137,.15), transparent 60%);
animation: pulse 2.2s ease-in-out infinite;
}
@keyframes pulse { 0% { opacity:.8; transform: scale(.9); } 50% { opacity:.35; transform: scale(1.05);} 100% { opacity:.8; transform: scale(.9);} }
.shield {
width: 84px; height: 84px; border-radius: 22px;
background: linear-gradient(160deg, #0a7bab, #065c80);
display: grid; place-items: center; color: #fff;
box-shadow: 0 8px 18px rgba(7,98,137,.25);
}
.lock { width: 36px; height: 36px; animation: breathe 2.2s ease-in-out infinite; }
@keyframes breathe { 0%{ transform: translateY(0);} 50%{ transform: translateY(-2px);} 100%{ transform: translateY(0);} }
.steps { display:flex; gap:10px; justify-content:center; margin: 10px 0 16px;}
.step-dot { width:8px;height:8px;border-radius:50%; background:#d6e7ef; }
.step-dot.active { background:#076289; }
.step-label { font-size:12px; color:#6b7280; text-align:center; margin-top:-6px; }
.secure-input { padding:10px 12px; border:1px solid #e5eaee; border-radius:10px; }
.secure-input:focus { border-color:#0a7bab; box-shadow: 0 0 0 3px rgba(10,123,171,.12); }
.btn-secure-primary{
background:#076289; border-color:#076289; color:#fff !important;
}
.btn-secure-primary:hover, .btn-secure-primary:focus{
background:#fff; border-color:#076289; color:#076289 !important; box-shadow:0 0 0 3px rgba(7,98,137,.12);
}
.btn-rounded { border-radius: 999px; }
.muted { color:#6b7280; }
.dev-badge { background:#fff3cd;border:1px solid #ffeeba;color:#856404;border-radius:10px;padding:8px 10px; }
</style>
<div class="secure-bg">
<div class="container">
<div class="offset-md-4 col-md-4 pt-5">
<div class="form-signin text-center mt-0 pt-3 px-3 secure-card">
<div class="shield-wrap mt-3">
<div class="shield-pulse" aria-hidden="true"></div>
<div class="shield" aria-hidden="true">
<svg class="lock" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.7" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">
<rect x="4" y="10" width="16" height="10" rx="2" ry="2" fill="currentColor" opacity=".18"></rect>
<path d="M8 10V7a4 4 0 0 1 8 0v3" stroke="#fff"/>
<circle cx="12" cy="15" r="1.6" fill="#fff"></circle>
<path d="M12 16.6v1.8" stroke="#fff"></path>
</svg>
</div>
</div>
<h2 class="mb-1 fw-semibold">Security Check</h2>
<p class="muted mb-1">
Protecting your money is our first priority. <br> This quick step keeps your Bitcardo account safe.
</p>
<div class="steps" role="group" aria-label="Verification progress">
<span class="step-dot active"></span>
<span class="step-dot"></span>
<span class="step-dot"></span>
</div>
<div class="step-label">Step 1 of 3 — Verify it’s you</div>
<?php if (!empty($error)): ?>
<div class="alert alert-danger py-2 mt-3 mx-2"><?= htmlspecialchars($error) ?></div>
<?php endif; ?>
<div class="px-2 pb-3 pt-1">
<?php if ($hasTotp && $totpOn && file_exists($libTotp)): ?>
<p class="muted">Open your authenticator app and enter the 6-digit code.</p>
<form method="post" class="text-start mt-2">
<input type="hidden" name="csrf" value="<?= htmlspecialchars(csrf_token()) ?>">
<label for="totp" class="form-label ms-1">Authenticator code</label>
<input name="totp" id="totp" class="form-control secure-input" inputmode="numeric" autocomplete="one-time-code" required>
<button type="submit" class="w-100 btn btn-secure-primary btn-rounded btn-lg mt-3">Verify & continue</button>
</form>
<p class="mt-3"><small><a href="/security/totp/setup.php" class="text-decoration-none">Manage TOTP & backup codes</a></small></p>
<?php elseif ($emailOn): ?>
<?php if (!empty($_SESSION['__DEV_OTP_CODE__'])): ?>
<div class="dev-badge text-start mx-1 mt-2">
<strong>DEV ONLY</strong>: OTP is
<code><?= htmlspecialchars($_SESSION['__DEV_OTP_CODE__']) ?></code>
</div>
<?php unset($_SESSION['__DEV_OTP_CODE__']); ?>
<?php endif; ?>
<?php if (empty($sent) && $cooldownRemain === 0): ?>
<div class="muted mt-2">We’ll send a one-time code to <strong><?= htmlspecialchars($userEmail) ?></strong>.</div>
<form method="post" class="mt-3">
<input type="hidden" name="csrf" value="<?= htmlspecialchars(csrf_token()) ?>">
<input type="hidden" name="send_email_code" value="1">
<button type="submit" id="sendBtn" class="w-100 btn btn-secure-primary btn-rounded btn-lg">
Send verification code
</button>
</form>
<p class="muted mt-2" style="font-size:12px;">It’s fast and keeps your funds protected.</p>
<?php else: ?>
<div class="alert alert-primary text-start mt-3 mx-1">
Code sent to <strong><?= htmlspecialchars($userEmail) ?></strong>. <br>Expires in 10 minutes.
</div>
<form method="post" id="verifyForm" class="text-start mt-2">
<input type="hidden" name="csrf" value="<?= htmlspecialchars(csrf_token()) ?>">
<label for="email_code" class="form-label ms-1">Enter 6-digit code</label>
<input name="email_code" id="email_code" class="form-control secure-input" inputmode="numeric" autocomplete="one-time-code" required>
<button type="submit" class="w-100 btn btn-secure-primary btn-rounded btn-lg mt-3">Verify & continue</button>
</form>
<form method="post" id="resendForm" class="text-center mt-2">
<input type="hidden" name="csrf" value="<?= htmlspecialchars(csrf_token()) ?>">
<input type="hidden" name="send_email_code" value="1">
<button type="submit" id="resendBtn" class="btn btn-light"<?= $cooldownRemain > 0 ? ' disabled' : '' ?>>
Resend code
</button>
<span id="cooldownText" class="text-muted" style="<?= $cooldownRemain > 0 ? '' : 'display:none;' ?>">
(wait <span id="cooldownNum"><?= (int)$cooldownRemain ?></span>s)
</span>
</form>
<script>
(function(){
var remain = <?= (int)$cooldownRemain ?>;
var btn = document.getElementById('resendBtn');
var text = document.getElementById('cooldownText');
var num = document.getElementById('cooldownNum');
if (btn && text && num && remain > 0) {
btn.setAttribute('disabled','disabled');
text.style.display = '';
num.textContent = remain;
var t = setInterval(function(){
remain--;
if (remain <= 0) {
clearInterval(t);
btn.removeAttribute('disabled');
text.style.display = 'none';
num.textContent = '0';
} else {
num.textContent = remain;
}
}, 1000);
}
})();
</script>
<?php endif; ?>
<?php else: ?>
<div class="alert alert-secondary text-start mx-1 mt-2">
Verification is required, but no challenge methods are enabled for your account.
</div>
<a href="/user/dashboard/index.php" class="btn btn-secondary btn-rounded mt-2">Back to dashboard</a>
<?php endif; ?>
</div>
<div class="muted pb-4" style="font-size:12px;">
Secured by Bitcardo • Bank-grade encryption • You’re in control
</div>
</div>
</div>
</div>
</div>
<?php include __DIR__ . '/footer.php'; ?>
Выполнить команду
Для локальной разработки. Не используйте в интернете!