Kỹ thuật tấn công CSRF và cách chống CSRF trong PHP

Vấn đề bảo mật website với dân coder có thể nói là rất quan trọng. Với những lập trình viên dày dặn kinh nghiệm thì họ sẽ có những cách xử lý khôn khéo để có thể bảo mật được dự án của mình, còn những bạn mới học nghề thì đây lại là vấn đề rất khó khăn. Về chủ đề bảo mật website thì rất nhiều thứ cần phải nói đến, và đương nhiên trong một bài tôi không thể nào trình bày hết được. Vì thế tôi sẽ tạo một serie với tên là “Bảo Mật Website Cho Coder”, trong chủ đề này tôi sẽ trình bày những vấn đề bảo mật liên quan đến dân code nhé.

Kỹ thuật tấn công CSRF và cách chống CSRF

Và bài đầu tiên cũng là bài mở hàng tôi sẽ nói về kỹ thuật tấn công CSRF (Cross-site Request Forgery), trước tiên ta tìm hiểu khái niệm về kỹ thuật tấn công này đã nhé.

1. Kỹ thuật tấn công CSRF là gì?

Để trả lời câu hỏi này tôi sẽ trình bày dưới dạng đọc hiểu, tức là tôi hiểu như thế nào thì tôi sẽ trình bày lại như vậy cho các bạn, nên sẽ có những sai sót và rất mong các bạn bỏ qua, ủng hộ góp ý kiến để tôi có thể hoàn thành serie này tốt đẹp hơn.

Kỹ thuật tấn công CSRF hay còn gọi là kỹ thuật tấn công “Cross-site Request Forgery“, nghĩa là kỹ thuật tấn công giả mạo chính chủ thể của nó. Tôi sẽ lấy một ví dụ thế này cho các bạn dễ hình dung.

Giả sử trong hệ thống các bạn có một action xử lý xóa người dùng với url như sau: domain.com/delete.php?id=12 (Xóa user có id = 12). Như vậy giả sử một người nào đó biết được URL này thì họ sẽ hack được, và họ sẽ lợi dụng chính admin của hệ thống. Họ sẽ gửi một email với nội dung là một hoặc nhiều thẻ hình ảnh (IMG) với SRC là url đó và mỗi hình có 1 id khác nhau, như vậy nếu admin đọc cái email đó thì trường hợp admin đang login vào hệ thống thì admin đã vô tình xóa đi những user như trong SRC của các hình trên. Đây là một ví dụ nho nhỏ điển hình thôi chứ trong thực tế ai lại đi làm chương trình xóa người dùng mà lại để cái ID to đùng trên kia :D, ấy mà đôi khi những bạn non tay nghề lại mặc phải đấy.

Còn nhiều trường hợp khác nữa mà tôi nghĩ tới đây bạn đã hiểu được nó là gì rồi, nên ta đi thẳng vào vấn đề cách phòng chống nhé.

2. Cách phòng chống tấn công CSRF

Thông thường để tránh tấn công ta sẽ chia làm hai đối tượng, một là đối tượng coder và hai là đối tượng người dùng cuối (user).

Với đối tượng người dùng cuối thì:

  • Hạn chế sử dụng login vào hệ thống khi nói chuyện tiếp xúc với những người lạ qua các kênh khác nhau, những email không rõ nguồn gốc. Khi không dùng hệ thống thì lập tức logout
  • Nên login vào một máy riêng và không cho người thứ 2 tiếp xúc với máy đó.
  • Thay đổi mật khẩu liên tục, và chọn những mật khẩu khó đoán, có kỹ tự đặc biệt. Vì hiện nay có rất nhiều phần mềm dò pass (Không liên quan CSRF nhưng đưa vào cho nó nhiều chữ :v).

Với đối tượng coder:

  • Thực hiện tạo những token auto và random với từng máy, từng trình duyệt và thiết lập thời gian sống cho token đó.
  • Không sử dụng phương thức GET với những request mà có ảnh hưởng đến CSDL.
  • Khi lấy dữ liệu từ người dùng thì kiểm tra chặt chẽ
  • URL trong admin càng khó nhớ càng bí hiểm càng tốt :D.

Phía trên là những đề nghị của bản thân mình, bạn có thể nghe theo hoặc không :D. Bây giờ ta sẽ đi tìm hiểu cách tạo token trong php để chống hack CSRF nhé.

3. Tạo token trong php để chống hack CSRF

Trong phần này tôi sẽ tạo một lớp Csrf để chống hack với hai phương thức POST và GET. Như vậy các bạn sẽ dễ dàng sử dụng nó. Và xin lưu ý rằng đây là một ví dụ để các bạn tham khảo vì nó có chống được tuyệt đối hay không thì bản thân mình không dám chắc :D.

