這是我六月初在新加坡 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 都會寫出這樣的程式碼:
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 裡,使用者擁有很多角色。
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 :
- 容易被猜到放哪裡
- 容易被 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 的參加者,用這種方式去驗證比較方便...