11/08/2018, 20:53

Bảo mật hơn trong tải file

Tải trên tiêu đề bài viết là tải lên lẫn tải xuống . Trong đó sẽ nói ra một số các phòng trách được lỗi đã nói trong bài viết Khai thác lỗ hổng của chức năng download file. Mình sẽ trình bày theo các phần như sau: Phạm vi máy chủ. Phạm vi mã nguồn. Phạm vi máy chủ Có hai vấn đề ...

Tải trên tiêu đề bài viết là tải lên lẫn tải xuống. Trong đó sẽ nói ra một số các phòng trách được lỗi đã nói trong bài viết Khai thác lỗ hổng của chức năng download file. Mình sẽ trình bày theo các phần như sau:

  1. Phạm vi máy chủ.
  2. Phạm vi mã nguồn.

Phạm vi máy chủ

Có hai vấn đề chính trong phần này cần đề cập là phân quyềnphạm vi lưu trữ.

Phân quyền

Phân quyền ở đây là chỉ cấp chính xác những quyền cần thiết cho những tài khoản cần thiết.

  • Không cấp quyền thực thi câu lệnh chạy script cho các file không có đuôi là file code. Ví dụ: sẽ không cho chạy câu lệnh php avatar.png.
  • Không cấp quyền tạo file/thư mục khi website của bạn không có phần upload.
  • Quyền overwrite file mã nguồn phải được cấp đúng.

Phạm vi lưu trữ

Máy chủ/thư mục lưu trữ các file mã nguồn nên đặt cách biệt với máy chủ/thư mục của các file tĩnh (JS, CSS, hình ảnh). Trên Linux thì có cái gọi là alias cho thư mục. Nếu đặt khác máy chủ, thì có thể hiểu rằng máy chủ chứa các file tĩnh là CDN, File server. Còn máy chủ đặt mã nguồn là Web server. Còn nếu đặt khác thư mục, thì phải chú ý phân quyền cho hợp lý các thư mục này.

Phạm vi mã nguồn

Một hàm download mẫu được viết bằng PHP.

<?php
    function download($filePath) {
        header('Content-Type: application/octet-stream');
        header('Content-Transfer-Encoding: Binary'); 
        header('Content-disposition: attachment; filename="' . basename($filePath) . '"'); 
        readfile($filePath);
    }
?>

Đây là kiểu hàm được viết đơn giản nhất cho việc download file.

Chặn theo đuôi file

Điều này là một điều cơ bản khi làm chức năng upload file, download file nhưng không phải ai cũng nhớ để thực hiện. Bạn không thể nào để người dùng upload một file để làm ảnh đại diện là avatar.php hay avatar.js được. Do đó, khi một người dùng upload một file lên thì phải kiểm tra đuôi file có đúng là những file được cho phép trong một trường hợp cụ thể nào đó hay không?

Trong trường hợp download, với hàm download như trên, thì chắc chắn sẽ bị tình trạng download mã nguồn về. Do đó có thể cải tiến tí như sau

<?php
    function download($filePath) {
        $fileInfo = pathinfo($fullPath);
        $ext        = strtolower($fileInfo['extension']);
        switch($ext) {
            case 'gif':
                $ct = 'image/gif';
                break;
            case 'jpeg':
            case 'jpg':
                $ct = 'image/jpeg';
                break;
            case 'png':
                $ct = 'image/png';
                break;
            default:
                exit('Go away, babe!');
        }
        header('Content-Type: ' . $ct);
        header('Content-Transfer-Encoding: Binary'); 
        header('Content-disposition: attachment; filename="' . basename($filePath) . '"'); 
        readfile($filePath);
    }
?>

Chuyện chặn download theo đuôi file là nằm ở dòng exit, còn $ct là chủ yếu có một số ứng dụng client sẽ chặn theo header (Mobile App, browsers,...).

Chặn theo định dạng MIME

Chắc hẳn rằng các bạn sẽ tự hỏi nếu giang hồ nào đó đổi đuôi file avatar.php thành avatar.png thì sao?, câu trả lời là Kiểm tra MIME type. Bạn phải kiểm tra xem định dạng file upload lên có phải là file hình hay không? Hay là định dạng file khác. Và tất nhiên, cách này phải đi kèm với chuyện phân quyền thực thi file đã nói phía trên.

Chặn theo đường dẫn download

Ví dụ, bạn chứa hình ảnh trong thư mục images, còn file xử lý download thì lại trong thư mục controllers chứa luôn cả images, thì hàm download trên sẽ được sửa như sau

<?php
    function download($fileName) {
        $filePath = 'images/' . $fileName;
        $fileInfo = pathinfo($fullPath);
        $ext        = strtolower($fileInfo['extension']);
        switch($ext) {
            case 'gif':
                $ct = 'image/gif';
                break;
            case 'jpeg':
            case 'jpg':
                $ct = 'image/jpeg';
                break;
            case 'png':
                $ct = 'image/png';
                break;
            default:
                exit('Go away, babe!');
        }
        header('Content-Type: ' . $ct);
        header('Content-Transfer-Encoding: Binary'); 
        header('Content-disposition: attachment; filename="' . basename($filePath) . '"'); 
        readfile($filePath);
    }
?>

Như vậy, khi gửi yêu cầu download, người dùng chỉ truyền tên file vào, xem như là tạm giấu được đường dẫn. Tạm ở đây ví người dùng có thể dùng đường dẫn tương đối như đã nêu ở bài Khai thác lỗ hổng của chức năng download file.
Vậy fix như trên thì người dùng không download được file khác trong controller?
Không đâu. Nếu mình nhập đường dẫn như vậy thì sao http://example.com/download.php?file=../download.php?

Kết

Đêm khuya hết ý, nên kết vậy thôi =))

0