Skip to content

Commit 4ec1558

Browse files
committed
added Request::isFrom() WIP
1 parent 4b03d58 commit 4ec1558

3 files changed

Lines changed: 121 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 provides access scheme for request sent via HTTP.
1313
* @method ?UrlImmutable getReferer() Returns referrer.
1414
* @method bool isSameSite() Is the request sent from the same origin?
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: 24 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, func_num_args, gethostbyaddr, implode, preg_match, preg_match_all, rsort, strcasecmp, strtolower, 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
use const CASE_LOWER;
1313

1414

@@ -238,6 +238,29 @@ public function isSameSite(): bool
238238
}
239239

240240

241+
/**
242+
* Checks whether Sec-Fetch headers match the expected values.
243+
* @param string|list<string>|null $site
244+
* @param string|list<string>|null $initiator
245+
*/
246+
public function isFrom(string|array|null $site = null, string|array|null $initiator = null): bool
247+
{
248+
$actualSite = $this->headers['sec-fetch-site'] ?? null;
249+
$actualDest = $this->headers['sec-fetch-dest'] ?? null;
250+
251+
if ($actualSite === null && ($origin = $this->getOrigin())) { // fallback for Safari < 16.4
252+
$actualSite = strcasecmp($origin->getScheme(), $this->url->getScheme()) === 0
253+
&& strcasecmp(rtrim($origin->getHost(), '.'), rtrim($this->url->getHost(), '.')) === 0
254+
&& $origin->getPort() === $this->url->getPort()
255+
? 'same-origin'
256+
: 'cross-site';
257+
}
258+
259+
return ($site === null || ($actualSite !== null && in_array($actualSite, (array) $site, strict: true)))
260+
&& ($initiator === null || ($actualDest !== null && in_array($actualDest, (array) $initiator, strict: true)));
261+
}
262+
263+
241264
/**
242265
* Is it an AJAX request?
243266
*/

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)