Skip to content

Commit d3e151c

Browse files
[12.x] redirect response enforce same origin (#57533)
* add `enforceSameOrigin()` method to `RedirectResponse` it can be dangerous to redirect to a cross origin target. this is something that could possibly be exploited when using something like the "referer" header to generate your target. this method force the target URL to match the origin (scheme, host, and port) of the current request URL. you must provide a fallback (ideally an absolute URL) if the check fails. optionally, you may disable scheme and/or port validation. you may not disable hostname validation, because honestly then what's the point? ideally I would be able to use the `$this->request` property to directly access the scheme, host, and port, but because of how the `Request` object handles standard and non-standard ports differently than the `Uri` class, it's more reliable to turn them both into `Uri`s. * add additional test ports are handled weird... * minor formatting * formatting --------- Co-authored-by: Taylor Otwell <[email protected]>
1 parent c958008 commit d3e151c

File tree

2 files changed

+84
-0
lines changed

2 files changed

+84
-0
lines changed

src/Illuminate/Http/RedirectResponse.php

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
use Illuminate\Support\Str;
99
use Illuminate\Support\Traits\ForwardsCalls;
1010
use Illuminate\Support\Traits\Macroable;
11+
use Illuminate\Support\Uri;
1112
use Illuminate\Support\ViewErrorBag;
1213
use Symfony\Component\HttpFoundation\File\UploadedFile as SymfonyUploadedFile;
1314
use Symfony\Component\HttpFoundation\RedirectResponse as BaseRedirectResponse;
@@ -182,6 +183,26 @@ public function withoutFragment()
182183
return $this->setTargetUrl(Str::before($this->getTargetUrl(), '#'));
183184
}
184185

186+
/**
187+
* Enforce that the redirect target must have the same host as the current request.
188+
*/
189+
public function enforceSameOrigin(
190+
string $fallback,
191+
bool $validateScheme = true,
192+
bool $validatePort = true,
193+
): static {
194+
$target = Uri::of($this->targetUrl);
195+
$current = Uri::of($this->request->getSchemeAndHttpHost());
196+
197+
if ($target->host() !== $current->host() ||
198+
($validateScheme && $target->scheme() !== $current->scheme()) ||
199+
($validatePort && $target->port() !== $current->port())) {
200+
$this->setTargetUrl($fallback);
201+
}
202+
203+
return $this;
204+
}
205+
185206
/**
186207
* Get the original response content.
187208
*

tests/Http/HttpRedirectResponseTest.php

Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -117,6 +117,69 @@ public function testFlashingErrorsOnRedirect()
117117
$response->withErrors($provider);
118118
}
119119

120+
public function testCanEnforceSameOriginWhenSameOrigin()
121+
{
122+
$response = new RedirectResponse('https://example.com/foo/bar');
123+
$response->setRequest(Request::create('https://example.com/baz/buzz'));
124+
$response->enforceSameOrigin('fallback');
125+
126+
$this->assertSame('https://example.com/foo/bar', $response->getTargetUrl());
127+
}
128+
129+
public function testCanEnforceSameOriginWhenSameOriginAndCustomPort()
130+
{
131+
$response = new RedirectResponse('https://example.com:1/foo/bar');
132+
$response->setRequest(Request::create('https://example.com:1/baz/buzz'));
133+
$response->enforceSameOrigin('fallback');
134+
135+
$this->assertSame('https://example.com:1/foo/bar', $response->getTargetUrl());
136+
}
137+
138+
public function testCanEnforceSameOriginWhenNotSameScheme()
139+
{
140+
$response = new RedirectResponse('https://example.com/foo/bar');
141+
$response->setRequest(Request::create('http://example.com/baz/buzz'));
142+
$response->enforceSameOrigin('fallback');
143+
144+
$this->assertSame('fallback', $response->getTargetUrl());
145+
}
146+
147+
public function testCanEnforceSameOriginWhenNotSameHostname()
148+
{
149+
$response = new RedirectResponse('https://example.com/foo/bar');
150+
$response->setRequest(Request::create('https://example2.com/baz/buzz'));
151+
$response->enforceSameOrigin('fallback');
152+
153+
$this->assertSame('fallback', $response->getTargetUrl());
154+
}
155+
156+
public function testCanEnforceSameOriginWhenNotSamePort()
157+
{
158+
$response = new RedirectResponse('https://example.com:1/foo/bar');
159+
$response->setRequest(Request::create('https://example.com:2/baz/buzz'));
160+
$response->enforceSameOrigin('fallback');
161+
162+
$this->assertSame('fallback', $response->getTargetUrl());
163+
}
164+
165+
public function testCanEnforceSameOriginWhenNotSameSchemeAndSchemeValidationIsDisabled()
166+
{
167+
$response = new RedirectResponse('https://example.com/foo/bar');
168+
$response->setRequest(Request::create('http://example.com/baz/buzz'));
169+
$response->enforceSameOrigin('fallback', validateScheme: false);
170+
171+
$this->assertSame('https://example.com/foo/bar', $response->getTargetUrl());
172+
}
173+
174+
public function testCanEnforceSameOriginWhenNotSamePortAndPortValidationIsDisabled()
175+
{
176+
$response = new RedirectResponse('https://example.com:1/foo/bar');
177+
$response->setRequest(Request::create('https://example.com:2/baz/buzz'));
178+
$response->enforceSameOrigin('fallback', validatePort: false);
179+
180+
$this->assertSame('https://example.com:1/foo/bar', $response->getTargetUrl());
181+
}
182+
120183
public function testSettersGettersOnRequest()
121184
{
122185
$response = new RedirectResponse('foo.bar');

0 commit comments

Comments
 (0)