Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
71 changes: 68 additions & 3 deletions src/Illuminate/Routing/UrlGenerator.php
Original file line number Diff line number Diff line change
Expand Up @@ -175,16 +175,81 @@ public function previous($fallback = false)
}

/**
* Get the previous path info for the request.
* Get the previous path for the request.
*
* @param mixed $fallback
* @return string
*/
public function previousPath($fallback = false)
{
$previousPath = str_replace($this->to('/'), '', rtrim(preg_replace('/\?.*/', '', $this->previous($fallback)), '/'));
$referrer = $this->request->headers->get('referer');

if (! $referrer) {
$referrer = $this->getPreviousUrlFromSession();
}

// checking the referrer against dangerous schemes (e.g., javascript:, data:, file: etc)
// to prevent open redirect vulnerabilities
if (! $referrer || $this->isDangerousUrl($referrer)) {
$path = $fallback ? parse_url($this->to($fallback), PHP_URL_PATH) ?? '/' : '/';

return rtrim($path, '/') ?: '/';
}

$previous = $this->to($referrer);

$previousUrlComponents = parse_url($previous);
$appUrlComponents = parse_url($this->to('/'));

// if the previous URL is not from the same origin, we will use the fallback or root URL
if (! $this->isSameOrigin($previousUrlComponents, $appUrlComponents)) {
$previous = $fallback ? $this->to($fallback) : $this->to('/');
}

$path = parse_url($previous, PHP_URL_PATH) ?? '/';

return rtrim($path, '/') ?: '/';
}

/**
* Check for dangerous schemes like javascript, data, file, or vbscript.
*
* @param string $url
* @return bool
*/
protected function isDangerousUrl($url)
{
// will return true if the URL starts with javascript, data, file, or vbscript
return preg_match('/^(javascript|data|file|vbscript):/i', $url);
}

/**
* Determine if two URLs are from the same origin, matching host, scheme, and port.
*
* @param array|false $urlOneComponents
* @param array|false $urlTwoComponents
* @return bool
*/
protected function isSameOrigin($urlOneComponents, $urlTwoComponents)
{
if ($urlOneComponents === false || $urlTwoComponents === false) {
return false;
}

$hostOne = $urlOneComponents['host'] ?? null;
$hostTwo = $urlTwoComponents['host'] ?? null;
$schemeOne = $urlOneComponents['scheme'] ?? null;
$schemeTwo = $urlTwoComponents['scheme'] ?? null;
$portOne = $urlOneComponents['port'] ?? null;
$portTwo = $urlTwoComponents['port'] ?? null;

if (! $hostOne || ! $hostTwo || ! $schemeOne || ! $schemeTwo) {
return false;
}

return $previousPath === '' ? '/' : $previousPath;
return strtolower($hostOne) === strtolower($hostTwo) &&
$schemeOne === $schemeTwo &&
$portOne === $portTwo;
}

