almost 4 years ago

這是我六月初在新加坡 Reddot Ruby Conf 給的演講。原稿投影片放在 Github 上。這篇文章是我自己整理的 Note。

其他的演講多半是重談 OWASP 那些標準需要注意的地方。這個演講當時在設計時,刻意跟其他演講不太同調。原始設計的出發點著重於:如果是一個熟 Rails 架構的 Hacker

(1) 會如何進攻你的 Application
(2) 初學者會在架構設計上犯哪些錯?
(3) 如果你是 Application 設計者,如何一開始就設計出相對安全的架構


在談 Security 之前,我們要來談談 Rails 是不是個「安全」的框架?

平心而論,Rails 的預設是比許多框架要「安全」許多的,它的設計預設就防了很多攻擊手段、屏蔽了開發者設計上的盲點,或者是預設就內置許多最佳實務。

  • Helper / View 預設提供了 HTML Escape : 防止 XSS
  • ORM 預設提供了 SQL Escape : 防止 SQL Injection
  • 表單提供 Authenticity Token :防止 CSRF
  • 在 Production mode 錯誤時直接扔 500 頁面:防止 PHP 忘記關 debug mode 結果吐程式碼這種慘劇...
  • 熱門的使用者驗證 Gem 如 Devise 直接內置密碼 Bcrypt 加密 :有些開發者懶得自己寫加密 function 乾脆明碼 password 直上
  • Sensitive data filtered from log:Rails 的 Log 自動會濾掉一些關鍵字如 [password],以免幹到 Log 就超精彩..
  • 熱門的上傳 Gem 如 Paperclip 內建 Filename Sanitization 的實作:防止 path attack

很多傳統打 PHP 網站的方式,是很難拿來直接打 Rails 的,因為一些常見的路都預設被堵死了....

但。這就表示 Rails 不好打嗎?

這也未必。Rails 是個「Framework」。所以其實也有「Pattern」可以依循。一般要打下一個站,去找 Framework 0day 效率通常太低了。找常見的「人為錯誤」其實是比較快的方式...

1. Massive Assignment

Rails 在表單設計上提供了強大的 Helper,表單的 attribute 直接對應到資料庫的欄位。

<%= f.text_field :title %>
<%= f.text_field :body %>

會生成

<input id="topic_title" name="topic[title]" size="30" type="text">
<input id="topic_body" name="topic[body]" size="30" type="text">

看出規律了嗎?所以如果是這樣,從 Chrome DOM Inspect 塞...

<input id="topic_title" name="topic[title]" size="30" type="text">
<input id="topic_body" name="topic[body]" size="30" type="text">
<input id="topic_user_id" name="topic[user_id]" size="30" type="text">

通常就可以達到目的...

因為大部分的 Rails Developer 都會寫出這樣的程式碼:

app/controllers/topics_controller.rb
class TopicsController < ApplicationController
 
   def edit
     @topic = Topic.find(params[:id])
     if @topic.update_attributes(params[:topic])
       redirect_to topic_path(@topic)
     else
       render :edit
     end
   end
end

注意 @topic.update_attributes(params[:topic]) 那一段。

也有風險的地方:role_ids

除了單一 attribute 會被攻擊外,另外還有一個虛擬的 attribute 設計會有風險。

在這個 user model 裡,使用者擁有很多角色。

app/models/user.rb
class User < ActiveRecord::Base
  has_many :roles
end

Rails 提供一個十分方便的設計。user 可以會自動擁有一個虛擬的 attribute role_id 可以用。role_id 是 Getter 也是 Setter。

如果你在系統的 rails console 下這樣的指令

  user.role_id = [1,2,3]

Rails 會自動幫這個 user 設上 Role id = 1, 2, 3 的三個角色。之所以會有這個「貼心」設計,是因為「大家」都用 checkbox 作多選角色,所以內建了這個設計方便一次設定上多個角色。範例 code 如下:

<% for role in Role.all %>
    <%= check_box_tag "user[role_ids][]", role.id, @user.roles.include?(role) %>
    <%=h role.name.camelize %>
<% end %>
<%= hidden_field_tag "user[role_ids][]", "" %>

但這也就表示,其實你的 application 有可能被這樣的假 DOM 玩到的風險....

<input id="user_title" name="user[title]" size="30" type="text">
<input id="user_body" name="user[body]" size="30" type="text">
<input id="user_role_ids" name="user[role_ids]" size="30" type="text">

特別是大家都曉得 role_id = 1 通常就是 admin

需要被檢查的地方:

  • has_many, has_many :through involve OWNERSHIP, Permission
  • user_roles, group_users, ….
  • UPDATE action

幾個可能有效的解法:

Rails 3 可以用的解法

( 這個方法在 Rails4 裡被移除了)

  • 使用 whitelist attribute
  • 打開 config.active_record.whitelist_attributes = true
  • 在 model 裡設定 attr_protected :roles
推薦的作法,同時也是 Rails 4 解法
  • 使用 Strong parameters。( Rails 4 如果沒過 permit 會直接丟 exception,算直接根除)
  • params.require(:topic).permit(:title, :body)

進階做法

使用 Reform。出發點是 validation 的責任不應該在 Model 身上,也不該丟給 Controller 去解決,這件事應該要在 Form 層面被解決掉。Reform 的作者同時也是 Cells 的作者 apotonick。

def create
  @form = SongRequestForm.new(song: Song.new, artist: Artist.new)
 
  if @form.validate(params[:song_request])
  ....
require 'reform/rails'

class UserProfileForm < Reform::Form
  include DSL
  include Reform::Form::ActiveRecord

  property :email,        on: :user
  properties [:gender, :age],   on: :profile

  model :user

  validates :email, :gender, presence: true
  validates :age, numericality: true
  validates_uniqueness_of :email
end

2. Admin

第二部分要談的是 Admin 的設計。這部分也是很讓人無言的。根據非官方統計,高達...99% 的開發者會把 admin 後台放在 ... /admin...XD

http://example.org/[admin]

這樣的設計有幾個 concern :

  1. 容易被猜到放哪裡
  2. 容易被 XSS 打到

通常建議的解法

把 admin 拆到其他地方去,如 subdomin,或者是不同 domain。

http://[admin].example.org
http://[admin].example.net
https://[admin].example.org
https://stop-here.myapp.in

一些其他的基本解法

  • 拆到 Intranet 上。(Ineternet 上基本找不到)
  • WiteList.contains?(request.remote_ip) 只准白名單
  • 拆到另外一個 Admin App 去管

也有風險的地方:admin?

這也是另外一個有風險的部份。關於 admin? 的實作。多數的開發者,是這樣做的:

  def admin?
    is_admin
  end

然後,就被針對 massive assignment 的攻擊打下來了...

一些解法

  • Setting.admin_emails.include?(email) // 起碼沒有那麼直觀
  • 使用第三方驗證 gem : 如 Github 的 team warden-github-rails 作驗證。想法是:Github 比你家難打太多了....XD 而且其實會是 Admin 的人通常也是 Github 這個 repo 的參加者,用這種方式去驗證比較方便...


下篇: Secure Your Application : The Basic (2)

← 如何打造一個 Remote 工作的環境 Secure Your Application : The Basic (2) →
 
comments powered by Disqus