Skip to content

Commit ea7cf1c

Browse files
committed
added cookie fallback to Request::isFrom() for browsers without Sec-Fetch (Safari < 16.4)
1 parent 1a1bfa2 commit ea7cf1c

2 files changed

Lines changed: 55 additions & 7 deletions

File tree

src/Http/Request.php

Lines changed: 11 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@
88
namespace Nette\Http;
99

1010
use Nette;
11-
use function array_change_key_case, base64_decode, count, explode, func_num_args, in_array, preg_match, strcasecmp, strlen, strtr;
11+
use function array_change_key_case, array_intersect, base64_decode, count, explode, func_num_args, in_array, preg_match, strcasecmp, strlen, strtr;
1212

1313

1414
/**
@@ -240,6 +240,8 @@ public function isSameSite(): bool
240240

241241
/**
242242
* Checks whether the request matches the given Sec-Fetch-Site, Sec-Fetch-Dest and Sec-Fetch-User values.
243+
* Falls back to the SameSite=Strict cookie in browsers without Sec-Fetch (Safari < 16.4), which only
244+
* proves the request is not cross-site, so checks on $dest or $user then fail.
243245
* @param self::From*|list<self::From*> $site
244246
* @param string|list<string>|null $dest
245247
*/
@@ -253,8 +255,14 @@ public function isFrom(
253255
$actualDest = $this->headers['sec-fetch-dest'] ?? null;
254256
$actualUser = ($this->headers['sec-fetch-user'] ?? null) === '?1';
255257

256-
return $actualSite !== null
257-
&& in_array($actualSite, (array) $site, strict: true)
258+
if ($actualSite === null) { // fallback for browsers without Sec-Fetch (Safari < 16.4)
259+
return $dest === null
260+
&& $user === null
261+
&& isset($this->cookies[Helpers::StrictCookieName])
262+
&& array_intersect((array) $site, [self::FromSameOrigin, self::FromSameSite, self::FromNone]) !== [];
263+
}
264+
265+
return in_array($actualSite, (array) $site, strict: true)
258266
&& ($dest === null || ($actualDest !== null && in_array($actualDest, (array) $dest, strict: true)))
259267
&& ($user === null || $user === $actualUser);
260268
}

tests/Http/Request.isFrom.phpt

Lines changed: 44 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -71,15 +71,55 @@ test('accepts multiple expected values', function () {
7171
});
7272

7373

74-
test('invalid/typo site value fails', function () {
75-
$request = new Http\Request(new Http\UrlScript, headers: ['Sec-Fetch-Site' => 'same-origin']);
76-
Assert::false($request->isFrom('same-orgin'));
74+
test('invalid/typo site value fails consistently', function () {
75+
$header = new Http\Request(new Http\UrlScript, headers: ['Sec-Fetch-Site' => 'same-origin']);
76+
Assert::false($header->isFrom('same-orgin'));
77+
78+
$cookie = new Http\Request(new Http\UrlScript, cookies: [Http\Helpers::StrictCookieName => '1']);
79+
Assert::false($cookie->isFrom('same-orgin'));
7780
});
7881

7982

80-
test('no Sec-Fetch-Site returns false', function () {
83+
test('no header, no cookie returns false', function () {
8184
$request = new Http\Request(new Http\UrlScript);
8285

8386
Assert::false($request->isFrom('same-origin'));
8487
Assert::false($request->isFrom('cross-site'));
8588
});
89+
90+
91+
test('cookie fallback proves only "not cross-site"', function () {
92+
$request = new Http\Request(new Http\UrlScript, cookies: [
93+
Http\Helpers::StrictCookieName => '1',
94+
]);
95+
96+
Assert::true($request->isFrom('same-origin'));
97+
Assert::true($request->isFrom('same-site'));
98+
Assert::true($request->isFrom('none'));
99+
Assert::true($request->isFrom(['same-origin', 'cross-site']));
100+
Assert::false($request->isFrom('cross-site'));
101+
});
102+
103+
104+
test('cookie fallback fails closed for dest & user', function () {
105+
$request = new Http\Request(new Http\UrlScript, cookies: [
106+
Http\Helpers::StrictCookieName => '1',
107+
]);
108+
109+
// dest/user can't be proven by the cookie alone, so a stricter check must not pass
110+
Assert::false($request->isFrom('same-origin', 'document'));
111+
Assert::false($request->isFrom('same-origin', user: true));
112+
Assert::false($request->isFrom('same-origin', user: false));
113+
});
114+
115+
116+
test('cookie fallback not used when Sec-Fetch-Site present', function () {
117+
$request = new Http\Request(new Http\UrlScript, cookies: [
118+
Http\Helpers::StrictCookieName => '1',
119+
], headers: [
120+
'Sec-Fetch-Site' => 'cross-site',
121+
]);
122+
123+
Assert::false($request->isFrom('same-origin'));
124+
Assert::true($request->isFrom('cross-site'));
125+
});

0 commit comments

Comments
 (0)