+5

Bàn về một số lỗi thường gặp về bảo mật trong Ruby on Rails

Giới thiệu

Một điểm mà mình nhận thấy ở hầu hết những tân binh hay thậm chí là một số Rails dev đó là sự lơ là và thiếu nhận thức về vấn đề bảo mật. Họ không được training cũng như không có ý thức về tầm quan trọng của nó. Trong thực tế điều này có thể dẫn đến những thiệt hại rất lớn cho khách hàng, ảnh hưởng đến sự sống còn của dự án, thậm chí cả công ty.

Với một chút kinh nghiệm của bản thân, cùng với việc tìm hiểu và học hỏi những bậc đàn anh, hôm nay mình sẽ giới thiệu một vài kiểu sai lầm về bảo mật tiêu biểu trong ứng dụng Ruby on Rails mà mọi người thường mắc phải.

Sử dụng String interpolation trong câu truy vấn

String interpolation - tạm dịch là nội suy chuỗi (trans: chuối lắm nên mình ko dịch nữa nhé 😄).

Trong lập trình máy tính, string interpolation (hay variable interpolation, variable substitution, hay variable expansion) là quá trình đánh giá một string literal trong đó có chứa một hay nhiều placeholders, sau đó trả về kết quả tại đó các placeholders được thay thế bởi các giá trị tương ứng của nó.

String interpolation khiến cho một chuỗi có thể dễ dàng thêm các giá trị khác vào bên trong nó. Đây là một ví dụ:

ruby_variable = "cool"
string = "Ruby is #{ruby_variable}" #=> "Ruby is cool"

Trông có vẻ không thực sự có ý nghĩa trong trường hợp này nhỉ 😄 nhưng trong trường hợp biến truyền vào hoặc string trở nên phức tạp hơn bạn sẽ nhận ra được sức mạnh của nó đấy. Bởi vậy, nhiều Rails dev sẽ sử dụng cách này để tạo các câu truy vấn:

User.where("name = '#{params[:name]}' AND password = '#{params[:password]}'")

Có vẻ hợp lý!? Nhưng, đây lại là kiểu sai lầm có thể thổi bay công ty của bạn. Đó chính là lỗ hổng cho kiểu tấn công SQL Injection kinh điển.

Với cách viết này, bạn đã cho phép kẻ tấn công có khả năng chèn vào (inject) bất cứ câu SQL nào hắn muốn để nhằm thay đổi tác dụng thực sự của câu truy vấn:

params[:name] = "') OR 1--"
User.where("name = '#{params[:name]}' AND password = '#{params[:password]}'")
Query
SELECT "users".* FROM "users" WHERE (name = '') OR 1--' AND password = '')
Result
#<ActiveRecord::Relation [#<User id: 85, name: "Bob", password: 
"Bobpass", age: 76, admin: false, created_at: "2016-11-11 18:51:41", 
updated_at: "2016-11-11 18:51:41">, #<User id: 86, name: "Jim", 
password: "Jimpass", age: 62, admin: false, created_at: "2016-11-11 
18:51:41", updated_at: "2016-11-11 18:51:41">, #<User id: 87, name: 
"Sarah", password: "Sarahpass", age: 41, admin: false, created_at: 
"2016-11-11 18:51:41", updated_at: "2016-11-11 18:51:41">, #<User id: 
88, name: "Tina", password: "Tinapass", age: 31, admin: false, 
created_at: "2016-11-11 18:51:41", updated_at: "2016-11-11 18:51:41">, 
#<User id: 89, name: "Tony", password: "Tonypass", age: 21, admin: 
false, created_at: "2016-11-11 18:51:41", updated_at: "2016-11-11 
18:51:41">, #<User id: 90, name: "Admin", password: "supersecretpass", 
age: 20, admin: true, created_at: "2016-11-11 18:51:41", updated_at: 
"2016-11-11 18:51:41">]>

You get it? Vậy nên đừng bao giờ, đừng bao giờ sử dụng string interpolation cho việc truy vấn trong Rails (hay ở bất cứ ngôn ngữ nào khác). Thay vào đó bạn nên viết câu truy vấn trên như thế này:

User.where("name = ? AND password = ?", params[:name], params[:password])

hoặc như này:

User.where(name: params[:name], password: params[:password])

