Created
March 18, 2013 17:34
-
-
Save mrclay/5189118 to your computer and use it in GitHub Desktop.
Validate a user-given URL for use in a Location HTTP header (to avoid be abused as an open redirect)
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<?php | |
namespace UFCOE; | |
/** | |
* Validate a Location HTTP header value. By default this allows | |
* most URLs that are in the same origin, but allows switching between HTTP/HTTPS. | |
* | |
* @todo tests, dummy! | |
*/ | |
class LocationValidator { | |
const PATTERN_DELIMITER = '~'; | |
/** | |
* @var bool Allow any URL that begins with "/"? | |
*/ | |
public $allowRootRelative = true; | |
/** | |
* @var bool Allow URLs like "page2"? | |
*/ | |
public $allowPathRelative = true; | |
/** | |
* @var string this should be "http", "https", or "https?" | |
*/ | |
public $schemePattern = 'https?'; | |
/** | |
* @var string this will be set by the constructor to match only the current host. Alter the pattern if you wish. | |
*/ | |
public $hostPattern; | |
/** | |
* @var array ports that can appear after hostname. e.g. [80, 8080, 8081] | |
*/ | |
public $allowedExplicitPorts = array(); | |
/** | |
* @param string $hostname | |
*/ | |
public function __construct($hostname = '') | |
{ | |
if (!$hostname) { | |
$hostname = $this->getCurrentHostname(); | |
} | |
$this->hostPattern = preg_quote($hostname, self::PATTERN_DELIMITER); | |
} | |
/** | |
* @param string $location a URL to be placed in a Location header | |
* @return bool | |
*/ | |
public function isValid($location) | |
{ | |
if ($location === '') { | |
return false; | |
} | |
// root-relative URI | |
if ($location[0] === '/') { | |
return $this->allowRootRelative; | |
} | |
// relative URIs starting with alphanum: It's important that the beginning cannot be mistaken | |
// for a protocol identifier. E.g. Some browsers interpret junk like "jav\nas\ncript:". We make | |
// sure any leading alphanums are followed by "/", ".", "-", "_", "?", or the end of the string. | |
if (preg_match('~^[a-z0-9]+([/\\.\\-_\\?]|$)~i', $location)) { | |
return $this->allowPathRelative; | |
} | |
return (bool) preg_match($this->getFullUrlPattern(), $location); | |
} | |
protected function getCurrentHostname() | |
{ | |
if (!empty($_SERVER['HTTP_HOST'])) { | |
return $_SERVER['HTTP_HOST']; | |
} | |
if (empty($_SERVER['SERVER_NAME'])) { | |
throw new \InvalidArgumentException('If $_SERVER is not populated, $hostname is required.'); | |
} | |
return $_SERVER['SERVER_NAME']; | |
} | |
protected function getFullUrlPattern() | |
{ | |
$portPattern = ''; | |
if ($this->allowedExplicitPorts) { | |
$portPattern = '(\\:(' . implode('|', $this->allowedExplicitPorts) . '))?'; | |
} | |
$pattern = "~^{$this->schemePattern}\\://{$this->hostPattern}{$portPattern}(/|$)~"; | |
return $pattern; | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment