Lập trình đăng nhập, ghi nhớ đăng nhập và quên mật khẩu an toàn trên PHP

Khi lập trình một website hay một ứng dụng có chức năng đăng nhập, ghi nhớ đăng nhập, quên mật khẩu... nhiều lập trình viên không chú ý nhiều đến việc làm sao mã hóa password của người dùng để không bị dịch ngược hoặc xác minh các chức năng này để phòng chống các lỗi bảo mật phổ biến như SQL Injection, XSS, CSRF,... Và bài viết này sẽ chia sẽ một số kinh nghiệm về lập trình an toàn trên PHP.

Những sai lầm bảo mật phổ biến khi lập trình:
  • Không mã hóa password khi lưu trữ hoặc mã hóa theo các phương thức không bảo mật như MD5, SHA.
  • Không có chính sách đặt mật khẩu, quên mật khẩu, nhớ mật khẩu rõ ràng.
Những nguyên tắc vàng bảo mật:
  • Luôn đặt bảo mật của người dùng lên hàng đầu.
  • Không bao giờ phải sử dụng hoặc lưu mật khẩu của user dưới dạng không mã hóa.
  • Luôn có chính sách bảo mật rõ ràng, hợp lí và tuân thủ nó nghiêm khắc.
  • Yêu cầu người khai đủ thông tin để có thể xác định chính xác chủ sở hữu khi có sự cố về bảo mật.
Chọn phương thức mã hóa nào để lưu trữ password trong database?

Sai lầm phổ biến:

  • #1: Không mã hóa password khi lưu trữ (cực kì nguy hiểm).
  • #2: Sử dụng những phương thức mã hóa kém bảo mật như MD5, SHA, ...

Các phương thức mã hóa an toàn?

Hiện tại chỉ có 4 phương thức mã hóa password được tin tưởng bởi các chuyên gia mã hóa và bảo mật là:

  • bcrypt
  • Argon2 (là phương thức mã hóa tốt nhất)
  • scrypt
  • PBKDF2

Nên chọn phương thức nào?

Với nhiều ứng dụng lớn và cần bảo mật cao, Argon2 có lẽ là tốt nhất, nhưng để tiện lợi sử dụng mà lại dễ lập trình, không cần cài đặt thêm ta nên dùng bcrypt.

Giải pháp:

Với nhiều nhà lập trình, thuật toán bcrypt sẽ dễ dàng và thuận tiện hơn, nên tôi chỉ giới thiệu về bcrypt với một đoạn code PHP mã hóa password:

$salt = strtr(base64_encode(mcrypt_create_iv(16, MCRYPT_DEV_URANDOM)), '+', '.');
$salt = sprintf("$2y$%02d$", 10) . $salt; //$2y$ là thuật toán BlowFish, 10 là độ dài của key mã hóa.
$password = crypt($password, $salt);
//Từ đây lưu $password vào database.
Bảo mật tính năng đăng nhập (Login):

Sai lầm phổ biến:

  • #1: Sử dụng các phương thức so sánh password không an toàn, như toán tử == (toán tử chỉ so sánh giá trị bằng nhau, ví dụ khi so sánh 0 với 0x123 sẽ trả vể true)
  • #2: So sánh password trực tiếp bằng câu lệnh SELECT trên SQL, vd: SELECT * FROM users WHERE email = $email, password = $password (rất dễ bị SQL Injection)

Giải pháp:

$password = mysqli_query("SELECT password FROM users WHERE email = '".$_POST['email']."'); 
$password = mysqli_fetch_array($password)['password'];
//Lấy password từ trong database.

$check = crypt($_POST['password'], $password);
//Mã hóa password của người dùng với key chính là password trong database, nếu password đúng thì sẽ trả về đoạn hash giống hệt trong database.
if (hash_equals($check, $password)) {
   //Đăng nhập thành công.
}
else {
   //Đăng nhập không thành công.
}

Một vài kinh nghiệm từ đoạn code trên:

  • Không bao giờ sử dụng câu lệnh SQL để kiểm tra password trực tiếp, bạn sẽ thấy tôi tiến hành lấy giá trị password mã hóa ra biến rồi kiểm tra bằng PHP.

  • Khi kiểm tra 2 đoạn mã hóa bằng PHP, không bao giờ xài toán tử ==, nên xài === và tốt nhất là hash_equals như tôi đã xài bên trên (hàm này sẽ so sánh từng bit trong 2 đoạn mã hóa).

  • Ngoài ra, để tăng tính bảo mật, bạn cần kiểm tra tên đăng nhập hoặc email (nếu là email thì phải dạng đúng của email, nếu là id số thì bắt buộc phải là số, ...) và sử dụng hàm mysqli_real_escape_string trước khi tiến hành lấy password để tránh lỗi SQL Injection cũng như các lổ hổng bảo mật khác.

Bảo mật tính năng Ghi nhớ đăng nhập (Remember Me):

Đây cũng là một tính năng rất quan trọng cần được bảo mật, vì nó có thể thay cho mật khẩu cấp quyền đăng nhập vào hệ thống, ứng dụng.

Sai lầm phổ biến:

  • #1: Sử dụng mỗi cookie để nhớ mật khẩu, ví dụ: remember_me_cookie = 1234. Và các hacker chỉ cần thay đổi cookie thành remember_me_cookie = 1 là đã có thể trở thành admin hoặc bất kì ai khác (:D).
  • #2: Có sử dụng token remember_me_token được tạo và lưu trong database khi người dùng chọn chức năng này để xác minh cookie, nhưng lại không biết rằng cookie hoàn toàn có thể đoán hay brute force (và vấn đề chỉ là thời gian).

Giải pháp:

Trong sai lầm phổ biến #2 nhắc tới bên trên, tuy cách này có vẻ khá là an toàn, nhưng vẫn tồn tại lổ hổng cho phép khai thác (thời gian :D), và bây giờ tôi sẽ tối ưu hóa thêm cách này để tăng tính bảo mật:

Ngoài remember_me_token, bạn tạo thêm một cột trong database có tên remember_me_identify. Như vậy cấu trúc database chuyên cho tính năng này như sau:

| Field                | Type    |
| id                   | int(11) |
| user_id              | int(11) |
| remember_me_identify | text    |
| remember_me_token    | text    |
| user_agent           | text    |
  • Khi người dùng chọn tính năng ghi nhớ đăng nhập, tiến hành tạo ra 2 chuỗi ngẫu nhiên là identifycode, lưu giá trị identify vào remember_me_identify và giá trị mã hóa của remember_me_code vào remember_me_token (nên sử dụng cách mã hóa đã giới thiệu bên trên).

  • Set cookie remember_me cho trình duyệt của người dùng với cấu trúc identify:code (với identify và code là 2 giá trị được tạo ngẫu nhiên từ bên trên).

  • Và khi người dùng truy cập vào trang mà chưa đăng nhập và có cookie remember_me, ta tiến hành kiểm tra cookie này giống hệt như kiểm tra đăng nhập và dùng đoạn code giống phần trên (identify là tên đăng nhập, code là password người dùng nhập vào, remember_me_token trong database là mật khẩu của tài khoản được mã hóa).

Một số bổ sung bảo mật cho tính năng Ghi nhớ đăng nhập:

  • Tạo thêm một cột user_agent trong database để lưu user_agent của trình duyệt user khi sử dụng tính năng này, vì cookie chỉ được set cho một trình duyệt xác định, nên nếu user_agent khác với user_agent đã lưu, có khả năng là do hacker đã chiếm được cookie của người dùng, và ta sẽ xóa đi hoặc khóa tài khoản.

  • Nên set cookie remember_me với các giá trị httponly = truesecure = true (nếu sử dụng https) để tăng bảo mật cho cookie của người dùng.

Bảo mật tính năng quên mật khẩu

Sai lầm phổ biến:

  • #1: Sử dụng câu hỏi bảo mật, thậm chí câu hỏi bảo mật ít người biết. (Ví dụ: Mẹ bạn tên gì? Chắc bạn bè bạn hay bất cứ ai cũng có thể biết :D)
  • #2: Gửi các link xác minh mật khẩu qua các bước trung gian như email, điện thoại (ai đó có thể nghe lén, xem trộm?)

Giải pháp:

  • Yêu cầu người dùng trực tiếp lên văn phòng để reset mật khẩu.

  • Yêu cầu người dùng cung cấp các giấy tờ tùy thân như Chứng minh thư, Giấy phép lái xe, VISA,... để xác minh và reset mật khẩu.

  • Kết hợp xác minh điện thoại và email, người dùng phải chứng minh sở hữu hai thông tin này mới được reset mật khẩu.

Tính năng quên mật khẩu cũng vô cùng quan trọng như 2 tính năng trên vì có thể cho phép đổi mật khẩu của tài khoản người dùng và từ đó có quyền đăng nhập.

Và những giải pháp này có vẻ quá khó khăn với nhiều ứng dụng nhỏ, vậy với những ứng dụng không yêu cầu tính bảo mật quá khắc khe, reset mật khẩu qua email là một phương thức có thể chấp nhận và dễ triển khai, tiện lợi cho người dùng.

Về mặt kĩ thuật: Bảo mật tính năng cũng giống hệt như tính năng quên mật khẩu:

  • Tạo cấu trúc bảng trong database để lưu các thông tin về quy trình quên mật khẩu.

  • Khi người dùng yêu cầu mật khẩu, tạo ngẫu nhiên 2 chuỗi code và identify, giá trị của identify và giá trị mã hóa của code được lưu vào database.

  • Tiến hành gửi email với link reset mật khẩu có dạng example.com/reset-password?code=identify:code (identify:code nên mã hóa theo phương thức base64)

  • Khi người dùng click vào link tiến hành tách identify và code và tiến hành kiểm tra như tính năng Ghi nhớ đăng nhập.

Một số bổ sung bảo mật cho tính năng quên mật khẩu:

  • Gửi email được mã hóa để tránh bị xem lén.
  • Tính năng quên mật khẩu trên ứng dụng cần có captcha (tránh robot, spam) và có kiểm tra lần cuối gửi email quên mật khẩu (nếu quá gần thời gian yêu cầu, ví dụ 5 phút, thì không cho sử dụng tính năng này).
  • Mã quên mật khẩu trong database cần có một thời hạn nhất định, nếu người dùng không click vào link để reset mật khẩu sau một thời gian, thì mã này sẽ hết hạn.
Kết luận:

Các phương pháp về kĩ thuật mà tôi trình bày ở trên sẽ giúp các bạn bảo mật thêm các ứng dụng của mình. Tuy nhiên, tất cả các phương pháp kĩ thuật chỉ là một phần nhỏ của an toàn và bảo mật thông tin, bạn nên có những chính sách hợp lí và thi hành nó một cách thật chính xác, triệt để.

Dây rút nhựa tp hcm