§防範跨網站請求偽造
跨網站請求偽造 (CSRF) 是一種安全漏洞,攻擊者會誘騙受害者的瀏覽器使用受害者的工作階段發出請求。由於工作階段令牌會隨每個請求發送,如果攻擊者可以強迫受害者的瀏覽器代表他們發出請求,攻擊者就可以代表使用者發出請求。
建議您熟悉 CSRF、攻擊媒介是什麼以及攻擊媒介是什麼。我們建議先從 OWASP 的這份資訊 開始。
對於哪些請求是安全的,哪些請求容易受到 CSRF 請求的影響,並沒有簡單的答案;原因在於對於外掛程式和未來規格擴充允許什麼,並沒有明確的規範。過去,瀏覽器外掛程式和擴充功能放寬了框架先前認為可信賴的規則,為許多應用程式引入了 CSRF 漏洞,而修復這些漏洞的責任就在於框架。基於這個原因,Play 在其預設值中採取保守的作法,但允許您確切設定何時進行檢查。預設情況下,當所有下列條件都成立時,Play 會要求進行 CSRF 檢查
- 請求方法不是
GET
、HEAD
或OPTIONS
。 - 請求有一個或多個
Cookie
或Authorization
標頭。 - CORS 過濾器未設定為信任請求的來源。
注意:如果您使用除 cookie 或 HTTP 驗證以外的瀏覽器驗證,例如 NTLM 或基於用戶端憑證的驗證,則必須設定
play.filters.csrf.header.protectHeaders = null
,這將保護所有請求,或將驗證中使用的標頭包含在protectHeaders
中。
§Play 的 CSRF 保護
Play 支援多種方法來驗證請求不是 CSRF 請求。主要機制是 CSRF 令牌。此令牌會放置在每個提交表單的查詢字串或主體中,也會放置在使用者的工作階段中。然後,Play 會驗證兩個令牌是否存在且是否相符。
為了允許對非瀏覽器請求進行簡單的保護,Play 預設會檢查具有 Cookie
或 Authorization
標頭的請求。您可以設定 play.filters.csrf.header.protectHeaders
來定義執行 CSRF 檢查時必須存在的標頭。如果您使用 AJAX 提出請求,則可以將 CSRF 令牌放置在 HTML 頁面中,然後使用 Csrf-Token
標頭將其新增到請求中。
或者,您可以設定 play.filters.csrf.header.bypassHeaders
來比對常見的標頭:常見的設定如下
- 如果存在
X-Requested-With
標頭,Play 會將該要求視為安全。許多熱門的 Javascript 函式庫(例如 jQuery)會將X-Requested-With
加入要求中。 - 如果存在值為
nocheck
的Csrf-Token
標頭,或存在有效的 CSRF 令牌,Play 會將該要求視為安全。
設定如下所示
play.filters.csrf.header.bypassHeaders {
X-Requested-With = "*"
Csrf-Token = "nocheck"
}
使用此設定選項時應小心,因為瀏覽器外掛程式在過去曾破壞過此類型的 CSRF 防禦。
§信任 CORS 要求
預設情況下,如果您在 CSRF 篩選器之前有 CORS 篩選器,CSRF 篩選器會讓來自受信任來源的 CORS 要求通過。若要停用此檢查,請設定設定選項 play.filters.csrf.bypassCorsTrustedOrigins = false
。
§套用全域 CSRF 篩選器
注意:從 Play 2.6.x 開始,CSRF 篩選器包含在 Play 的預設篩選器清單中,會自動套用至專案。請參閱 篩選器頁面 以取得更多資訊。
Play 提供一個全域 CSRF 篩選器,可套用至所有要求。這是將 CSRF 防護新增至應用程式的最簡單方法。若要手動新增篩選器,請將其新增至 application.conf
play.filters.enabled += "play.filters.csrf.CSRFFilter"
您也可以在路由檔案中停用特定路由的 CSRF 篩選器。若要執行此操作,請在您的路由之前新增 nocsrf
修改器標籤
+ nocsrf
POST /api/new controllers.Api.newThing
§使用內含要求
所有 CSRF 功能都假設內含 RequestHeader
(或 Request
,它會延伸 RequestHeader
)在內含範圍中可用,且在沒有可用範圍的情況下不會編譯。範例如下所示。
§在動作中定義內含要求
對於所有需要存取 CSRF 令牌的動作,要求必須使用 implicit request =>
隱式公開,如下所示
// this actions needs to access CSRF token
def someMethod: Action[AnyContent] = Action { implicit request =>
// access the token as you need
Ok
}
這是因為像 CSRF.getToken
這樣的輔助方法存取會接收要求作為隱式參數來擷取 CSRF 令牌,例如
def someAction: Action[AnyContent] = Action { implicit request =>
accessToken // request is passed implicitly to accessToken
Ok("success")
}
def accessToken(implicit request: Request[_]) = {
val token = CSRF.getToken // request is passed implicitly to CSRF.getToken
}
§在方法之間傳遞隱式要求
如果您已將程式碼分成使用 CSRF 功能的方法,則可以從動作傳遞隱式要求
def action: Action[AnyContent] = Action { implicit request =>
anotherMethod("Some para value")
Ok
}
def anotherMethod(p: String)(implicit request: Request[_]) = {
// do something that needs access to the request
}
§在範本中定義隱式要求
您的 HTML 範本應包含隱式 RequestHeader
參數到您的範本,如果它還沒有,因為 CSRF.formField
輔助程式需要傳入一個(在下面討論更多)
@(...)(implicit request: RequestHeader)
由於您通常會將 CSRF 與需要 MessagesProvider
實例的表單輔助程式結合使用,您可能想要使用 MessagesAbstractController
或提供 MessagesRequestHeader
的其他控制器
@(...)(implicit request: MessagesRequestHeader)
或者,如果您使用具有 I18nSupport
的控制器,您可以將訊息作為單獨的隱式參數傳入
@(...)(implicit request: RequestHeader, messages: Messages)
§取得目前的令牌
可以使用 CSRF.getToken
方法存取目前的 CSRF 令牌。它需要一個隱式 RequestHeader
,因此請確保有一個在範圍內。
val token: Option[CSRF.Token] = CSRF.getToken
注意:如果安裝了 CSRF 過濾器,只要使用的 cookie 是 HttpOnly(表示無法從 JavaScript 存取),Play 便會嘗試避免產生 token。在傳送具有嚴格主體的回應時,除非已呼叫
CSRF.getToken
,否則 Play 會略過將 token 加入回應。對於不需要 CSRF token 的回應,這會大幅提升效能。如果未將 cookie 設定為 HttpOnly,Play 會假設您希望從 JavaScript 存取它,並在不論如何的情況下產生它。
如果您沒有使用 CSRF 過濾器,您也應該注入 CSRFAddToken
和 CSRFCheck
動作包裝函式,以強制在特定動作上加入 token 或 CSRF 檢查。否則,token 將無法使用。
import play.api.mvc._
import play.api.mvc.Results._
import play.filters.csrf._
import play.filters.csrf.CSRF.Token
class CSRFController(components: ControllerComponents, addToken: CSRFAddToken, checkToken: CSRFCheck)
extends AbstractController(components) {
def getToken =
addToken(Action { implicit request =>
val Token(name, value) = CSRF.getToken.get
Ok(s"$name=$value")
})
}
為了協助將 CSRF token 加入表單,Play 提供了一些範本輔助程式。第一個會將它加入動作 URL 的查詢字串
@import helper._
@form(CSRF(routes.ItemsController.save())) {
...
}
這可能會產生類似這樣的表單
<form method="POST" action="/items?csrfToken=1234567890abcdef">
...
</form>
如果不想在查詢字串中包含 token,Play 也提供一個輔助程式,用於將 CSRF token 加入表單中的隱藏欄位
@form(routes.ItemsController.save()) {
@CSRF.formField
...
}
這可能會產生類似這樣的表單
<form method="POST" action="/items">
<input type="hidden" name="csrfToken" value="1234567890abcdef"/>
...
</form>
§將 CSRF token 加入工作階段
為了確保 CSRF token 可用於在表單中產生,並傳送回用戶端,如果輸入要求中尚未提供 token,全域過濾器會為所有接受 HTML 的 GET 要求產生新的 token。
§逐動作套用 CSRF 過濾
有時,全域 CSRF 過濾可能不適當,例如在應用程式可能想要允許一些跨來源表單張貼的情況。一些非基於工作階段的標準(例如 OpenID 2.0)需要使用跨網站表單張貼,或在伺服器到伺服器 RPC 通訊中使用表單提交。
在這些情況下,Play 提供兩個可以與應用程式動作組合的動作。
第一個動作是 CSRFCheck
動作,它會執行檢查。它應該加入所有接受工作階段驗證的 POST 表單提交的動作
import play.api.mvc._
import play.filters.csrf._
def save = checkToken {
Action { implicit req =>
// handle body
Ok
}
}
第二個動作是CSRFAddToken
動作,它會產生 CSRF 令牌(如果輸入要求中尚未存在)。它應該新增到所有呈現表單的動作
import play.api.mvc._
import play.filters.csrf._
def form = addToken {
Action { implicit req => Ok(views.html.itemsForm) }
}
套用這些動作的更方便方式是將它們與 Play 的動作組成結合使用
import play.api.mvc._
import play.filters.csrf._
class PostAction @Inject() (parser: BodyParsers.Default) extends ActionBuilderImpl(parser) {
override def invokeBlock[A](request: Request[A], block: (Request[A]) => Future[Result]) = {
// authentication code here
block(request)
}
override def composeAction[A](action: Action[A]) = checkToken(action)
}
class GetAction @Inject() (parser: BodyParsers.Default) extends ActionBuilderImpl(parser) {
override def invokeBlock[A](request: Request[A], block: (Request[A]) => Future[Result]) = {
// authentication code here
block(request)
}
override def composeAction[A](action: Action[A]) = addToken(action)
}
然後您可以將撰寫動作所需的樣板程式碼減到最少
def save: Action[AnyContent] = postAction {
// handle body
Ok
}
def form: Action[AnyContent] = getAction { implicit req => Ok(views.html.itemsForm) }
§CSRF 組態選項
CSRF 組態選項的完整範圍可以在 filters reference.conf中找到。一些範例包括
play.filters.csrf.token.name
- 令牌的名稱,用於會話和要求主體/查詢字串中。預設為csrfToken
。play.filters.csrf.cookie.name
- 如果已設定,Play 會將 CSRF 令牌儲存在具有給定名稱的 cookie 中,而不是會話中。play.filters.csrf.cookie.secure
- 如果設定了play.filters.csrf.cookie.name
,CSRF cookie 是否應設定為安全標記。預設值與play.http.session.secure
相同。play.filters.csrf.body.bufferSize
- 為了從主體中讀取令牌,Play 必須先將主體暫存,並有可能加以剖析。這會設定用於暫存主體的最大暫存大小。預設為 100k。play.filters.csrf.token.sign
- Play 是否應使用已簽署的 CSRF 令牌。已簽署的 CSRF 令牌可確保每個要求的令牌值都是隨機的,因此可以擊敗 BREACH 類型的攻擊。
§使用編譯時相依性注入的 CSRF
如果你的應用程式使用編譯時期依賴注入,你可以使用所有上述功能。連線由特徵 CSRFComponents 提供協助,你可以將其混入應用程式元件蛋糕中。有關編譯時期依賴注入的更多詳細資訊,請參閱 相關文件頁面。
§測試 CSRF
在呈現時,你可能需要將 CSRF 令牌新增至範本。你可以使用 import play.api.test.CSRFTokenHelper._
來執行此操作,它會使用 withCSRFToken
方法豐富 play.api.test.FakeRequest
import play.api.test.CSRFTokenHelper._
import play.api.test.FakeRequest
import play.api.test.Helpers._
import play.api.test.WithApplication
class UserControllerSpec extends Specification {
"UserController GET" should {
"render the index page from the application" in new WithApplication() {
override def running() = {
val controller = app.injector.instanceOf[UserController]
val request = FakeRequest().withCSRFToken
val result = controller.userGet().apply(request)
status(result) must beEqualTo(OK)
contentType(result) must beSome("text/html")
}
}
}
}
下一步:自訂驗證
在此文件發現錯誤?此頁面的原始程式碼可以在 這裡 找到。在閱讀 文件指南 後,請隨時貢獻一個拉取請求。有問題或建議要分享?請前往 我們的社群論壇 與社群展開對話。