diff --git a/api/endpoints/netscan.php b/api/endpoints/netscan.php index eefe78b..ed71a56 100644 --- a/api/endpoints/netscan.php +++ b/api/endpoints/netscan.php @@ -9,7 +9,7 @@ if ($method !== 'POST') { } $reqKey = $_SERVER['HTTP_X_REGISTRATION_KEY'] ?? ''; -if ($reqKey !== NETSCAN_KEY) { +if (!hash_equals(NETSCAN_KEY, $reqKey)) { http_response_code(401); echo json_encode(['error' => 'Unauthorized']); exit; } diff --git a/public_html/login.php b/public_html/login.php index ff56d48..c4ee262 100644 --- a/public_html/login.php +++ b/public_html/login.php @@ -5,27 +5,47 @@ require_once __DIR__ . '/../api/config.php'; session_start(); if (!empty($_SESSION['jarvis_token'])) { header('Location: /'); exit; } $error = ''; +// ── Login rate limiting (Redis, per client IP) ──────────────────────────────── +// Blocks brute force: 10 failed attempts within 15 min -> locked out for 15 min. +$clientIp = $_SERVER['HTTP_CF_CONNECTING_IP'] ?? $_SERVER['HTTP_X_FORWARDED_FOR'] ?? $_SERVER['REMOTE_ADDR'] ?? 'unknown'; +$clientIp = trim(explode(',', $clientIp)[0]); +$rl = null; +try { + $rl = new Redis(); + $rl->connect('127.0.0.1', 6379, 1.5); +} catch (Throwable $e) { $rl = null; } // fail open if Redis is down +$rlKey = 'login_fail:' . $clientIp; +$RL_MAX = 10; $RL_WINDOW = 900; + if ($_SERVER['REQUEST_METHOD'] === 'POST') { - $u = trim($_POST['username'] ?? ''); - $p = $_POST['password'] ?? ''; - if ($u && $p) { - $pdo = new PDO('mysql:host=' . DB_HOST . ';dbname=' . DB_NAME . ';charset=utf8mb4', - DB_USER, DB_PASS, - [PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION]); - $row = $pdo->prepare('SELECT * FROM users WHERE username=? LIMIT 1'); - $row->execute([$u]); - $user = $row->fetch(PDO::FETCH_ASSOC); - if ($user && password_verify($p, $user['password_hash'])) { - $token = bin2hex(random_bytes(32)); - $_SESSION['jarvis_token'] = $token; - $_SESSION['jarvis_user_id'] = $user['id']; - $_SESSION['jarvis_name'] = $user['display_name']; - $pdo->prepare('UPDATE users SET last_seen=NOW() WHERE id=?')->execute([$user['id']]); - header('Location: /'); - exit; - } - $error = 'ACCESS DENIED'; - } else { $error = 'ENTER CREDENTIALS'; } + $fails = ($rl && $rl->exists($rlKey)) ? (int)$rl->get($rlKey) : 0; + if ($fails >= $RL_MAX) { + $error = 'TOO MANY ATTEMPTS — LOCKED'; + } else { + $u = trim($_POST['username'] ?? ''); + $p = $_POST['password'] ?? ''; + if ($u && $p) { + $pdo = new PDO('mysql:host=' . DB_HOST . ';dbname=' . DB_NAME . ';charset=utf8mb4', + DB_USER, DB_PASS, + [PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION]); + $row = $pdo->prepare('SELECT * FROM users WHERE username=? LIMIT 1'); + $row->execute([$u]); + $user = $row->fetch(PDO::FETCH_ASSOC); + if ($user && password_verify($p, $user['password_hash'])) { + if ($rl) $rl->del($rlKey); + session_regenerate_id(true); + $token = bin2hex(random_bytes(32)); + $_SESSION['jarvis_token'] = $token; + $_SESSION['jarvis_user_id'] = $user['id']; + $_SESSION['jarvis_name'] = $user['display_name']; + $pdo->prepare('UPDATE users SET last_seen=NOW() WHERE id=?')->execute([$user['id']]); + header('Location: /'); + exit; + } + if ($rl) { $rl->incr($rlKey); $rl->expire($rlKey, $RL_WINDOW); } + $error = 'ACCESS DENIED'; + } else { $error = 'ENTER CREDENTIALS'; } + } } ?>