class Csrf
{
    // Tên kiểm tra token
    private $_csrf_token_name    = 'cms-token-name';
    
    // Thời gian sống của session, 3600 = 1h
    private $_csrf_time_live = 3600;
    
    private $_csrf_value = '';
    // Hàm khởi tạo, có hai tham số
    // - $use_token : nếu true thì có sử dụng validate token
    // - $token_post: nếu true thì nếu method = post thì sẽ validate token trong form
    // - $token_get : nếu true thì nếu method = get thì sẽ validate token trên url
    function __construct($use_token = true, $token_post = false, $token_get = false)
    {
        // Nếu không muốn sử dụng token thì dừng
        if (!$use_token){
            return;
        }
        
        // Tạo CSRF Token
        $this->__create_csrf_token();
        
        // Nếu có validate cho phương thức POST
        if ($token_post && !$this->__validate_post()){
            die ('Token sai');
        }
        
        // Nếu có validate cho phương thức GET
        if ($token_get && !$this->__validate_get()){
            die ('Token sai');
        }
    }
    
    // Mỗi người dùng sẽ có một mã token riêng biệt,
    // nên trong hàm này ta sẽ tạo một quy luật riêng để tạo token nhé
    private function __create_csrf_token()
    {
        // Khởi tạo token name
        $this->_csrf_token_name = 'token'.md5($_SERVER['REMOTE_ADDR'].$_SERVER['HTTP_USER_AGENT'].'@#$%^+&*(-)');
        
        // Nếu token chưa dược khởi tạo thì khởi tạo
        if (!isset($_COOKIE[$this->_csrf_token_name]))
        {
            // Tạo token
            $token = md5($_SERVER['REMOTE_ADDR'].$_SERVER['HTTP_USER_AGENT']);
            
            // Lưu token trong 1h
            setcookie($this->_csrf_token_name, $token, time() + $this->_csrf_time_live);
        }
        else{
            $token = $_COOKIE[$this->_csrf_token_name];
            setcookie($this->_csrf_token_name, $token, time() + $this->_csrf_time_live);
        }
        $this->_csrf_value = $token;
    }
    
    // Kiểm tra phương thức POST
    private function __validate_post()
    {
        // Kiểm tra nếu phương thức hiện tại là POST
        if ($_SERVER['REQUEST_METHOD'] == 'POST')
        {
            // Kiểm tra có tồn tại token không
            if (!isset($_POST[$this->_csrf_token_name]) || !isset($_COOKIE[$this->_csrf_token_name])){
                return false;
            }
            // Nếu tokeon không phù hợp
            else if ($_POST[$this->_csrf_token_name] != $_COOKIE[$this->_csrf_token_name]){
                return false;
            }
        }
        return true;
    }
    
    
    // Kiểm tra phương thức GET
    private function __validate_get()
    {
        // Kiểm tra nếu phương thức hiện tại là POST
        if ($_SERVER['REQUEST_METHOD'] == 'GET')
        {
            // Kiểm tra có tồn tại token không
            if (!isset($_GET[$this->_csrf_token_name]) || !isset($_COOKIE[$this->_csrf_token_name])){
                return false;
            }
            // Nếu tokeon không phù hợp
            else if ($_GET[$this->_csrf_token_name] != $_COOKIE[$this->_csrf_token_name]){
                return false;
            }
        }
        return true;
    }
    
    // Lấy token name
    function get_token_name(){
        return $this->_csrf_token_name;
    }
    
    // Lấy token value
    function get_token_value(){
        return $this->_csrf_value;
    }
    
    // Tạo link có token
    function create_link($url){
        return $url.'?'.$this->_csrf_token_name.'='.$this->_csrf_value;
    }
}

Trong phần comment tôi đã giải thích rất kỹ rồi. Chỉ có một điều lưu ý là ở hàm khởi tạo đối tượng CSRF tôi đã đưa vào 3 thông số, mỗi thông số có tác dụng như thế nào thì trong file tôi đã giải thích rồi nhé.

Và một điều nữa là tên token tôi để hơi dài, như vậy sẽ an toàn hơn nhưng nhìn URL sẽ xấu đi :D. Bạn có thể chỉnh lại tên token cố định cũng được.

Cách sử dụng:

/*Cách Sử Dụng*/
// Thiết lập giá trị cấu hình cho hàm khởi tạo
// Sau đó hệ thống sẽ tự check token theo cấu hình mà bạn config
$token = new Csrf(true, true, true);
// Lấy token name
echo $token->get_token_name();
// Lấy token value
echo $token->get_token_value();
// Lấy URL có token
echo $token->create_link('domain.com/admin.php');
// Bây giờ bạn có thể đưa nó vào hệ thống

