Skip to content

Commit d323f69

Browse files
committed
Merge branch '4.18' of https://github.com/craftcms/cms into 5.10
# Conflicts: # CHANGELOG-WIP.md
2 parents 403f949 + c843075 commit d323f69

3 files changed

Lines changed: 186 additions & 0 deletions

File tree

CHANGELOG-WIP.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@
3333
- `delete` GraphQL queries now have a `hardDelete` argument. ([#18511](https://github.com/craftcms/cms/pull/18511))
3434
- Entry `postDate` values are now `null` on creation, rather than set to the `dateCreated` value. ([#18642](https://github.com/craftcms/cms/pull/18642))
3535
- Assets’ `url` GraphQL fields’ `immediately` arguments are no longer deprecated. ([#18581](https://github.com/craftcms/cms/issues/18581))
36+
- Added `craft\filters\SecFetchSiteFilter` for request origin verification. ([#18641](https://github.com/craftcms/cms/pull/18641))
3637
- `craft\fields\data\LinkData::getUrl()` now has an `$anyStatus` argument, which can be set to `false` to prevent a value from being returned if a disabled/pending/expired element is linked. ([#18527](https://github.com/craftcms/cms/issues/18527))
3738

3839
### Extensibility

src/filters/SecFetchSiteFilter.php

Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
1+
<?php
2+
/**
3+
* @link https://craftcms.com/
4+
* @copyright Copyright (c) Pixel & Tonic, Inc.
5+
* @license https://craftcms.github.io/license/
6+
*/
7+
8+
namespace craft\filters;
9+
10+
use Craft;
11+
use yii\base\ActionFilter;
12+
use yii\web\BadRequestHttpException;
13+
14+
/**
15+
* Action filter for validating the `Sec-Fetch-Site` header.
16+
*
17+
* @since 5.10.0
18+
*/
19+
class SecFetchSiteFilter extends ActionFilter
20+
{
21+
use ConditionalFilterTrait;
22+
23+
/**
24+
* Whether to use origin verification only (no CSRF token fallback).
25+
*/
26+
public bool $originOnly = true;
27+
28+
/**
29+
* Whether to accept `same-site` in addition to `same-origin` (e.g. subdomains).
30+
*/
31+
public bool $allowSameSite = false;
32+
33+
public string $headerName = 'Sec-Fetch-Site';
34+
35+
public ?string $errorMessage = null;
36+
37+
public ?array $safeMethods = null;
38+
39+
/**
40+
* @inheritdoc
41+
*/
42+
public function beforeAction($action): bool
43+
{
44+
$this->setDefaults();
45+
46+
$request = Craft::$app->getRequest();
47+
48+
if (in_array($request->getMethod(), $this->safeMethods, true)) {
49+
return true;
50+
}
51+
52+
$secFetchSite = $request->getHeaders()->get($this->headerName);
53+
54+
if ($secFetchSite === 'same-origin') {
55+
return true;
56+
}
57+
58+
if ($secFetchSite === 'same-site' && $this->allowSameSite) {
59+
return true;
60+
}
61+
62+
if ($this->originOnly) {
63+
throw new BadRequestHttpException($this->errorMessage);
64+
}
65+
66+
return true;
67+
}
68+
69+
private function setDefaults(): void
70+
{
71+
$this->safeMethods = $this->safeMethods ?? Craft::$app->getRequest()->csrfTokenSafeMethods;
72+
$this->errorMessage = $this->errorMessage ?? Craft::t('yii', 'Unable to verify your data submission.');
73+
}
74+
}
Lines changed: 111 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,111 @@
1+
<?php
2+
/**
3+
* @link https://craftcms.com/
4+
* @copyright Copyright (c) Pixel & Tonic, Inc.
5+
* @license https://craftcms.github.io/license/
6+
*/
7+
8+
namespace crafttests\unit\filters;
9+
10+
use Craft;
11+
use craft\filters\SecFetchSiteFilter;
12+
use craft\test\TestCase;
13+
use craft\web\Request;
14+
use yii\base\Action;
15+
use yii\web\BadRequestHttpException;
16+
use yii\web\Controller;
17+
18+
/**
19+
* Unit tests for SecFetchSiteFilter
20+
*
21+
* @author Pixel & Tonic, Inc. <support@pixelandtonic.com>
22+
* @since 5.10.0
23+
*/
24+
class SecFetchSiteFilterTest extends TestCase
25+
{
26+
private SecFetchSiteFilter $filter;
27+
private Action $action;
28+
private Request $request;
29+
30+
protected function setUp(): void
31+
{
32+
parent::setUp();
33+
34+
$controller = $this->createMock(Controller::class);
35+
$this->action = new Action('test-action', $controller);
36+
$this->filter = new SecFetchSiteFilter();
37+
$this->request = Craft::$app->getRequest();
38+
}
39+
40+
public function testAllowsSameOriginForUnsafeMethods(): void
41+
{
42+
$_SERVER['REQUEST_METHOD'] = 'POST';
43+
$this->request->getHeaders()->set('Sec-Fetch-Site', 'same-origin');
44+
45+
self::assertTrue($this->filter->beforeAction($this->action));
46+
}
47+
48+
public function testAllowsSameSiteWhenConfigured(): void
49+
{
50+
$_SERVER['REQUEST_METHOD'] = 'POST';
51+
$this->request->getHeaders()->set('Sec-Fetch-Site', 'same-site');
52+
53+
$this->filter->allowSameSite = true;
54+
self::assertTrue($this->filter->beforeAction($this->action));
55+
}
56+
57+
public function testAllowsFallbackWhenHeaderMissingAndNotOriginOnly(): void
58+
{
59+
$_SERVER['REQUEST_METHOD'] = 'POST';
60+
$this->request->getHeaders()->remove('Sec-Fetch-Site');
61+
62+
$this->filter->originOnly = false;
63+
self::assertTrue($this->filter->beforeAction($this->action));
64+
}
65+
66+
public function testRejectsWhenOriginOnlyAndHeaderInvalid(): void
67+
{
68+
$_SERVER['REQUEST_METHOD'] = 'POST';
69+
$this->request->getHeaders()->set('Sec-Fetch-Site', 'cross-site');
70+
71+
$this->filter->originOnly = true;
72+
73+
$this->expectException(BadRequestHttpException::class);
74+
$this->filter->beforeAction($this->action);
75+
}
76+
77+
public function testEnforcesWhenCsrfDisabled(): void
78+
{
79+
$_SERVER['REQUEST_METHOD'] = 'POST';
80+
$this->request->getHeaders()->set('Sec-Fetch-Site', 'cross-site');
81+
82+
$original = $this->request->enableCsrfValidation;
83+
$this->request->enableCsrfValidation = false;
84+
85+
try {
86+
$this->filter->originOnly = true;
87+
$this->expectException(BadRequestHttpException::class);
88+
$this->filter->beforeAction($this->action);
89+
} finally {
90+
$this->request->enableCsrfValidation = $original;
91+
}
92+
}
93+
94+
public function testSkipsSafeMethods(): void
95+
{
96+
$_SERVER['REQUEST_METHOD'] = 'GET';
97+
$this->request->getHeaders()->set('Sec-Fetch-Site', 'cross-site');
98+
99+
$this->filter->originOnly = true;
100+
self::assertTrue($this->filter->beforeAction($this->action));
101+
}
102+
103+
public function testInvalidHeaderFallsThroughWhenNotOriginOnly(): void
104+
{
105+
$_SERVER['REQUEST_METHOD'] = 'POST';
106+
$this->request->getHeaders()->set('Sec-Fetch-Site', 'cross-site');
107+
108+
$this->filter->originOnly = false;
109+
self::assertTrue($this->filter->beforeAction($this->action));
110+
}
111+
}

0 commit comments

Comments
 (0)