Vấn đề bảo mật trong Rails - phần 2
Bài đăng này đã không được cập nhật trong 3 năm
Ở bài viết trước, chúng ta đã đi qua cách thức đầu tiên và khá phổ biến mà các hacker hay nhắm vào thông qua session. Bài viết này sẽ đề cập đến cách thức tiếp theo
Cross-Site Request Forgery (CSRF)
Phương pháp tấn công thực hiện include các đoạn mã độc hay chèn một link vào một page, cái mà sẽ chuyển hướng đến một ứng dụng web mà người dùng user đã authenticate trước đó. Nếu session cho ứng dụng web đó chưa hết hạn, kẻ tấn công có thể thực thi các hành động trái phép.
Ở bài trước, khi nói về session, ta đã biết rằng, các ứng dụng Rails sử dụng session dựa trên cookie. Hoặc là session ID được lưu trong cookie và có một session hash phía server, hoặc là toàn bộ session hash lưu phía client. Trong cả 2 trường hợp, trình duyệt đều tự động gửi cookie với mỗi request đến một tên miền, nếu nó có thể tìm thấy được một cookie cho tên miền đó. Điểm gây tranh cãi ở đây là nếu một request đến từ một site của một tên miền khác, nó cũng sẽ gửi cookie. Hãy xem ví dụ sau, cái mà được mô tả trong hình ảnh phía trên:
- Bob đang truy cập, đã authenticate và làm việc trên một ứng dụng nào đó, chẳng hạn như: http://www.webapp.com. Để giải trí, anh ta mở tab khác để lướt web. Tại tab này Bob duyệt các tin tức và xem một bài viết từ một hacker, trên đó chứa một hình ảnh là một phần tử image HTML. Phần tử ảnh này sẽ tham chiếu đến ứng dụng webapp bên trên. Chẳng hạn:
<img src="http://www.webapp.com/project/1/destroy">
- Session của Bob tại trang www.webapp.com vẫn tồn tại vì anh ta chưa log out trong vài phút trước.
- Bằng việc xem bài post, trình duyệt tìm thấy một hình ảnh. Nó cố gắng để load hình ảnh này từ www.webapp.com. Như đã nói bên trên, nó cũng sẽ request đến với một cookie hợp lệ.
- Ứng dụng web www.webapp.com tiến hành verify thông tin người dùng từ session tương ứng sau đó destroy project với ID là 1.
- Bob không hề biết đến cuộc tấn công này và vài ngày sau anh ta thấy project với ID là 1 của mình biến mất.
Điều quan trọng cần lưu ý là các file hình ảnh hay link liên kết không nhất thiết phải nằm trong tên miền của ứng dụng web, nó có thể ở mọi nơi, trong các forum, blog post hay email.
CSRF xuất hiện rất ít trong CVE (Common Vulnerabilities and Exposures) - ít hơn 0,1% trong năm 2006 - nhưng nó thực sự là một "sleeping giant"(gã khổng lồ đang ngủ) theo Grossman. Điều này hoàn toàn tương phản với kết quả của nhiều hợp đồng bảo mật - CSRF là một vấn đề bảo mật quan trọng.
CSRF Countermeasures
First, as is required by the W3C, use GET and POST appropriately.
Secondly, a security token in non-GET requests will protect your application from CSRF.
Giao thức HTTP cơ bản cung cấp 2 kiểu request chính - GET và POST(còn nhiều kiểu khác nhưng chúng không được support bởi hầu hết trình duyệt). Tổ chức World Wide Web Consortium (W3C) cung cấp một danh sách cho chọn HTTP GET hay POST:
Use GET if:
- Sự tương tác giống như một câu hỏi( nghĩa là nó thực thi an toàn như truy vấn, đọc hay tìm kiếm)
Use POST if:
- Tương tác giống như một order
- Tương tác thay đổi trang thái của một tài nguyên
- Người dùng chịu trách nhiệm về kết quả của tương tác.
Nếu ứng dụng web của bạn là RESTful, bạn có thể sử dụng các method HTTP bổ sung như PATCH, PUT hay DELETE. Tuy nhiên, không phải tất cả các trình duyệt web hiện này đều hỗ trợ chúng - chỉ có GET và POST. Rails sử dụng một trường hidden_method để handle rào cản này.
Request POST cũng có thể được gửi đi tự động. Ví dụ, đường dẫn www.harmless.com được hiển thị như là đích đến trong thanh trạng thái của trình duyệt. Nhưng nó thực sự đã tạo ra một form mới và gửi một request POST:
<a href="http://www.harmless.com/" onclick="
var f = document.createElement('form');
f.style.display = 'none';
this.parentNode.appendChild(f);
f.method = 'POST';
f.action = 'http://www.example.com/account/destroy';
f.submit();
return false;">To the harmless survey
</a>
Hoặc kẻ tấn công đặt một đoạn code của event onmouseover của một image:
<img src="http://www.harmless.com/img" width="400" height="400" onmouseover="..." />
Và còn nhiều khả năng khác như sử dụng một thẻ <script> để tạo một cross-site request đến một URL với một JSONP(json with padding) hay Javascript response, với response mà hacker có thể trích xuất những dữ liệu nhạy cảm. Để chống lại rò rỉ dữ liệu, chúng ta nên ngăn chặn các tag cross-site <script>. Tuy nhiên, với các request Ajax, tuân theo nguyên tắc same-origin policy(chỉ cho phép các request trên cùng tên miền), vì vậy chúng ta vẫn phải cho phép chúng thực thi bình thường.
Lưu ý: chúng ta không thể phân biệt được nguồn gốc của các thẻ <script>, liệu các thẻ này từ chính site này hay từ site bất kì nào khác. Vì vậy, chúng ta nên chặn tất cả các thẻ <script>, ngay cả khi nó được gọi từ chính tên miền hiện tạitại.
Để bảo vệ trước tất cả các request giả mạo, chúng tôi giới thiệu một required security token, cái mà chỉ site hiện tại biết được. Chúng ta include security token này vào mỗi request và verify nó trên server. Đây là 1 dòng trong file application controller và nó được tạo mặc định khi bạn tạo một ứng dụng Rails bất kì:
protect_from_forgery with: :exception
Nó sẽ tự động include một security token vào tất cả forms và request Ajax được sinh bởi Rails. Nếu nhận được một request với một security token không khớp, sẽ ném ra một exception.
By default, Rails includes an unobtrusive scripting adapter,
which adds a header called X-CSRF-Token with the security token on every non-GET Ajax call.
Without this header, non-GET Ajax requests won't be accepted by Rails.
When using another library to make Ajax calls,
it is necessary to add the security token as a default header for Ajax calls in your library.
To get the token, have a look at <meta name='csrf-token' content='THE-TOKEN'>
tag printed by <%= csrf_meta_tags %> in your application view.
Theo mặc định, Rails sẽ include một unobtrusive scripting adapter, add một header có tên X-CSRF-Token với giá trị là security token trên mỗi lời gọi Ajax không phải kiểu GET. Nếu lời gọi Ajax, không phải GET, không có header nàynày, nó sẽ bị từ chối bởi Rails. Khi sử dụng một thư viện khác để tạo lời gọi Ajax, cũng phải thêm security token vào header cho lời gọi Ajax trong thư viện này. Để get token này, xem thẻ <meta name='csrf-token' content='THE-TOKEN'> được in bởi <%= csrf_meta_tags %> trong mục view của app.
Thông thường, chúng ta dùng cookie để lưu trữ thông tin người dùng đang đăng nhập. Trong trường hợp này nếu request từ 1 site khác, mà cookie vẫn còn hiệu lực thì CSRF protection không còn hiệu quả. Bạn có thể xử lý trong trường hợp này:
rescue_from ActionController::InvalidAuthenticityToken do |exception|
sign_out_user # Example method that will destroy the user cookies
end
Method trên có thể được đặt trong AppplicationController và sẽ được gọi khi token CSRF không tồn tại hoặc không khớp trên một request khác GET.
Redirection and Files
Một lỗ hổng bảo mật khác xin được giới thiệu là việc sử dụng redirection và files trong các ứng dụng web
Redirection
Redirection in a web application is an underestimated cracker tool: Not only can the attacker forward the user to a trap web site, they may also create a self-contained attack.
Bất cứ khi nào người dùng pass URL cho redirection, đều có thể gây nguy hiểm. Các tấn công rõ ràng nhất là redirect người dùng đến một ứng dụng web giả mạo, trông giống hệt như bản gốc. Tấn công người dùng bằng cách gửi một liên kết giả mạo đến email người dùng, chèn liên kế bởi XSS trong ứng dụng web, hay đặt một liên kết trong một trang bên ngoài. Người dùng không ngờ tới vì link được bắt đầu bằng URL chính xác của ứng dụng web, còn URL đến site giả mạo được ẩn trong parameter redirection: http://www.example.com/site/redirect?to=www.attacker.com Ta có thể handle nó bằng cách:
def legacy
redirect_to(params.update(action:'main'))
end
Điều này sẽ redirect người dùng đến main action nếu họ cố gắng để access vào một legacy action. Mục đích nhằm bảo vệ các tham số URL cho các legacy action và pass chúng đến main action. Tuy nhiên, nó có thể bị khai thác bởi hacker nếu chúng chứa một khóa host trong URL:
http://www.example.com/site/legacy?param1=xy¶m2=23&host=www.attacker.com
Nếu nó nằm ở cuối URL, nó sẽ khó bị chú ý và redirect người dùng đến host attacker.com. Một biện pháp đối phó đơn giản là chỉ include các params được cho phép trong một legacy action. Một khi người dùng redirect đến một URL, hãy check nó trong một white list hoặc một format regular expression.
Self-contained XSS
Một kiểu tấn công redirection khác là self-contained XSS, được thực hiện trên các trình duyệt firefox và opera bằng cách sử dụng giao thức data. Giao thức này hiển thị trực tiếp nội dung cuả nó trong trình duyệt, đó có thể là bất cứ thứ gì từ HTML hay Javascript đến images:
data:text/html;base64,PHNjcmlwdD5hbGVydCgnWFNTJyk8L3NjcmlwdD4K
Ví dụ trên là một mã JS được mã hóa bằng Base64, nhằm hiển thị một thông báo message box đơn giản. Kẻ tấn công có thể redirect đến URL này kèm với mã độc hại trong đó. Để đối phó, ngăn cản người dùng cung cấp URL redirect:
http://bobscrazydiscountspoons.com/foo/bar.php?xss=data:image/png;base64,ABCdefghIjKlmNoPqrSTuvwxyZ01d
File Uploads
Make sure file uploads don't overwrite important files, and process media files asynchronously.
Rất nhiều các ứng dụng web cho phép người dùng upload files. Tên tệp, cái mà người dùng có thể chọn(một phần hay toàn bộ) phải luôn được lọc nhằm tránh kẻ tấn công có thể sử dụng một tên tệp độc hại nhằm ghi đè lên bất kì tệp nào của server. Chẳng hạn, nếu bạn đang lưu trữ các tệp tải lên tại /var/www/uploads và người dùng nhập một tên tệp là "../../../etc/passwd", nó có thể ghi đè lên một file quan trọng. Tất nhiên, trình thông dịch của Ruby sẽ yêu cầu quyền thích hợp để làm điều này,
Khi lọc tên file input từ người dùng, đừng cố gắng để remove đi các phần độc hại trong tên tệp. Giả sử ứng dụng web set lọai bỏ tất cả "../" trong tên tệp và kẻ tấn công có thể sử dụng một string như: "....//", và kết quả vẫn sẽ là "../". Cách tốt nhất là sử dụng một whitelist, cái sẽ kiểm tra validate của tên file và set cho một format được chấp nhận. Nếu tên vào là unvalid, reject nó nhưng không remove chúng:
def sanitize_filename(filename)
filename.strip.tap do |name|
# NOTE: File.basename doesn't work right with Windows paths on Unix
# get only the filename, not the whole path
name.sub! /\A.*(\\|\/)/, ''
# Finally, replace all non alphanumeric, underscore
# or periods with underscore
name.gsub! /[^\w\.\-]/, '_'
end
end
Việc xử lý đồng bộ việc upload files có thể gặp trở ngại nếu kẻ tấn công đồng thời tải file từ nhiều máy tính khiến server bị overload và crash. Giải pháp cho vấn đề này là xử lý file media một cách không đồng bộ. Lưu file và sắp xếp request xử lý trong database. Một process thứ 2 sẽ handle và xử lý tệp tin trong background.
Executable Code in File Uploads
Source code in uploaded files may be executed when placed in specific directories. Do not place file uploads in Rails' /public directory if it is Apache's home directory.
Hầu hết các máy chú web có một option gọi là DocumentRoor. Đây là thư mục chính của trang website, Mọi thứ trong đường dẫn này sẽ được serverd bởi mấy chủ web. Nếu các file với tên có phần mở rộng nào đó, code trong file đó sẽ thực thị khi request. Ví dụ cho các file PHP và CGI. Gỉa sử kẻ tấn công upload file có tên "file.cgi" với code trong nó, code sẽ được thực thi khi có ai đó download file này
Nếu máy chủ DocumentRoot của bạn trỏ đến thư mục /public, đừng đặt thư mục upload file vào nó, hãy lưu files trong thư mục ít nhất 1 cấp.
File Downloads
Make sure users cannot download arbitrary files.
Cũng như filter các tệp tin được tải lên, chúng ta cũng phải làm tương tự với các file được download xuống. Phương thức send_file() sẽ gửi các file từ máy chủ đến client. Nếu bạn sử dụng một tên file, mà người dùng nhập, không filter, thì người dùng có thể tải xuống bất kì file nào:
send_file('/var/www/uploads/' + params[:filename])
Một cách đơn giản, chỉ cần truyền vào tên file dạng như: "../../../etc/passwd" để download thông tin login của server. Một giải pháp đơn giản để ngăn chặn điều này là kiểm tra xem file yêu cầu download có nằm trong thư mục dự kiến:
basename = File.expand_path(File.join(File.dirname(__FILE__), '../../files'))
filename = File.expand_path(File.join(basename, @file.public_filename))
raise if basename !=
File.expand_path(File.join(File.dirname(filename), '../../../'))
send_file filename, disposition: 'inline'
Một giải pháp khác là lưu trữ tên file trong database. Điều này nhằm tránh code trong file được tải lên có thể được thực thi. Plugin attachment_fu thực hiện điều này theo cách tương tự.
All rights reserved