/**
Expand Down
249 changes: 249 additions & 0 deletions tests/Routing/RoutingUrlGeneratorPreviousPathTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,249 @@
<?php

namespace Illuminate\Tests\Routing;

use Illuminate\Http\Request;
use Illuminate\Routing\RouteCollection;
use Illuminate\Routing\UrlGenerator;
use PHPUnit\Framework\TestCase;

class RoutingUrlGeneratorPreviousPathTest extends TestCase
{
protected function getUrlGenerator($referer = null, $host = 'www.foo.com', $scheme = 'http')
{
$routes = new RouteCollection;

$request = Request::create("{$scheme}://{$host}/");

if ($referer) {
$request->headers->set('referer', $referer);
}

return new UrlGenerator($routes, $request);
}

public function testPreviousPathWithSameDomainUrl()
{
$url = $this->getUrlGenerator('http://www.foo.com/bar/baz?query=value');

$this->assertSame('/bar/baz', $url->previousPath());
}

public function testPreviousPathWithSameDomainRootUrl()
{
$url = $this->getUrlGenerator('http://www.foo.com/');

$this->assertSame('/', $url->previousPath());
}

public function testPreviousPathWithSameDomainUrlAndQueryString()
{
$url = $this->getUrlGenerator('http://www.foo.com/products/123?category=electronics&sort=price');

$this->assertSame('/products/123', $url->previousPath());
}

public function testPreviousPathWithSameDomainUrlAndFragment()
{
$url = $this->getUrlGenerator('http://www.foo.com/docs/api#authentication');

$this->assertSame('/docs/api', $url->previousPath());
}

public function testPreviousPathBlocksExternalDomain()
{
$url = $this->getUrlGenerator('https://evil-site.com/malicious/path');

$this->assertSame('/', $url->previousPath());
}

public function testPreviousPathBlocksExternalDomainWithAnyPaths()
{
$url = $this->getUrlGenerator('https://attacker.com/admin/users');

$this->assertSame('/', $url->previousPath());
}

public function testPreviousPathBlocksDifferentScheme()
{
$url = $this->getUrlGenerator('https://www.foo.com/secure/area', 'www.foo.com', 'http');

$this->assertSame('/', $url->previousPath());
}

public function testPreviousPathAllowsSameSchemeWithPaths()
{
$url = $this->getUrlGenerator('https://www.foo.com/secures/area', 'www.foo.com', 'https');

$this->assertSame('/secures/area', $url->previousPath());
}

public function testPreviousPathBlocksSubdomainAttack()
{
$url = $this->getUrlGenerator('http://sub.foo.com/malicious');

$this->assertSame('/', $url->previousPath());
}

public function testPreviousPathBlocksDifferentPortBasedAttack()
{
$url = $this->getUrlGenerator('http://www.foo.com:8080/admin', 'www.foo.com', 'http');

$this->assertSame('/', $url->previousPath());
}

public function testPreviousPathAllowsSameDomainWithSamePortUrl()
{
$url = $this->getUrlGenerator('http://www.foo.com:8080/allowed', 'www.foo.com:8080', 'http');

$this->assertSame('/allowed', $url->previousPath());
}

public function testPreviousPathHandlesEmptyReferer()
{
$url = $this->getUrlGenerator('');

$this->assertSame('/', $url->previousPath());
}

public function testPreviousPathHandlesNullReferer()
{
$url = $this->getUrlGenerator(null);

$this->assertSame('/', $url->previousPath());
}

public function testPreviousPathWithFallbackForExternalUrl()
{
$url = $this->getUrlGenerator('https://evil.com/malicious');

$this->assertSame('/dashboard', $url->previousPath('/dashboard'));
}

public function testPreviousPathNormalizesTrailingSlashes()
{
$url = $this->getUrlGenerator('http://www.foo.com/path/to/resource/');

$this->assertSame('/path/to/resource', $url->previousPath());
}

public function testPreviousPathHandlesDeepNestedPaths()
{
$url = $this->getUrlGenerator('http://www.foo.com/level1/level2/level3/level4/list');

$this->assertSame('/level1/level2/level3/level4/list', $url->previousPath());
}

public function testPreviousPathHandlesSpecialCharactersInPath()
{
$url = $this->getUrlGenerator('http://www.foo.com/path-with-dashes/under_scores/123');

$this->assertSame('/path-with-dashes/under_scores/123', $url->previousPath());
}

public function testPreviousPathBlocksDataUri()
{
$url = $this->getUrlGenerator('data:text/html,<script>alert("xss")</script>');

$this->assertSame('/', $url->previousPath());
}

public function testPreviousPathBlocksJavascriptUri()
{
$url = $this->getUrlGenerator('javascript:alert("xss")');

$this->assertSame('/', $url->previousPath());
}

public function testPreviousPathAllowsJavascriptUriIfOriginIsSame()
{
$url = $this->getUrlGenerator('http://www.foo.com/javascript:alert("same-origin")', 'www.foo.com');

$this->assertSame('/javascript:alert("same-origin")', $url->previousPath());
}

public function testPreviousPathBlocksFileUri()
{
$url = $this->getUrlGenerator('file:///etc/passwd');

$this->assertSame('/', $url->previousPath());
}

public function testPreviousPathAllowsFileUriIfOriginIsSame()
{
$url = $this->getUrlGenerator('http://www.foo.com/file:///etc/same');

$this->assertSame('/file:///etc/same', $url->previousPath());
}

public function testPreviousPathHandlesCaseInsensitiveHost()
{
$url = $this->getUrlGenerator('http://WWW.FOO.COM/path', 'www.foo.com');

$this->assertSame('/path', $url->previousPath());
}

public function testPreviousPathHandlesCaseVariations()
{
$url = $this->getUrlGenerator('http://www.FOO.com/path', 'www.foo.com');

$this->assertSame('/path', $url->previousPath());
}

public function testPreviousPathHandlesUrlWithoutPath()
{
$url = $this->getUrlGenerator('http://www.foo.com');

$this->assertSame('/', $url->previousPath());
}

public function testPreviousPathHandlesComplexQueryString()
{
$url = $this->getUrlGenerator('http://www.foo.com/search?q=php&framework=laravel&sort=popularity&page=34');

$this->assertSame('/search', $url->previousPath());
}

public function testPreviousPathHandlesEncodedCharacters()
{
$url = $this->getUrlGenerator('http://www.foo.com/path%20with%20spaces%20encoded/resource');

$this->assertSame('/path%20with%20spaces%20encoded/resource', $url->previousPath());
}

public function testIsSameOriginMethod()
{
$url = $this->getUrlGenerator();

$reflection = new \ReflectionClass($url);
$method = $reflection->getMethod('isSameOrigin');
$method->setAccessible(true);

$this->assertTrue($method->invoke($url,
['host' => 'www.foo.com', 'scheme' => 'http'],
['host' => 'www.foo.com', 'scheme' => 'http']
));

// different host
$this->assertFalse($method->invoke($url,
['host' => 'evil.com', 'scheme' => 'http'],
['host' => 'www.foo.com', 'scheme' => 'http']
));

// different scheme
$this->assertFalse($method->invoke($url,
['host' => 'www.foo.com', 'scheme' => 'https'],
['host' => 'www.foo.com', 'scheme' => 'http']
));

// missing component
$this->assertFalse($method->invoke($url, false, ['host' => 'www.foo.com', 'scheme' => 'http']));
$this->assertFalse($method->invoke($url, ['host' => 'www.foo.com'], ['host' => 'www.foo.com', 'scheme' => 'http']));

// case insensitive
$this->assertTrue($method->invoke($url,
['host' => 'WWW.FOO.COM', 'scheme' => 'http'],
['host' => 'www.foo.com', 'scheme' => 'http']
));
}
}