CSRF Protection in Rails

CSRF Protection in Rails

Đầu tiên, tấn công CSRF là gì? Cross-Site Request Forgery (CSRF) là kiểu tấn công cho phép một người dùng xấu giả mạo các yêu cầu hợp pháp và gửi chúng đến hệ thống của bạn. Chúng cố gắng gửi đến hệ thống như một người dùng đã được chứng thực trong hệ thống. Rails xây dựng hệ thống bảo vệ chống lại kiểu tấn công này bằng cách tạo ra các thẻ duy nhất và xác nhận tính xác thực của chúng với mỗi lần gửi.
Nếu hiện tại ban đang sử dụng Rails, rất có thể bạn đang sử dụng rails trong chính project của mình mà đôi khi không để ý đến. Tính năng này gần như có ngay từ đầu và nhờ tính năng này của rails mà việc sử dụng rails để làm các project trở nên dễ dàng hơn rất nhiều.
Gần đây mình có tìm hiểu trên mạng và quyết định đưa ra một số điều mình tìm hiểu được để mọi người cùng thảo luận để hiểu rõ hơn nữa về CSRF protection trong rails. Dưới đây là các tìm hiểu liên quan đến cách tạo ra các token cho mỗi response và làm cách nào để chúng dùng để xác thực được tính đúng đắn của mỗi request được gửi đến.

Về cơ bản

CSRF sẽ có 2 thành phần chính về cơ bản. Thứ nhất, đó là một token duy nhất được nhúng vào trang HTML trong project của chúng ta. Với cùng token đó, nó sẽ được lưu trữ trong session cookie. Khi một người dùng tạo POST request lên hệ thống thì token ở trong html sẽ được gửi lên cùng với request đó. Rails sẽ nhận token được gửi lên cùng với request và token được lưu trong session cookie và so sánh để chắc chắn rằng chúng khớp với nhau.

Chúng ta sử dụng nó như thế nào?

Là một rails developer, về cơ bản bạn có thể dùng CSRF protection một cách miễn phí với một dòng lệnh vô cùng đơn giản trong application_controller.rb

protect_from_forgery with: :exception 

Tiếp đến là thêm 1 dòng nữa trong application.html.erb

<%= csrf_meta_tags %>


Trên đây là 2 dòng rất cơ bản để sử dụng được CSRF protection của rails nhưng thực sự thì nó hoạt động thế nào?

Khởi tạo và mã hóa

Chúng ta hay cùng bắt đầu với csrf_meta_tag. Nó là một hàm đơn giản trong view helper thực hiện công việc nhúng authencity token vào HTML.


# actionview/lib/action_view/helpers/csrf_helper.rb

def csrf_meta_tags
  if protect_against_forgery?
    [
      tag("meta", name: "csrf-param", content: request_forgery_protection_token),
      tag("meta", name: "csrf-token", content: form_authenticity_token)
    ].join("\n").html_safe
  end
end


Ở đây, cái chúng ta cần tập trung chính là csrf-token - nơi mà mọi điều kỳ diệu của CSRF protection diễn ra. Thẻ helper form_authenticity_token là nơi lấy token thực tế. Ở thời điểm này, mình đã thử nhập module RequestForgeryProtection của ActionController. Giờ là lúc bắt đầu tìm hiểu về nó ^.^
Module RequestForgeryProtection xử lý tất cả mọi thứ để làm với CSRF. Nổi bật nhất là method protect_from_forgery mà bạn có thể thấy trong ApplicationController, nơi mà xây dựng một số liên kết móc nối để đảm bảo rằng mỗi request đều được bảo vệ trước tấn công CSRF và trả lại như thế nào nếu request không được xác thực. Ngoài ra, nó cũng quan tâm đến việc sinh mã, mã hóa và giải mã CSRF token. Điểm hay của module này là nó chính là một scope nhỏ của rails. Ngoài sự trợ giúp của một số view helper thì bạn có thể thấy hầu như tất cả các tiến trình thực hiện việc bảo vệ trước tấn công CSRF ở ngay trong file này.
Tiếp tục tìm hiểu sâu hơn một chút về cách hoạt động của CSRF token trong HTML. form_authenticity_token dưới đây là một method bọc đơn giản để truyền bất kỳ tham số tùy chọn nào nhằm mục đích set giá trị token cho chính session, sử dụng masked_authenticity_token (Tạo ra một phiên bản masked của authenticity token thay đổi theo từng yêu cầu. Mask được sử dụng để giảm thiểu các cuộc tấn công SSL như BREACH.):

# actionpack/lib/action_controller/metal/request_forgery_protection.rb

