文件

§防範跨網站請求偽造

跨網站請求偽造 (CSRF) 是一種安全漏洞,攻擊者會誘騙受害者的瀏覽器使用受害者的工作階段發出請求。由於工作階段令牌會隨每個請求發送,如果攻擊者可以強迫受害者的瀏覽器代表他們發出請求,攻擊者就可以代表使用者發出請求。

建議您熟悉 CSRF、攻擊媒介是什麼以及攻擊媒介是什麼。我們建議先從 OWASP 的這份資訊 開始。

對於哪些請求是安全的,哪些請求容易受到 CSRF 請求的影響,並沒有簡單的答案;原因在於對於外掛程式和未來規格擴充允許什麼,並沒有明確的規範。過去,瀏覽器外掛程式和擴充功能放寬了框架先前認為可信賴的規則,為許多應用程式引入了 CSRF 漏洞,而修復這些漏洞的責任就在於框架。基於這個原因,Play 在其預設值中採取保守的作法,但允許您確切設定何時進行檢查。預設情況下,當所有下列條件都成立時,Play 會要求進行 CSRF 檢查

注意:如果您使用除 cookie 或 HTTP 驗證以外的瀏覽器驗證,例如 NTLM 或基於用戶端憑證的驗證,則必須設定 play.filters.csrf.header.protectHeaders = null,這將保護所有請求,或將驗證中使用的標頭包含在 protectHeaders 中。

§Play 的 CSRF 保護

Play 支援多種方法來驗證請求不是 CSRF 請求。主要機制是 CSRF 令牌。此令牌會放置在每個提交表單的查詢字串或主體中,也會放置在使用者的工作階段中。然後,Play 會驗證兩個令牌是否存在且是否相符。

為了允許對非瀏覽器請求進行簡單的保護,Play 預設會檢查具有 CookieAuthorization 標頭的請求。您可以設定 play.filters.csrf.header.protectHeaders 來定義執行 CSRF 檢查時必須存在的標頭。如果您使用 AJAX 提出請求,則可以將 CSRF 令牌放置在 HTML 頁面中,然後使用 Csrf-Token 標頭將其新增到請求中。

或者,您可以設定 play.filters.csrf.header.bypassHeaders 來比對常見的標頭:常見的設定如下

設定如下所示

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 過濾器,您也應該注入 CSRFAddTokenCSRFCheck 動作包裝函式,以強制在特定動作上加入 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中找到。一些範例包括

§使用編譯時相依性注入的 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")
      }
    }
  }
}

下一步:自訂驗證


在此文件發現錯誤?此頁面的原始程式碼可以在 這裡 找到。在閱讀 文件指南 後,請隨時貢獻一個拉取請求。有問題或建議要分享?請前往 我們的社群論壇 與社群展開對話。