Skip to content

Instantly share code, notes, and snippets.

@mrclay
Created March 18, 2013 17:34
Show Gist options
  • Save mrclay/5189118 to your computer and use it in GitHub Desktop.
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)
<?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