Skip to content

Commit 3dcd40c

Browse files
committed
added Request::isFrom() WIP
1 parent fa77ea6 commit 3dcd40c

3 files changed

Lines changed: 123 additions & 1 deletion

File tree

src/Http/IRequest.php

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@
1414
* HTTP request provides access scheme for request sent via HTTP.
1515
* @method ?UrlImmutable getReferer() Returns referrer.
1616
* @method bool isSameSite() Is the request sent from the same origin?
17+
* @method bool isFrom(string|list<string>|null $site = null, string|list<string>|null $initiator = null)
1718
*/
1819
interface IRequest
1920
{

src/Http/Request.php

Lines changed: 24 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@
1010
namespace Nette\Http;
1111

1212
use Nette;
13-
use function array_change_key_case, base64_decode, count, explode, func_num_args, gethostbyaddr, implode, preg_match, preg_match_all, rsort, strcasecmp, strtolower, strtr;
13+
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;
1414
use const CASE_LOWER;
1515

1616

@@ -240,6 +240,29 @@ public function isSameSite(): bool
240240
}
241241

242242

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

tests/Http/Request.isFrom.phpt

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

0 commit comments

Comments
 (0)