Với các cách viết như trên. Rails sẽ đảm bảo câu SQL được dựng nên là an toàn để truy vấn. Để đọc nhiều hơn về SQL Injection trong Rails, các bạn có thể đọc thêm tại đây.

No scopes

projects = Project.where...

Bạn thử nghĩ xem liệu có khi nào một người muốn thực hiện tìm kiếm trên tập tất cả Projects không? Rất hiếm khi. Thay vì làm như vậy, những projects đó nên được scoped - giới hạn gắn với điều kiện gì đó. Kiểu như này

projects = current_user.projects.where...

Hay cái gì đó tương tự như vậy. Gần như rất ít trường hợp bạn cần phải tìm kiếm thứ gì đó trên toàn bộ dữ liệu của bảng. Nhưng mình lại thấy lỗi này xảy ra một cách thường xuyên, đặc biệt là đối với những bạn mới nhập môn - finder method được gọi từ top-level model, trong khi chúng nên được scoped lại. Bởi trong trường hợp truy vấn xảy ra lỗi, người dùng có thể vô tình truy cập đến những đối tượng thông tin đáng ra không thuộc quyền của họ, hậu quả điển hình là những vụ rò rỉ thông tin người dùng.

Khi review code, đây gần như là điều đầu tiên mình chú ý đến: do tên của model được sử dụng với chữ in hoa. FYI, Brakeman, một công cụ quét lỗi bảo mật cho Rails cũng kiểm tra lỗi này.

Tin tưởng dữ liệu đầu vào

Đây là một đoạn JS phía client mà mình mới đọc được gần đây:

$.ajax({  
  url: "/selected_answer",  
  type: "POST",  
  dataType: "json",  
  data: {user_id: current_user.id, question_id: question_id}
});

user_id được truyền vào là id của người dùng đang đăng nhập.

Nên biết rằng, bất cứ thứ gì từ JS code đều có thể bị giả mạo. Do đó user_id trong trường hợp này là một cái gì đó hoàn toàn không đáng tin cậy. Điều gì có thể ngăn cản một ai đó đặt bất cứ một giá trị id nào mà họ muốn? Và đây là lúc hoàn hảo để sử dụng Session.

Trong Rails, Sessions là thứ không thể làm giả được. Một khi bạn gắn user_id vào Rails session, người dùng không thể thay đổi giá trị user_id đó bằng cách làm trò với cookies của họ.

Tuy nhiên còn một điều nữa ở ví dụ trên: question_id. Bạn cũng không nên tin tưởng giá trị này. Điều này dẫn lại đến với quan điểm thứ #2, scope một câu truy vấn đảm bảo question lấy ra chắc chắn thuộc quyền truy cập của người dùng.

Cho nên thay vì viết thế này:

@question = Question.find(params[:question_id))

Bạn nên làm như này:

@question = current_user.questions.find(params[:question_id))

Bằng cách này, giả sử question_id có bị thay đổi bởi ai đó ở tầng giữa, truy vấn này đơn giản trả về đối tượng question thuộc quyền truy cập của user đang đăng nhập mà thôi.

Escape html sai cách

Người ta thường dùng raw hay html_safe để escape html. Tuy nhiên việc sử dụng sai cách các phương thức này sẽ dẫn đến lỗ hổng cho kiểu tấn công Cross-site scripting (XSS).

Hãy cùng quan sát đoạn code ví dụ sau:

<p><%== user.description %></p>

<p><%= raw user.description %></p>

<p><%= user.description.html_safe %></p>

Mối nguy hiểm tiềm tàng đã được tạo ra bởi các phương thức này không loại bỏ hay mã hóa đoạn script tags. Bởi vậy nếu ai đó cố gắng submit giá trị description như này:

{
  email: "john.doe@gmail.com",
  name: "John Doe",
  description: "<script>alert('Hello world!')</script>"
}

Sẽ không có gì bất ngờ nếu trình duyệt bỗng dưng alert lên một cái popup trong lần truy cập tới của bạn. Và nếu bạn cho rằng alert('Hello world!') là nhàm chán, thì có nhiều điều khác mà một hacker có thể làm, như việc lấy cắp dữ liệu cookie, tải các tệp script độc hại, hay chuyển hướng người dùng tới một trang tấn công giả mạo (phishing website).

{
  description: "<script>alert(document.cookie)</script>"
}

{
  description: "<script src='path/to/evil_script.js'></script>"
}