# Sets the token value for the current session.
def form_authenticity_token(form_options: {})
  masked_authenticity_token(session, form_options: form_options)
end

# Creates a masked version of the authenticity token that varies
# on each request. The masking is used to mitigate SSL attacks
# like BREACH.
def masked_authenticity_token(session, form_options: {}) # :doc:
  # ...
  raw_token = if per_form_csrf_tokens && action && method
    # ...
  else
    real_csrf_token(session)
  end

  one_time_pad = SecureRandom.random_bytes(AUTHENTICITY_TOKEN_LENGTH)
  encrypted_csrf_token = xor_byte_strings(one_time_pad, raw_token)
  masked_token = one_time_pad + encrypted_csrf_token
  Base64.strict_encode64(masked_token)```
end


Kể từ khi bắt đầu giới thiệu các biểu mẫu CSRF tokens trong rails 5, method masked_authenticity_token đã trở nên phức tạp hơn khá nhiều. Trong đoạn tìm hiểu này, chúng ta sẽ tập trung vào việc thực hiện ban đầu của nó, một CSRF token đơn trên mỗi request. Trong trường hợp đó, chúng ta chỉ tập trung vào trường hợp else trong câu điều kiện bên trên - kết thúc việc thiết lập raw_token thành giá trị trả về của #real_csrf_token.
Tại sao chúng ta phải truyền session vào real_csrf_token? Đó là bởi method này thực ra làm 2 công việc: Tạo ra mẫu nguyên bản - token chưa được mã hóa và đưa token đó vào session cokkie:

# actionpack/lib/action_controller/metal/request_forgery_protection.rb

def real_csrf_token(session) # :doc:
  session[:_csrf_token] ||= SecureRandom.base64(AUTHENTICITY_TOKEN_LENGTH)
  Base64.strict_decode64(session[:_csrf_token])
end


Nhớ rằng method này cuối cùng sẽ được gọi bởi chúng ta đã khai báo csrf_meta_tags trong application layout. Việc làm trên sẽ luôn đảm bảo việc token trong session cookies khi được sinh sẽ luôn phù hợp với token ở page.
Rồi giờ chúng ta cùng nhìn lại phần dưới của masked_authenticity_token:

  one_time_pad = SecureRandom.random_bytes(AUTHENTICITY_TOKEN_LENGTH)
  encrypted_csrf_token = xor_byte_strings(one_time_pad, raw_token)
  masked_token = one_time_pad + encrypted_csrf_token
  Base64.strict_encode64(masked_token)

Sau khi đã chèn token vào session cookie thì giờ đây method này sẽ liên quan đến việc trả lại mã token này vào trong trang HTML của chúng ta và ở đây mình có tìm hiểu một số cách phòng ngừa trong đây (chủ yếu là để giảm thiểu khả năng tấn công SSL BREAK, nhưng mình chưa tìm hiểu kỹ nên sẽ không đi sâu vào). Lưu ý rằng ở đây chúng ta thấy không mã hóa token để đưa vào session cookie bởi từ rails 4 session cookie đã được mã hóa rồi.
Vào chi tiết một chút, trước tiên, ta sẽ có khởi tạo một one-time pad được sử dụng để mã hóa raw token (tạm hiểu là token ở dạng sơ khai đầu tiên). One-time pad là một kỹ thuật mật mã sử dụng khóa được tạo ngẫu nhiên để mã hóa một thông báo cơ bản với cùng độ dài và yêu cầu khóa (sử dụng để giải mã tin nhắn). Nó được gọi là pad "một lần" vì lý do: mỗi khóa được sử dụng cho một tin nhắn, và sau đó bị loại bỏ. Rails thực hiện điều này bằng cách tạo ra một one-time pad mới cho mỗi mã CSRF mới, sau đó sử dụng nó để mã hóa các mã thông báo bằng cách sử dụng XOR bitwise hoạt động. Chuỗi pad một lần được thêm vào chuỗi mã hoá, sau đó mã hóa bằng Base64 để tạo chuỗi sẵn sàng cho HTML. Khi thao tác này hoàn tất, chúng tôi sẽ gửi masked authenticity token trở lại ngăn xếp, nơi nó kết thúc trong cách hiển thị application layout:

<meta name="csrf-param" content="authenticity_token" />
<meta name="csrf-token" content="vtaJFQ38doX0b7wQpp0G3H7aUk9HZQni3jHET4yS8nSJRt85Tr6oH7nroQc01dM+C/dlDwt5xPff5LwyZcggeg==" />

Giải mã và xác minh

Cho đến nay, chúng tôi đã giới thiệu bằng cách nào CSRF token được tạo ra và nó kết thúc như thế nào trong HTML và cookie của bạn. Tiếp theo, chúng ta hãy nhìn vào cách Rails xác nhận một request được gửi đến.
Khi bạn submit form từ trên trang web. CSRF token sẽ được gửi cùng với phần còn lại của form data khi gửi lên (Trong một params mặc định đó là authenticity_token). Nó cũng có thể được gửi thông qua X-CSRF-Token HTTP header. Nhớ lại dòng này trong ApplicationController của chúng ta:

protect_from_forgery with: :exception 

Trong số những thứ khác, method protect_from_forgery này thêm một hành động vào trước vòng đời của mọi hành động điều khiển:

before_action :verify_authenticity_token, options

Thao tác before_action này bắt đầu quá trình so sánh các CSRF token trong params yêu cầu hoặc header với các token trong session cookie.

# actionpack/lib/action_controller/metal/request_forgery_protection.rb

def verify_authenticity_token # :doc:
  # ...
  if !verified_request?
    # handle errors ...
  end
end

# ...

def verified_request? # :doc:
  !protect_against_forgery? || request.get? || request.head? ||
    (valid_request_origin? && any_authenticity_token_valid?)
end

Sau khi thực hiện một số nhiệm vụ quản trị (ví dụ: chúng tôi không cần phải xác minh các HEAD hoặc GET request), quá trình xác minh bắt đầu một cách nghiêm túc với cuộc gọi tới any_authenticity_token_valid?:

def any_authenticity_token_valid? # :doc:
  request_authenticity_tokens.any? do |token|
    valid_authenticity_token?(session, token)
  end
end


Kể từ khi một request có thể gửi token trong form param hoặc header của request, Rails chỉ đòi hỏi rằng ít nhất một trong số những mã này phù hợp với token trong session cookie.
valid_authenticity_token? Là một method khá dài nhưng cuối cùng nó chỉ làm ngược lại masked_authenticity_token để giải mã và so sánh mã thông báo:

def valid_authenticity_token?(session, encoded_masked_token) # :doc:
  # ...
  
  begin
    masked_token = Base64.strict_decode64(encoded_masked_token)
  rescue ArgumentError # encoded_masked_token is invalid Base64
    return false
  end

  if masked_token.length == AUTHENTICITY_TOKEN_LENGTH
    # ...

  elsif masked_token.length == AUTHENTICITY_TOKEN_LENGTH * 2
    csrf_token = unmask_token(masked_token)

    compare_with_real_token(csrf_token, session) ||
      valid_per_form_csrf_token?(csrf_token, session)
  else
    false # Token is malformed.
  end
end


Trước tiên, chúng ta cần lấy chuỗi mã hoá Base64 và giải mã nó để kết thúc với "masked token". Từ đây, ta có thể phát hiện ra token và sau đó so sánh nó với token trong phiên làm việc (session):

def unmask_token(masked_token) # :doc:
  one_time_pad = masked_token[0...AUTHENTICITY_TOKEN_LENGTH]
  encrypted_csrf_token = masked_token[AUTHENTICITY_TOKEN_LENGTH..-1]
  xor_byte_strings(one_time_pad, encrypted_csrf_token)
end

Trước khi unmask_token có thể thực hiện các phương pháp về mật mã cần thiết để giải mã token, chúng ta phải tách token được che dấu vào các phần cần thiết của nó: one-time pad và token được mã hóa. Sau đó, nó XOR hai strings để cuối cùng tạo ra raw token ban đầu.
Cuối cùng, compare_with_real_token dựa vào ActiveSupport :: SecureUtils để đảm bảo các token là khớp với nhau.

def compare_with_real_token(token, session) # :doc:
  ActiveSupport::SecurityUtils.secure_compare(token, real_csrf_token(session))
end

Và, cuối cùng, nếu yêu cầu được ủy quyền - nó sẽ vượt qua!

Phần kết luận

Bản thân mình trước đây không bao giờ để ý đến cái gọi là CSRF protection, vì như nhiều thứ khác trong Rails, nó "chỉ làm việc" một cách gần như mặc định. Và sau khi tìm hiểu thì giống như các thứ khác, bạn thấy khá hay khi hiểu những gì mà nó hoạt động.
Mỗi lần tự đi sâu xem hoạt động của 1 thứ gì đó, nó giúp ta hiểu thêm nhiều điều còn lặn sâu bên dưới của Rails mà mọi lần gần như ta chỉ "coppy and paste". Bài viết trên đây chỉ là một chút tìm hiểu và dịch lại các nguồn mà mình đã xem. Mong các bạn có thể góp ý thêm để bài viết trở nên có ích hơn.
Xin cảm ơn các bạn đã đọc bài viết này.