mirror of
https://github.com/yiisoft/yii2.git
synced 2025-10-30 01:56:35 +08:00
Merge pull request #20280 from olegbaturin/20279-custom-csrf-header
add CSRF validation by custom HTTP header
This commit is contained in:
@ -14,6 +14,9 @@ Yii Framework 2 Change Log
|
||||
- Enh #20273: Remove unnecessary `paragonie/random_compat` dependency (timwolla)
|
||||
- Chg #20276: Removed autogenerated migration phpdoc (userator)
|
||||
- Bug #20284: Revert punycode to 1.4.x which supports pre ES6 format (mtangoo)
|
||||
- New #20279: Add to the `\yii\web\Request` CSRF validation by custom HTTP header (olegbaturin)
|
||||
- Enh #20279: Add to the `\yii\web\Request` `csrfHeader` property to configure a custom HTTP header for CSRF validation (olegbaturin)
|
||||
- Enh #20279: Add to the `\yii\web\Request` `csrfTokenSafeMethods` property to configure a custom safe HTTP methods list (olegbaturin)
|
||||
|
||||
2.0.51 July 18, 2024
|
||||
--------------------
|
||||
|
||||
@ -42,7 +42,7 @@ use yii\validators\IpValidator;
|
||||
* not available.
|
||||
* @property-read CookieCollection $cookies The cookie collection.
|
||||
* @property-read string $csrfToken The token used to perform CSRF validation.
|
||||
* @property-read string|null $csrfTokenFromHeader The CSRF token sent via [[CSRF_HEADER]] by browser. Null is
|
||||
* @property-read string|null $csrfTokenFromHeader The CSRF token sent via [[csrfHeader]] by browser. Null is
|
||||
* returned if no such header is sent.
|
||||
* @property-read array $eTags The entity tags.
|
||||
* @property-read HeaderCollection $headers The header collection.
|
||||
@ -91,7 +91,7 @@ use yii\validators\IpValidator;
|
||||
class Request extends \yii\base\Request
|
||||
{
|
||||
/**
|
||||
* The name of the HTTP header for sending CSRF token.
|
||||
* Default name of the HTTP header for sending CSRF token.
|
||||
*/
|
||||
const CSRF_HEADER = 'X-CSRF-Token';
|
||||
/**
|
||||
@ -113,10 +113,41 @@ class Request extends \yii\base\Request
|
||||
* `yii.getCsrfToken()`, respectively. The [[\yii\web\YiiAsset]] asset must be registered.
|
||||
* You also need to include CSRF meta tags in your pages by using [[\yii\helpers\Html::csrfMetaTags()]].
|
||||
*
|
||||
* For SPA, you can use CSRF validation by custom header with a random or an empty value.
|
||||
* Include a header with the name specified by [[csrfHeader]] to requests that must be validated.
|
||||
* Warning! CSRF validation by custom header can be used only for same-origin requests or
|
||||
* with CORS configured to allow requests from the list of specific origins only.
|
||||
*
|
||||
* @see Controller::enableCsrfValidation
|
||||
* @see https://en.wikipedia.org/wiki/Cross-site_request_forgery
|
||||
*/
|
||||
public $enableCsrfValidation = true;
|
||||
/**
|
||||
* @var string the name of the HTTP header for sending CSRF token. Defaults to [[CSRF_HEADER]].
|
||||
* This property may be changed for Yii API applications only.
|
||||
* Don't change this property for Yii Web application.
|
||||
*/
|
||||
public $csrfHeader = self::CSRF_HEADER;
|
||||
/**
|
||||
* @var array the name of the HTTP header for sending CSRF token.
|
||||
* by default validate CSRF token on non-"safe" methods only
|
||||
* This property is used only when [[enableCsrfValidation]] is true.
|
||||
* @see https://datatracker.ietf.org/doc/html/rfc9110#name-safe-methods
|
||||
*/
|
||||
public $csrfTokenSafeMethods = ['GET', 'HEAD', 'OPTIONS'];
|
||||
/**
|
||||
* @var array "unsafe" methods not triggered a CORS-preflight request
|
||||
* This property is used only when both [[enableCsrfValidation]] and [[validateCsrfHeaderOnly]] are true.
|
||||
* @see https://fetch.spec.whatwg.org/#http-cors-protocol
|
||||
*/
|
||||
public $csrfHeaderUnsafeMethods = ['GET', 'HEAD', 'POST'];
|
||||
/**
|
||||
* @var bool whether to use custom header only to CSRF validation of SPA. Defaults to false.
|
||||
* If false and [[enableCsrfValidation]] is true, CSRF validation by token will used.
|
||||
* Warning! CSRF validation by custom header can be used for Yii API applications only.
|
||||
* @see https://cheatsheetseries.owasp.org/cheatsheets/Cross-Site_Request_Forgery_Prevention_Cheat_Sheet.html#employing-custom-request-headers-for-ajaxapi
|
||||
*/
|
||||
public $validateCsrfHeaderOnly = false;
|
||||
/**
|
||||
* @var string the name of the token used to prevent CSRF. Defaults to '_csrf'.
|
||||
* This property is used only when [[enableCsrfValidation]] is true.
|
||||
@ -1772,10 +1803,14 @@ class Request extends \yii\base\Request
|
||||
* along via a hidden field of an HTML form or an HTTP header value to support CSRF validation.
|
||||
* @param bool $regenerate whether to regenerate CSRF token. When this parameter is true, each time
|
||||
* this method is called, a new CSRF token will be generated and persisted (in session or cookie).
|
||||
* @return string the token used to perform CSRF validation.
|
||||
* @return null|string the token used to perform CSRF validation. Null is returned if the [[validateCsrfHeaderOnly]] is true.
|
||||
*/
|
||||
public function getCsrfToken($regenerate = false)
|
||||
{
|
||||
if ($this->validateCsrfHeaderOnly) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if ($this->_csrfToken === null || $regenerate) {
|
||||
$token = $this->loadCsrfToken();
|
||||
if ($regenerate || empty($token)) {
|
||||
@ -1819,11 +1854,11 @@ class Request extends \yii\base\Request
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string|null the CSRF token sent via [[CSRF_HEADER]] by browser. Null is returned if no such header is sent.
|
||||
* @return string|null the CSRF token sent via [[csrfHeader]] by browser. Null is returned if no such header is sent.
|
||||
*/
|
||||
public function getCsrfTokenFromHeader()
|
||||
{
|
||||
return $this->headers->get(static::CSRF_HEADER);
|
||||
return $this->headers->get($this->csrfHeader);
|
||||
}
|
||||
|
||||
/**
|
||||
@ -1860,8 +1895,14 @@ class Request extends \yii\base\Request
|
||||
public function validateCsrfToken($clientSuppliedToken = null)
|
||||
{
|
||||
$method = $this->getMethod();
|
||||
// only validate CSRF token on non-"safe" methods https://tools.ietf.org/html/rfc2616#section-9.1.1
|
||||
if (!$this->enableCsrfValidation || in_array($method, ['GET', 'HEAD', 'OPTIONS'], true)) {
|
||||
|
||||
if ($this->validateCsrfHeaderOnly) {
|
||||
return in_array($method, $this->csrfHeaderUnsafeMethods, true)
|
||||
? $this->headers->has($this->csrfHeader)
|
||||
: true;
|
||||
}
|
||||
|
||||
if (!$this->enableCsrfValidation || in_array($method, $this->csrfTokenSafeMethods, true)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
|
||||
@ -211,6 +211,124 @@ class RequestTest extends TestCase
|
||||
}
|
||||
}
|
||||
|
||||
public function testCustomSafeMethodsCsrfTokenValidation()
|
||||
{
|
||||
$this->mockWebApplication();
|
||||
|
||||
$request = new Request();
|
||||
$request->csrfTokenSafeMethods = ['OPTIONS'];
|
||||
$request->enableCsrfCookie = false;
|
||||
$request->enableCsrfValidation = true;
|
||||
|
||||
$token = $request->getCsrfToken();
|
||||
|
||||
// accept any value on custom safe request
|
||||
foreach (['OPTIONS'] as $method) {
|
||||
$_SERVER['REQUEST_METHOD'] = $method;
|
||||
$this->assertTrue($request->validateCsrfToken($token));
|
||||
$this->assertTrue($request->validateCsrfToken($token . 'a'));
|
||||
$this->assertTrue($request->validateCsrfToken([]));
|
||||
$this->assertTrue($request->validateCsrfToken([$token]));
|
||||
$this->assertTrue($request->validateCsrfToken(0));
|
||||
$this->assertTrue($request->validateCsrfToken(null));
|
||||
$this->assertTrue($request->validateCsrfToken());
|
||||
}
|
||||
|
||||
// only accept valid token on other requests
|
||||
foreach (['GET', 'HEAD', 'POST'] as $method) {
|
||||
$_SERVER['REQUEST_METHOD'] = $method;
|
||||
$this->assertTrue($request->validateCsrfToken($token));
|
||||
$this->assertFalse($request->validateCsrfToken($token . 'a'));
|
||||
$this->assertFalse($request->validateCsrfToken([]));
|
||||
$this->assertFalse($request->validateCsrfToken([$token]));
|
||||
$this->assertFalse($request->validateCsrfToken(0));
|
||||
$this->assertFalse($request->validateCsrfToken(null));
|
||||
$this->assertFalse($request->validateCsrfToken());
|
||||
}
|
||||
}
|
||||
|
||||
public function testCsrfHeaderValidation()
|
||||
{
|
||||
$this->mockWebApplication();
|
||||
|
||||
$request = new Request();
|
||||
$request->validateCsrfHeaderOnly = true;
|
||||
$request->enableCsrfValidation = true;
|
||||
|
||||
// only accept valid header on unsafe requests
|
||||
foreach (['GET', 'HEAD', 'POST'] as $method) {
|
||||
$_SERVER['REQUEST_METHOD'] = $method;
|
||||
$request->headers->remove(Request::CSRF_HEADER);
|
||||
$this->assertFalse($request->validateCsrfToken());
|
||||
|
||||
$request->headers->add(Request::CSRF_HEADER, '');
|
||||
$this->assertTrue($request->validateCsrfToken());
|
||||
}
|
||||
|
||||
// accept no value on other requests
|
||||
foreach (['DELETE', 'PATCH', 'PUT', 'OPTIONS'] as $method) {
|
||||
$_SERVER['REQUEST_METHOD'] = $method;
|
||||
$this->assertTrue($request->validateCsrfToken());
|
||||
}
|
||||
}
|
||||
|
||||
public function testCustomHeaderCsrfHeaderValidation()
|
||||
{
|
||||
$this->mockWebApplication();
|
||||
|
||||
$request = new Request();
|
||||
$request->csrfHeader = 'X-JGURDA';
|
||||
$request->validateCsrfHeaderOnly = true;
|
||||
$request->enableCsrfValidation = true;
|
||||
|
||||
// only accept valid header on unsafe requests
|
||||
foreach (['GET', 'HEAD', 'POST'] as $method) {
|
||||
$_SERVER['REQUEST_METHOD'] = $method;
|
||||
$request->headers->remove('X-JGURDA');
|
||||
$this->assertFalse($request->validateCsrfToken());
|
||||
|
||||
$request->headers->add('X-JGURDA', '');
|
||||
$this->assertTrue($request->validateCsrfToken());
|
||||
}
|
||||
}
|
||||
|
||||
public function testCustomUnsafeMethodsCsrfHeaderValidation()
|
||||
{
|
||||
$this->mockWebApplication();
|
||||
|
||||
$request = new Request();
|
||||
$request->csrfHeaderUnsafeMethods = ['POST'];
|
||||
$request->validateCsrfHeaderOnly = true;
|
||||
$request->enableCsrfValidation = true;
|
||||
|
||||
// only accept valid custom header on unsafe requests
|
||||
foreach (['POST'] as $method) {
|
||||
$_SERVER['REQUEST_METHOD'] = $method;
|
||||
$request->headers->remove(Request::CSRF_HEADER);
|
||||
$this->assertFalse($request->validateCsrfToken());
|
||||
|
||||
$request->headers->add(Request::CSRF_HEADER, '');
|
||||
$this->assertTrue($request->validateCsrfToken());
|
||||
}
|
||||
|
||||
// accept no value on other requests
|
||||
foreach (['GET', 'HEAD'] as $method) {
|
||||
$_SERVER['REQUEST_METHOD'] = $method;
|
||||
$request->headers->remove(Request::CSRF_HEADER);
|
||||
$this->assertTrue($request->validateCsrfToken());
|
||||
}
|
||||
}
|
||||
|
||||
public function testNoCsrfTokenCsrfHeaderValidation()
|
||||
{
|
||||
$this->mockWebApplication();
|
||||
|
||||
$request = new Request();
|
||||
$request->validateCsrfHeaderOnly = true;
|
||||
|
||||
$this->assertEquals($request->getCsrfToken(), null);
|
||||
}
|
||||
|
||||
public function testResolve()
|
||||
{
|
||||
$this->mockWebApplication([
|
||||
|
||||
Reference in New Issue
Block a user