{
  description: "<script>window.location = 'http://fake_gmail.com'</script>"
}

May mắn cho chúng ta, Rails cung cấp một helper khác giúp xử lý vấn đề trong những trường hợp như thế này, đó là sanitize. Khi sử dụng sanitize với tùy chọn mặc định, nó sẽ trả về kết quả tương tự với html_safe, trừ việc nó sẽ loại bỏ tất cả các thẻ <script>, <link><style>.

<p><%=sanitize user.description %></p>

<!-- "<span>Hello<br/>world</span><script></script>" -->  
<p><span>Hello<br>world</span></p>

Sử dụng phép gán của Javascript bên trong template cũng dễ bị khai thác để tấn công XSS. Ví dụ:

<script>  
  var description = "<%== user.description %>";
  // exploited with '";alert("Hello world!");"'
  // exploited with '</script><script>alert("Hello world!")</script>'
</script> 

Thường thì ta chỉ cần sử dụng to_json để "làm gọn" (sanitize) các giá trị trước khi gán cho một biến JS.

<script>  
  var description = <%== user.description.to_json %>;
  // var description = "\";alert(\"Hello world!\");\"";
  // var description = "\u003c/script\u003e\u003cscript\u003ealert..."
</script>

Tuy nhiên nếu ứng dụng của bạn cần một thiết lập như thế này:

ActiveSupport.escape_html_entities_in_json = false 

Khi đó to_json vẫn có thể bị khai thác như thường:

<script>  
  var description = <%== user.description.to_json %>;
  // exploited "</script><script>alert('Hello world!')</script>"
</script>

<!-- Result -->  
<script>  
  var description = "</script><script>alert('Hello world!')</script>";
</script>  

Trong trường hợp này, lựa chọn tốt nhất của bạn là dùng escape_javascript helper (hoặc alias j)

<script>  
  var description = "<%==j user.description %>";
  // var description = "<\/script><script>alert(\'Problem?\')<\/script>";
</script>  

Sử dụng cookie không an toàn

Hầu hết các ứng dụng web sử dụng cookie lưu trữ session data hay id. Biết được điều này, các hackers luôn tìm cách lấy cắp hoặc sao chép dữ liệu cookie nhằm mục đích chiếm dụng user's session.

Bởi vậy, giữ cho website cookie được bảo mật là vấn đề rất quan trọng, và có hai sự lựa chọn bạn phải luôn cân nhắc sử dụng cho các giá trị cookie nhạy cảm, đó là http_onlysecure.

Cài đặt http_only với true để vô hiệu hóa khả năng JS truy cập vào cookie, do đó có thể ngăn các hackers đánh cắp dữ liệu nhạy cảm trong cookie khi website bị tấn công XSS.

Tùy chọn secure đảm bảo cookie của bạn sẽ không bao giờ được truyền qua cơ chế không bảo mật. Bởi vậy nếu ứng dụng của bạn yêu cầu tất cả các request sử dụng giao thức https, lựa chọn này nên được set giá trị true.

Thiết lập cho ứng dụng Rails trong môi trường production nên được cài đặt như thế này:

# config/environments/production.rb
Rails.application.configure do  
  force_ssl = true
end

# config/initializers/session_store.rb
Rails.application.config.session_store :cookie_store,  
                                       key: '_my_app_session',
                                       secure: Rails.env.production?,
                                       httponly: true,
                                       expire_after: 60.minutes

Hơn thế nữa, nếu ứng dụng của bạn có bất kỳ cookie nào nhạy cảm bên trong dữ liệu session hay id, nó cũng nên được mã hóa trước, sau đó thiết lập bảo mật với các option http_onlysecure

cookie.signed[:remember_me_token] = {  
  value: 'XXX',
  expires: 1.day,
  httponly: true,
  secure: Rails.env.production?
}

Kết luận

Luôn nhớ rằng vấn đề bảo mật luôn là cực kì quan trọng đối với một ứng dụng bất kể quy mô của nó là lớn hay nhỏ. Luôn cảnh giác và đề cao tính an toàn của hệ thống là đức tính tốt cần có của mỗi lập trình viên giỏi.

Tham khảo thêm

Most secure Rails apps

Common security mistakes in Rails


All rights reserved

Viblo
Hãy đăng ký một tài khoản Viblo để nhận được nhiều bài viết thú vị hơn.
Đăng kí