Skip to content

Commit be27298

Browse files
committed
added Request::isFrom() WIP
1 parent e3e4414 commit be27298

3 files changed

Lines changed: 122 additions & 1 deletion

File tree

src/Http/IRequest.php

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212
* HTTP request contract providing access to URL, headers, cookies, uploaded files, and body.
1313
* @method ?UrlImmutable getReferer() Returns the referrer URL.
1414
* @method bool isSameSite() Checks whether the request is coming from the same site.
15+
* @method bool isFrom(string|list<string>|null $site = null, string|list<string>|null $initiator = null)
1516
*/
1617
interface IRequest
1718
{

src/Http/Request.php

Lines changed: 25 additions & 1 deletion
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, gethostbyaddr, implode, in_array, preg_match, preg_match_all, rsort, strcasecmp, strtr;
11+
use function array_change_key_case, base64_decode, count, explode, func_num_args, gethostbyaddr, implode, in_array, preg_match, preg_match_all, rsort, strcasecmp, strtr;
1212

1313

1414
/**
@@ -238,6 +238,30 @@ public function isSameSite(): bool
238238
}
239239

240240

241+
/**
242+
* Checks whether the request origin and initiator match the given Sec-Fetch-Site and Sec-Fetch-Dest values.
243+
* Falls back to the Origin header for browsers that don't send Sec-Fetch headers (Safari < 16.4).
244+
* @param string|list<string>|null $site expected Sec-Fetch-Site values (e.g. 'same-origin', 'cross-site')
245+
* @param string|list<string>|null $initiator expected Sec-Fetch-Dest values (e.g. 'document', 'empty')
246+
*/
247+
public function isFrom(string|array|null $site = null, string|array|null $initiator = null): bool
248+
{
249+
$actualSite = $this->headers['sec-fetch-site'] ?? null;
250+
$actualDest = $this->headers['sec-fetch-dest'] ?? null;
251+
252+
if ($actualSite === null && ($origin = $this->getOrigin())) { // fallback for Safari < 16.4
253+
$actualSite = strcasecmp($origin->getScheme(), $this->url->getScheme()) === 0
254+
&& strcasecmp(rtrim($origin->getHost(), '.'), rtrim($this->url->getHost(), '.')) === 0
255+
&& $origin->getPort() === $this->url->getPort()
256+
? 'same-origin'
257+
: 'cross-site';
258+
}
259+
260+
return ($site === null || ($actualSite !== null && in_array($actualSite, (array) $site, strict: true)))
261+
&& ($initiator === null || ($actualDest !== null && in_array($actualDest, (array) $initiator, strict: true)));
262+
}
263+
264+
241265
/**
242266
* Checks whether the request was made via AJAX (X-Requested-With: XMLHttpRequest).
243267
*/

tests/Http/Request.isFrom.phpt

Lines changed: 96 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,96 @@
1+
<?php declare(strict_types=1);
2+
3+
use Nette\Http;
4+
use Tester\Assert;
5+
6+
require __DIR__ . '/../bootstrap.php';
7+
8+
9+
test('matches both headers', function () {
10+
$request = new Http\Request(new Http\UrlScript, headers: [
11+
'Sec-Fetch-Site' => 'same-origin',
12+
'Sec-Fetch-Dest' => 'document',
13+
]);
14+
15+
Assert::true($request->isFrom('same-origin', 'document'));
16+
});
17+
18+
19+
test('fails when expected header missing', function () {
20+
$request = new Http\Request(new Http\UrlScript, headers: [
21+
'Sec-Fetch-Site' => 'same-origin',
22+
]);
23+
24+
Assert::false($request->isFrom('same-origin', 'document'));
25+
});
26+
27+
28+
test('accepts multiple expected values', function () {
29+
$request = new Http\Request(new Http\UrlScript, headers: [
30+
'Sec-Fetch-Site' => 'cross-site',
31+
'Sec-Fetch-Dest' => 'image',
32+
]);
33+
34+
Assert::true($request->isFrom(['same-origin', 'cross-site'], ['document', 'image']));
35+
Assert::false($request->isFrom(['cross-site'], ['Document']));
36+
Assert::false($request->isFrom(['Cross-Site'], ['image']));
37+
});
38+
39+
40+
test('fallback same-origin from Origin header', function () {
41+
$url = new Http\UrlScript('https://nette.org/app/');
42+
$request = new Http\Request($url, headers: [
43+
'Origin' => 'https://nette.org',
44+
]);
45+
46+
Assert::true($request->isFrom('same-origin'));
47+
});
48+
49+
50+
test('fallback cross-site from Origin header', function () {
51+
$url = new Http\UrlScript('https://nette.org/');
52+
$request = new Http\Request($url, headers: [
53+
'Origin' => 'https://example.com',
54+
]);
55+
56+
Assert::true($request->isFrom('cross-site'));
57+
});
58+
59+
60+
test('fallback missing without Origin header', function () {
61+
$url = new Http\UrlScript('https://nette.org/');
62+
$request = new Http\Request($url);
63+
64+
Assert::false($request->isFrom('same-origin'));
65+
});
66+
67+
68+
test('fallback not used when header present', function () {
69+
$url = new Http\UrlScript('https://nette.org/');
70+
$request = new Http\Request($url, headers: [
71+
'Sec-Fetch-Site' => 'none',
72+
'Origin' => 'https://nette.org',
73+
]);
74+
75+
Assert::false($request->isFrom('same-origin'));
76+
});
77+
78+
79+
test('fallback cross-site when port differs', function () {
80+
$url = new Http\UrlScript('https://nette.org:443');
81+
$request = new Http\Request($url, headers: [
82+
'Origin' => 'https://nette.org:444',
83+
]);
84+
85+
Assert::true($request->isFrom('cross-site'));
86+
});
87+
88+
89+
test('fallback ignored for invalid Origin', function () {
90+
$url = new Http\UrlScript('https://nette.org/');
91+
$request = new Http\Request($url, headers: [
92+
'Origin' => 'null',
93+
]);
94+
95+
Assert::false($request->isFrom('same-origin'));
96+
});

0 commit comments

Comments
 (0)