Và đây là tôi gộp vào một file chung:

/*
 * @Author: thehalfheart
 * @website: freetuts.net
 * @Email: thehalfheart@gmail.com
 * @Desc : Chống hack csrf bằng token
 */
/*Cách Sử Dụng*/
// Thiết lập giá trị cấu hình cho hàm khởi tạo
// Sau đó hệ thống sẽ tự check token theo cấu hình mà bạn config
$token = new Csrf(true, true, true);
// Lấy token name
echo $token->get_token_name();
// Lấy token value
echo $token->get_token_value();
// Lấy URL có token
echo $token->create_link('domain.com/admin.php');
// Bây giờ bạn có thể đưa nó vào hệ thống
class Csrf
{
    // Tên kiểm tra token
    private $_csrf_token_name    = 'cms-token-name';
    
    // Thời gian sống của session, 3600 = 1h
    private $_csrf_time_live = 3600;
    
    private $_csrf_value = '';
    // Hàm khởi tạo, có hai tham số
    // - $use_token : nếu true thì có sử dụng validate token
    // - $token_post: nếu true thì nếu method = post thì sẽ validate token trong form
    // - $token_get : nếu true thì nếu method = get thì sẽ validate token trên url
    function __construct($use_token = true, $token_post = false, $token_get = false)
    {
        // Nếu không muốn sử dụng token thì dừng
        if (!$use_token){
            return;
        }
        
        // Tạo CSRF Token
        $this->__create_csrf_token();
        
        // Nếu có validate cho phương thức POST
        if ($token_post && !$this->__validate_post()){
            die ('Token sai');
        }
        
        // Nếu có validate cho phương thức GET
        if ($token_get && !$this->__validate_get()){
            die ('Token sai');
        }
    }
    
    // Mỗi người dùng sẽ có một mã token riêng biệt,
    // nên trong hàm này ta sẽ tạo một quy luật riêng để tạo token nhé
    private function __create_csrf_token()
    {
        // Khởi tạo token name
        $this->_csrf_token_name = 'token'.md5($_SERVER['REMOTE_ADDR'].$_SERVER['HTTP_USER_AGENT'].'@#$%^+&*(-)');
        
        // Nếu token chưa dược khởi tạo thì khởi tạo
        if (!isset($_COOKIE[$this->_csrf_token_name]))
        {
            // Tạo token
            $token = md5($_SERVER['REMOTE_ADDR'].$_SERVER['HTTP_USER_AGENT']);
            
            // Lưu token trong 1h
            setcookie($this->_csrf_token_name, $token, time() + $this->_csrf_time_live);
        }
        else{
            $token = $_COOKIE[$this->_csrf_token_name];
            setcookie($this->_csrf_token_name, $token, time() + $this->_csrf_time_live);
        }
        $this->_csrf_value = $token;
    }
    
    // Kiểm tra phương thức POST
    private function __validate_post()
    {
        // Kiểm tra nếu phương thức hiện tại là POST
        if ($_SERVER['REQUEST_METHOD'] == 'POST')
        {
            // Kiểm tra có tồn tại token không
            if (!isset($_POST[$this->_csrf_token_name]) || !isset($_COOKIE[$this->_csrf_token_name])){
                return false;
            }
            // Nếu tokeon không phù hợp
            else if ($_POST[$this->_csrf_token_name] != $_COOKIE[$this->_csrf_token_name]){
                return false;
            }
        }
        return true;
    }
    
    
    // Kiểm tra phương thức GET
    private function __validate_get()
    {
        // Kiểm tra nếu phương thức hiện tại là POST
        if ($_SERVER['REQUEST_METHOD'] == 'GET')
        {
            // Kiểm tra có tồn tại token không
            if (!isset($_GET[$this->_csrf_token_name]) || !isset($_COOKIE[$this->_csrf_token_name])){
                return false;
            }
            // Nếu tokeon không phù hợp
            else if ($_GET[$this->_csrf_token_name] != $_COOKIE[$this->_csrf_token_name]){
                return false;
            }
        }
        return true;
    }
    
    // Lấy token name
    function get_token_name(){
        return $this->_csrf_token_name;
    }
    
    // Lấy token value
    function get_token_value(){
        return $this->_csrf_value;
    }
    
    // Tạo link có token
    function create_link($url){
        return $url.'?'.$this->_csrf_token_name.'='.$this->_csrf_value;
    }
}

4. Lời kết

Vì kiến thức tôi cũng giới hạn nên tôi chia sẽ không được sâu, nếu có gì sai sót mong bạn góp ý theo hướng tích cực nhé.

freetuts