Brute force attack và phòng chống trong lập trình web PHP
|Brute force attack là gì?
Brute force attack là kiểu tấn công mà trong đó, kẻ tấn công lần lượt thử nhiều kết hợp username và mật khẩu khác nhau cho đến khi mò ra cặp username và mật khẩu đúng để xâm nhập vào hệ thống.
Đây là kiểu tấn công đơn giản, không cố giải mã thông tin nào, nhưng lại rất tốn thời gian, tuỳ thuộc vào mật khẩu có dễ mò ra hay không. Dù vậy, nếu kẻ tấn công sử dụng một công cụ tự động nào đó và sẵn sàng dành rất nhiều thời gian (dù là nhiều ngày nhiều tháng, ai mà biết được) để mò thì khả năng thành công khá lớn. Kiểu tấn công này có thể được sử dụng khi các nỗ lực tấn công khác không thành công.
Hướng dẫn người dùng cách phòng tránh
Người dùng có thể phần nào giảm bớt khả năng bị tấn công kiểu này nếu làm cho cặp username và mật khẩu của mình thật khó mò ra, cụ thể:
- Tránh đặt username thông dụng như admin, administrator,…
- Tránh đặt mật khẩu quá dễ đoán, như chuỗi ký tự liên tục (12345 chẳng hạn), tên, ngày sinh, số điện thoại,…
- Nên đặt username và mật khẩu càng dài càng tốt, mật khẩu nên chứa nhiều loại ký tự cùng lúc như chữ cái, chữ số, ký tự đặc biệt, ký tự viết hoa lẫn thường vì như vậy thời gian có thể mò ra lâu hơn, nhiều khả năng làm nản lòng kẻ tấn công.
- Thường xuyên thay đổi mật khẩu
Lập trình phòng chống brute force attack
Dù là viết ứng dụng desktop, web hay di động, chúng ta đều có thể chống brute force attack bằng một số cách sau:
- Không cho phép người dùng nhập mật khẩu quá đơn giản, dễ đoán
- Bảo mật đường dẫn đăng nhập
- Sử dụng CAPTCHA để tránh công cụ tự động
- Hạn chế số lần đăng nhập sai (có thể khoá tài khoản người dùng sau một số lần đăng nhập không thành công)
Minh hoạ chống brute force attack với ứng dụng web PHP
Mình chỉ làm một minh hoạ đơn giản cho bạn nào chưa có kinh nghiệm cần tham khảo. Minh hoạ này chỉ chú trọng đến chống brute force attack, chứ không quan tâm đến các lỗi bảo mật khác. Bạn có thể áp dụng cho ứng dụng của mình, nhưng nhớ là nó không tối ưu, bạn nên sửa lại cho phù hợp với nhu cầu.
Trong bài này, để tiết kiệm thời gian và công sức, mình sẽ sử dụng thư viện jQuery để viết mã kịch bản bên phía client,
Cơ sở dữ liệu
Mình sử dụng bảng đơn giản sau để lưu các tài khoản người dùng:
users(id, username, password, is_lock)
Trong đó:
- id là khoá chính của bảng
- username là tên đăng nhập, yêu cầu tên đăng nhập không trùng nhau giữa các người dùng
- password: mật khẩu. Trong minh hoạ này mình không dùng mã hoá cho mật khẩu. Trong ứng dụng thực tế, bạn phải mã hoá mật khẩu chứ không để ở dạng plain text.
- is_lock: quy ước 0 là tài khoản đang hiệu lực, 1 là tài khoản đã bị khoá, nếu tài khoản bị khoá thì người dùng không thể đăng nhập được.
Ngoài ra, cần 2 bảng sau để lưu số lần đăng nhập sai:
login_attempts_by_user(id, user_id, attempt_time): số lần đăng nhập sai theo từng user
login_attempts_by_ip(id, ip_address, attempt_time): số lần đăng nhập sai theo địa chỉ IP của user
Trong đó:
- id là khóa chính của bảng
- user_id: tham chiếu đến cột id của bảng users
- ip_address: địa chỉ IP mà người dùng sử dụng để cố gắng đăng nhập vào hệ thống
- attempt_time: UNIX timestamp thời điểm mà người dùng đăng nhập không thành công. Mỗi lần đăng nhập sai, sẽ lưu user_id hoặc ip_address kèm thời điểm tương ứng vào CSDL.
Yêu cầu về username và password
Như trên đã đề cập, bạn nên yêu cầu người dùng tránh những tên đăng nhập và mật khẩu dễ đoán. Bạn cần kiểm tra nhập liệu của người dùng khi đăng ký tài khoản.
Giả sử mình có yêu cầu sau về username và password:
- Username dài từ 6 đến 10 ký tự
- Username không được trùng nhau
- Username chỉ chứa các ký tự a-z, A-Z, _ và chữ số
- Username không được là admin
- Password phải dài từ 6 đến 10 ký tự
- Password phải chứa ít nhất một chữ viết thường, ít nhất một chữ viết hoa, ít nhất một chữ số và ít nhất một ký tự đặc biệt
- Password không được trùng với username
Mình minh hoạ bằng trang HTML đăng ký sau:
Mã HTML:
<!doctype html>
<html>
<head>
<meta charset="UTF-8">
<title>Demo chống brute force attack</title>
<script src="http://ajax.googleapis.com/ajax/libs/jquery/1.11.2/jquery.min.js"></script>
<style type="text/css">
.form-field {
display:block;
margin:10px 5px;
}
</style>
</head>
<body>
<form id="register_form" name="register_form"
method="post" action="doRegister.php">
<div class="form-field">
<label for="username">Username:</label>
<input type="text" id="username" name="username" />
</div>
<div class="form-field">
<label for="password">Password:</label>
<input type="password" id="password" name="password" />
</div>
<div class="form-field">
<label for="retype_password">Retype Password:</label>
<input type="password" id="retype_password" name="retype_password" />
</div>
<div class="form-field">
<input type="submit" name="submit" id="submit" value="Register">
</div>
</form>
</body>
</html>
Trong mã trên, khi người dùng bấm nút Register, toàn bộ dữ liệu đăng ký sẽ gởi lên file doRegister.php.
Kiểm tra bên phía client
Để kiểm tra username và password bên phía client, chúng ta sẽ thêm một số mã sau vào trang (phần in đậm):
<!doctype html>
<html>
<head>
<meta charset="UTF-8">
<title>Demo chống brute force attack</title>
<script src="http://ajax.googleapis.com/ajax/libs/jquery/1.11.2/jquery.min.js"></script>
<style type="text/css">
.form-field {
display:block;
margin:10px 5px;
}
.error {
color: #f00;
}
</style>
<script type="text/javascript">
function validateForm() {
var error = ""; // Chuỗi thông báo lỗi
var result = true; // Kết quả trả về của hàm
// nếu mọi thứ đều đúng trả về true,
// nếu có một yêu cầu không đúng, trả về false
// Các giá trị người dùng nhập vào
var username = $('#username').val();
var password = $('#password').val();
var retype_password = $('#retype_password').val();
// Các mẫu Regular Expression để kiểm tra
// Chỉ chứa ký tự chữ cái, số và _
var lettersAndNumbers = /\W/;
// Chứa ít nhất một chữ cái viết hoa
var upper = /[A-Z]/;
// Chứa ít nhất một chữ cái viết thường
var lower = /[a-z]/;
// Chứa ít nhất một chữ số
var number = /[0-9]/;
// Chứa ít nhất một ký tự đặc biệt
var special = /[ !"#$%&'()*+,\-./:;<=>?@[\\\]^_`{|}~]/;
// Kiểm tra độ dài username
if (username.length < 6 || username.length > 10) {
error = "Username phải có từ 6 đến 10 ký tự<br />";
result = false;
}
// Kiểm tra username chỉ chứa a-z, A-Z, _ và số 0-9
if (lettersAndNumbers.test(username)) {
error += "Username chỉ chứa các ký tự A-Z, a-z, 0-9, _<br />";
result = false;
}
// Kiểm tra username không được là admin
if (username.toLowerCase()=="admin") {
error += "Username không được là admin<br />";
result = false;
}
// Kiểm tra độ dài password
if (password.length < 6 || password.length > 10) {
error += "Password phải có từ 6 đến 10 ký tự<br />";
result = false;
}
// Kiểm tra password không được trùng username
if (username.toLowerCase()==password.toLowerCase()) {
error += "Username không được trùng password<br />";
result = false;
}
// Kiểm tra retype_password phải giống password
if (retype_password!=password) {
error += "Retype password phải giống Password<br />";
result = false;
}
// Kiểm tra password phải chứa ít nhất một ký tự viết hoa,
// một ký tự viết thường, một chữ số, một ký tự đặc biệt
if (!(upper.test(password) &&
lower.test(password) &&
number.test(password) &&
special.test(password))) {
error += "Password phải chứa ít nhất một ký tự hoa,";
error += "một ký tự thường, một chữ số, một ký tự đặc biệt<br />";
result = false;
}
// Nếu có lỗi, báo lỗi bằng cách đặt thông điệp lỗi
// vào div class error ngay trước form
if (!result) {
$('.error').html(error);
}
return result;
}
</script>
</head>
<body>
<div class="error"></div>
<form id="register_form" name="register_form"
method="post" action="doRegister.php"
onsubmit="return validateForm();">
<div class="form-field">
<label for="username">Username:</label>
<input type="text" id="username" name="username" />
</div>
<div class="form-field">
<label for="password">Password:</label>
<input type="password" id="password" name="password" />
</div>
<div class="form-field">
<label for="retype_password">Retype Password:</label>
<input type="password" id="retype_password" name="retype_password" />
</div>
<div class="form-field">
<input type="submit" name="submit" id="submit" value="Register">
</div>
</form>
</body>
</html>
Kiểm tra bên phía server
Ở phía client, chúng ta không thể kiểm tra username đã trùng hay chưa, vì việc đó cần kết nối đến database. Ngoài ra, cũng cần phải kiểm tra lại các ràng buộc đã kiểm bên phía client để đề phòng những người dùng hay táy máy vượt qua được các kiểm tra bên phía client. Để hiện thực, viết mã sau cho file doRegister.php:
<!doctype html>
<html>
<head>
<meta charset="UTF-8">
<title>Demo chống brute force attack</title>
</head>
<body>
<?php
// Bắt dữ liệu do client truyền lên
$username = isset($_POST['username'])
? $_POST['username']
: '';
$password = isset($_POST["password"])
? $_POST["password"]
: '';
$retype_password = isset($_POST["retype_password"])
? $_POST["retype_password"]
: '';
// Kiểm tra lại ràng buộc
$error = ""; // Thông điệp báo lỗi
$result = true; // Biến tạm lưu kết quả kiểm tra
// nếu có một yêu cầu không đáp ứng, sẽ sửa lại thành false
// Các mẫu Regular Expression để kiểm tra
$lettersAndNumbers = '/\W/';
$upper = '/[A-Z]/';
$lower = '/[a-z]/';
$number = '/[0-9]/';
$special = '/[ !"#$%&\'()*+,\\-.\/\:\;\<=\>?@[\\\\\\]^_`{|}~]/';
// username và password không được rỗng
if (strlen($username) < 6 || strlen($username) > 10) {
$error = "Username phải có từ 6 đến 10 ký tự<br />";
$result = false;
}
// Kiểm tra username chỉ chứa a-z, A-Z, _ và số 0-9
if (preg_match($lettersAndNumbers, $username)) {
$error .= "Username chỉ chứa các ký tự A-Z, a-z, 0-9, _<br />";
$result = false;
}
// Kiểm tra username không được là admin
if (strtolower($username)=="admin") {
$error .= "Username không được là admin<br />";
$result = false;
}
// Kiểm tra độ dài password
if (strlen($password) < 6 || strlen($password) > 10) {
$error .= "Password phải có từ 6 đến 10 ký tự<br />";
$result = false;
}
// Kiểm tra password không được trùng username
if (strtolower($username)==strtolower($password)) {
$error .= "Username không được trùng password<br />";
$result = false;
}
// Kiểm tra retype_password phải giống password
if ($retype_password!=$password) {
$error .= "Retype password phải giống Password<br />";
$result = false;
}
// Kiểm tra password phải chứa ít nhất một ký tự viết hoa,
// một ký tự viết thường, một chữ số, một ký tự đặc biệt
if (!(preg_match($upper, $password) &&
preg_match($lower, $password) &&
preg_match($number, $password) &&
preg_match($special, $password))) {
$error .= "Password phải chứa ít nhất một ký tự hoa,";
$error .= "một ký tự thường, một chữ số, một ký tự đặc biệt<br />";
$result = false;
}
// Kiểm tra xem username có trùng hay không
// Thông số kết nối CSDL, bạn thay lại cho phù hợp
$dbservername = 'servername';
$dbusername = 'username';
$dbpassword = 'password';
$dbname = 'database';
// Biến tạm xem có trùng username hay chưa
$is_username_exists = false;
$conn = new mysqli($dbservername, $dbusername, $dbpassword, $dbname);
if ($conn->connect_error) {
$error .= "Không kết nối được đến CSDL<br />";
$result = false;
} else {
$stmt = $conn->prepare('SELECT * FROM users WHERE username = ?');
$stmt->bind_param('s', $username);
$exe_ret = $stmt->execute();
if ($exe_ret) {
$stmt->store_result();
if ($stmt->num_rows > 0) {
// Đã trùng username
$is_username_exists = true;
$error .= "Username đã tồn tại<br />";
$result = false;
}
} else {
// Lỗi không thực thi được câu lệnh SQL
$error .= "Không kiểm tra được username đã tồn tại hay chưa<br />";
$result = false;
}
$stmt->close();
// Nếu username chưa tồn tại
if (!$is_username_exists) {
// Thêm người dùng vào database
$stmt = $conn->prepare('INSERT INTO users (username, `password`) VALUES (?, ?)');
$stmt->bind_param('ss', $username, $password);
$exe_ret = $stmt->execute();
if ($exe_ret) {
if ($stmt->affected_rows != 1) {
$error .= "Không thêm được người dùng vào CSDL<br />";
$result = false;
}
} else {
// Lỗi không thực thi được câu lệnh SQL
$error .= "Không thêm được người dùng vào CSDL<br />";
$result = false;
}
$stmt->close();
}
$conn->close();
}
// Nếu có lỗi, báo lỗi và
// cung cấp link để trở về trang đăng ký
if (!$result) {
echo $error;
echo '<br /><a href="register.html">Trở lại trang đăng ký</a>';
}
// Ngược lại, người dùng đã đăng ký thành công
else {
// Báo đăng ký thành công, cung cấp link đến trang đăng nhập
echo 'Đã đăng ký người dùng thành công';
// Bạn lưu ý trang đăng nhập là login.php, vì như minh họa bên dưới,
// trong file dùng captcha nên cần xử lý bằng mã PHP
echo '<br /><a href="login.php">Bấm để đăng nhập</a>';
}
?>
</body>
</html>
Ngoài ra, bạn cũng nên sử dụng CAPTCHA để tránh việc kẻ tấn công sử dụng công cụ tự động đăng ký hàng loạt tài khoản, sẽ làm nghẽn server của mình. Đây là vấn đề bảo mật khác nên mình không để cập trong ví dụ này.
Kiểm tra khi đăng nhập
Để chống brute force attack, ta cần chống việc nhập sai username và password nhiều lần liên tiếp, vì đó có khả năng là kẻ tấn công đang cố thử mò mật khẩu. Trong bài này, mình minh họa luồng xử lý theo lưu đồ sau:
Như vậy, mình sẽ kiểm tra IP của máy khách, nếu trong vòng 2 giờ đã có trên 10 lần đăng nhập sai từ IP này thì sẽ báo lỗi và không xử lý. Nếu trong vòng 2 giờ có trên 3 lần và dưới 10 lần đăng nhập sai từ cùng IP, CAPTCHA sẽ được hiển thị. Ngược lại, CAPTCHA không hiển thị. Nếu người dùng đăng nhập sai 5 lần liên tiếp, tài khoản của họ sẽ bị khóa.
Trong ví dụ này, CAPTCHA không hiện ngay từ đầu để tiết kiệm thời gian cho người dùng, chỉ khi người dùng nhập sai username/password trên 3 lần liên tiếp thì mới hiện CAPTCHA.
Cách dễ dàng nhất để dùng CAPTCHA là sử dụng dịch vụ reCAPTCHA của Google. Bạn có thể dùng tài khoản Google để đăng ký cho site của mình tại đây: https://www.google.com/recaptcha/admin#list. Từ khoảng cuối năm 2014, reCAPTCHA đã có phiên bản mới, không hiện ngay văn bản hay số khó đọc để xác minh người dùng nữa, mà chỉ hiện một hộp checkbox I’m not a robot. Người dùng chỉ việc bấm chọn vào checkbox là xong. Chỉ khi Google nghi ngờ thì khi bấm vào mới yêu cầu nhập một chuỗi CAPTCHA. Nếu dùng reCAPTCHA mới này, mình nghĩ có thể cho xuất hiện ngay từ đầu luôn cũng được, không cần phải sai 3 lần liên tiếp, vì việc bấm chọn checkbox không gây khó chịu cho người dùng.

Ngoài ra, bạn có thể search trên Google nhiều thư viện CAPTCHA khác.
Trong bài này, mình sẽ dùng thư viện CAPTCHA tại https://www.phpcaptcha.org/.
Dưới đây là mã cho trang login.php (có chú thích trong mã):
<?php
session_start();
include_once $_SERVER['DOCUMENT_ROOT'] . '/securimage/securimage.php';
// Đối tượng dùng để kiểm tra CAPTCHA, nếu có
$securimage = new Securimage();
// Thông số kết nối CSDL, bạn thay lại cho phù hợp
$dbservername = 'servername';
$dbusername = 'username';
$dbpassword = 'password';
$dbname = 'database';
// Đối tượng kết nối CSDL
$conn = new mysqli($dbservername, $dbusername, $dbpassword, $dbname);
// Hàm đếm số lần đăng nhập sai,
// Trả về số lần đăng nhập sai
function number_of_login_attempts_by_username($username) {
// Sử dụng đối tượng kết nối đã khởi tạo ở toàn cục
global $conn;
// Timestamp của 2 giờ trước, chúng ta chỉ
// đếm số lần đăng nhập sai trong vòng 2 giờ
$timestamp = time() - (2 * 60 * 60);
// Câu lệnh SQL đếm số lần đăng nhập sai trong vòng 2 giờ
$sql = "SELECT COUNT(*)
FROM login_attempts_by_user
WHERE user_id = (
SELECT id FROM users
WHERE username = ?
)
AND attempt_time > ?";
if ($stmt = $conn->prepare($sql)) {
$stmt->bind_param('si', $username, $timestamp);
$stmt->execute();
$stmt->bind_result($count_login_attempts);
$stmt->fetch();
$stmt->close();
return $count_login_attempts;
} else {
throw new Exception("Lỗi với CSDL");
}
}
// Hàm đếm số lần submit form login theo địa chỉ IP
// nhưng người dùng nhập sai username hoặc password
function number_of_login_attempts_by_ip_address($ip_address) {
// Sử dụng đối tượng kết nối đã khởi tạo ở toàn cục
global $conn;
// Timestamp của 2 giờ trước, chúng ta chỉ
// đếm số lần submit sai trong vòng 2 giờ
$timestamp = time() - (2 * 60 * 60);
// Câu lệnh SQL đếm số lần submit sai trong vòng 2 giờ
$sql = "SELECT COUNT(*)
FROM login_attempts_by_ip
WHERE ip_address = ?
AND attempt_time > ?";
if ($stmt = $conn->prepare($sql)) {
$stmt->bind_param('si', $ip_address, $timestamp);
$stmt->execute();
$stmt->bind_result($count_login_attempts);
$stmt->fetch();
$stmt->close();
return $count_login_attempts;
} else {
throw new Exception("Lỗi với CSDL");
}
}
// Hàm kiểm tra xem tài khoản username đã bị khóa hay chưa
// Trả về giá trị lưu trong cột is_lock
function check_is_lock($username) {
// Sử dụng đối tượng kết nối đã khởi tạo ở toàn cục
global $conn;
// Câu lệnh SQL đếm số lần đăng nhập sai trong vòng 2 giờ
$sql = "SELECT is_lock
FROM users
WHERE username = ?";
if ($stmt = $conn->prepare($sql)) {
$stmt->bind_param('s', $username);
$stmt->execute();
$stmt->bind_result($is_lock);
$stmt->fetch();
$stmt->close();
return $is_lock;
} else {
throw new Exception("Lỗi với CSDL");
}
}
// Hàm kiểm tra xem username có trong CSDL hay không
// bằng cách đếm số dòng trong bảng CSDL trùng username.
// Nếu username có tồn tại, sẽ đếm được 1 dòng, ngược lại là 0
// Trả về số dòng tìm được
function check_username_exists($username) {
// Sử dụng đối tượng kết nối đã khởi tạo ở toàn cục
global $conn;
// Câu lệnh SQL đếm số người dùng khớp với username
$sql = "SELECT COUNT(*)
FROM users
WHERE username = ?";
if ($stmt = $conn->prepare($sql)) {
$stmt->bind_param('s', $username);
$stmt->execute();
$stmt->bind_result($count_username);
$stmt->fetch();
$stmt->close();
return $count_username;
} else {
throw new Exception("Lỗi với CSDL");
}
}
// Hàm kiểm tra xem cặp username và password có khớp hay không
// bằng cách đếm số dòng trong CSDL trùng username và password
// Nếu đúng username và password, sẽ đếm được 1 dòng, ngược lại là 0
// Trả về số dòng tìm được
function check_password($username, $password) {
// Sử dụng đối tượng kết nối đã khởi tạo ở toàn cục
global $conn;
// Câu lệnh SQL đếm số người dùng khớp với username và password
$sql = "SELECT COUNT(*)
FROM users
WHERE username = ?
AND password = ?";
if ($stmt = $conn->prepare($sql)) {
$stmt->bind_param('ss', $username, $password);
$stmt->execute();
$stmt->bind_result($count_username);
$stmt->fetch();
$stmt->close();
return $count_username;
} else {
throw new Exception("Lỗi với CSDL");
}
}
// Hàm tăng số lần đăng nhập sai
function increase_login_attempt($username) {
// Sử dụng đối tượng kết nối đã khởi tạo ở toàn cục
global $conn;
// Câu lệnh SQL thêm ID người dùng và timestamp ngay lúc đăng nhập sai
$sql = "INSERT INTO login_attempts_by_user (user_id, attempt_time)
SELECT id, " . time() . "
FROM users WHERE username = ?";
if ($stmt = $conn->prepare($sql)) {
$stmt->bind_param('s', $username);
$stmt->execute();
$stmt->close();
} else {
throw new Exception("Lỗi với CSDL");
}
}
// Hàm tăng số lần submit form login nhưng đăng nhập sai
function increase_access_attempt($ip_address) {
// Sử dụng đối tượng kết nối đã khởi tạo ở toàn cục
global $conn;
$now = time();
// Câu lệnh SQL thêm địa chỉ IP người dùng và timestamp ngay lúc đăng nhập sai
$sql = "INSERT INTO login_attempts_by_ip (ip_address, attempt_time)
VALUES (?, ?)";
if ($stmt = $conn->prepare($sql)) {
$stmt->bind_param('si', $ip_address, $now);
$stmt->execute();
$stmt->close();
} else {
throw new Exception("Lỗi với CSDL");
}
}
// Hàm dùng để khóa tài khoản người dùng
function lock_account($username) {
// Sử dụng đối tượng kết nối đã khởi tạo ở toàn cục
global $conn;
// Câu lệnh SQL khóa tài khoản người dùng
$sql = "UPDATE users SET is_lock = 1 WHERE username = ?";
if ($stmt = $conn->prepare($sql)) {
$stmt->bind_param('s', $username);
$stmt->execute();
$stmt->close();
} else {
throw new Exception("Lỗi với CSDL");
}
}
// Hàm dùng để hiểm thị CAPTCHA
function show_captcha() {
?>
<div class="form-field">
<label for="captcha_code">CAPTCHA:</label>
<div class="captcha-image">
<!-- Hình ảnh CAPTCHA -->
<img id="captcha" src="/securimage/securimage_show.php" alt="CAPTCHA Image" />
</div>
<div class="captcha-input">
<!-- Ô textbox cho người dùng nhập CAPTCHA -->
<input type="text" id="captcha_code" name="captcha_code" size="10" maxlength="6" />
<!-- Link cho phép người dùng đổi CAPTCHA khác
nếu không đọc được CAPTCHA hiện tại -->
<a href="javascript:void(0);" onClick="document.getElementById('captcha').src = '/securimage/securimage_show.php?' + Math.random(); return false">Ảnh khác</a>
</div>
</div>
<?php
}
// Hàm dùng để lấy địa chỉ IP của người dùng
function get_client_ip() {
$ipaddress = '';
if (getenv('HTTP_CLIENT_IP'))
$ipaddress = getenv('HTTP_CLIENT_IP');
else if(getenv('HTTP_X_FORWARDED_FOR'))
$ipaddress = getenv('HTTP_X_FORWARDED_FOR');
else if(getenv('HTTP_X_FORWARDED'))
$ipaddress = getenv('HTTP_X_FORWARDED');
else if(getenv('HTTP_FORWARDED_FOR'))
$ipaddress = getenv('HTTP_FORWARDED_FOR');
else if(getenv('HTTP_FORWARDED'))
$ipaddress = getenv('HTTP_FORWARDED');
else if(getenv('REMOTE_ADDR'))
$ipaddress = getenv('REMOTE_ADDR');
else
$ipaddress = 'UNKNOWN';
return $ipaddress;
}
?>
<!doctype html>
<html>
<head>
<meta charset="UTF-8">
<title>Demo chống brute force attack</title>
<script src="http://ajax.googleapis.com/ajax/libs/jquery/1.11.2/jquery.min.js"></script>
<style type="text/css">
.form-field, .captcha-image, .captcha-input {
display:block;
margin:10px 5px;
}
.error {
color: #f00;
}
</style>
<script type="text/javascript">
// Hàm kiểm tra nhập liệu phía client,
// vì đã kiểm tra đăng nhập bên server
// nên ở trang này chỉ kiểm tra bắt buộc nhập các trường
function validateForm() {
var error = ""; // Chuỗi thông báo lỗi
var result = true; // Kết quả trả về của hàm
// nếu mọi thứ đều đúng trả về true,
// nếu có một yêu cầu không đúng, trả về false
// Các giá trị người dùng nhập vào
var username = $('#username').val();
var password = $('#password').val();
// Kiểm tra độ dài username
if (username.length < 6 || username.length > 10) {
error = "Username phải có từ 6 đến 10 ký tự<br />";
result = false;
}
// Kiểm tra độ dài password
if (password.length < 6 || password.length > 10) {
error += "Password phải có từ 6 đến 10 ký tự<br />";
result = false;
}
// Nếu có trường nhập CAPTCHA,
// CAPTCHA không được rỗng
if ($('#captcha_code').length) {
if (!$('#captcha_code').val()) {
error += "Phải nhập CAPTCHA<br />";
result = false;
}
}
// Nếu có lỗi, báo lỗi bằng cách đặt thông điệp lỗi
// vào div class error ngay trước form
if (!result) {
$('.error').html(error);
}
return result;
}
</script>
</head>
<?php
// Dữ liệu do client truyền lên
$username = isset($_POST['username'])
? $_POST['username']
: '';
$password = isset($_POST["password"])
? $_POST["password"]
: '';
$captcha_code = isset($_POST["captcha_code"])
? $_POST["captcha_code"]
: '';
// Lấy địa chỉ IP của người dùng
$client_ip_address = get_client_ip();
// Nếu không lấy được địa chỉ IP của người dùng
// bỏ qua hoặc báo lỗi tùy ý muốn của bạn
if ($client_ip_address == "UNKNOWN") {
// ...
}
// Biến tạm để báo hiện CAPTCHA nếu đã trên 3 lần đăng nhập sai
$is_over_3_attempts = false;
// Kiểm tra số lần đăng nhập không thành công từ IP này
$number_of_login_attempts_by_ip_address = number_of_login_attempts_by_ip_address($client_ip_address);
// Kiểm tra xem IP của người dùng đã có trên 10 lần đăng nhập sai,
// nếu có thì báo lỗi và dừng xử lý
if ($number_of_login_attempts_by_ip_address >= 10) {
die("<body>IP của bạn đã bị chặn do đăng nhập sai nhiều lần.<br />
Hãy chờ trong 2 giờ rồi thử lại.</body>");
}
// Ngược lại, nếu đã có từ 3 lần đăng nhập sai thì đặt
// biến tạm thành true để hiện CAPTCHA
else if ($number_of_login_attempts_by_ip_address >= 3) {
$is_over_3_attempts = true;
}
// Kiểm tra xem có bắt được cả username và password
// do client truyền lên hay không, nếu có thì
// người dùng đã bấm Login nên cần xử lý, ngược lại,
// chỉ hiện form đăng nhập
if (!empty($username) && !empty($password)) {
// Nếu người dùng tại IP này submit form login
// từ lần thứ 4 liên tiếp, thì CAPTCHA đã xuất hiện
if ($number_of_login_attempts_by_ip_address >=4) {
// Nếu đúng thì kiểm tra CAPTCHA
// Nếu sai CAPTCHA thì báo lỗi và dừng xử lý
if (!$securimage->check($captcha_code)) {
die("<body>CAPTCHA không đúng.</body>");
}
}
// Nếu CAPTCHA đúng, hoặc người dùng tại IP này submit form
// dưới 4 lần liên tiếp.
// Kiểm tra xem username có tồn tại trong CSDL hay không
if (check_username_exists($username)) {
// Kiểm tra xem username này đã bị khóa hay chưa
if (check_is_lock($username)) {
die("<body>Tài khoản của bạn đã bị khóa</body>");
} else {
// Tài khoản chưa bị khóa, kiểm tra số lần đăng nhập sai
// Nếu đã trên 5 lần đăng nhập sai thì khóa tài khoản và dừng xử lý
if (number_of_login_attempts_by_username($username) >= 5) {
lock_account($username);
die("<body>Tài khoản của bạn đã bị khóa.</body>");
}
// Nếu chưa tới 5 lần đăng nhập sai
// thì kiểm tra xem đúng password hay không.
// Nếu người dùng nhập đúng username lẫn password
if (check_password($username, $password)) {
// Báo đăng nhập thành công, chuyển hướng trang,
// khởi tạo session,...
echo("<body>Bạn đã đăng nhập thành công</body>");
}
// Ngược lại, người dùng nhập đúng username nhưng sai password
// thì tăng số lần đăng nhập sai theo IP lẫn theo username
else {
echo "Tên đăng nhập/mật khẩu không đúng";
increase_login_attempt($username);
increase_access_attempt($client_ip_address);
}
}
} else {
// Người dùng nhập sai username
echo "Tên đăng nhập/mật khẩu không đúng";
// Tăng số lần đăng nhập sai theo IP
increase_access_attempt($client_ip_address);
}
}
?>
<body>
<div class="error"></div>
<h1>Login</h1>
<form id="login_form" name="login_form" method="post" onsubmit="return validateForm();">
<div class="form-field">
<label for="username">Username:</label>
<input type="text" id="username" name="username" />
</div>
<div class="form-field">
<label for="password">Password:</label>
<input type="password" id="password" name="password" />
</div>
<?php
// Nếu đã có từ 3 lần đăng nhập sai
// với cùng địa chỉ IP thì hiển thị CAPTCHA
if ($is_over_3_attempts) {
show_captcha();
}
?>
<div class="form-field">
<input type="submit" name="submit" id="submit" value="Login">
</div>
</form>
</body>
</html>
<?php
$conn->close();
?>