diff --git a/src/applications/config/option/PhabricatorSecurityConfigOptions.php b/src/applications/config/option/PhabricatorSecurityConfigOptions.php index dde1d0d7be..4d4da4bd02 100644 --- a/src/applications/config/option/PhabricatorSecurityConfigOptions.php +++ b/src/applications/config/option/PhabricatorSecurityConfigOptions.php @@ -1,340 +1,355 @@ deformat(pht(<<newOption('security.alternate-file-domain', 'string', null) ->setLocked(true) ->setSummary(pht('Alternate domain to serve files from.')) ->setDescription( pht( 'By default, Phabricator serves files from the same domain '. 'the application is served from. This is convenient, but '. 'presents a security risk.'. "\n\n". 'You should configure a CDN or alternate file domain to mitigate '. 'this risk. Configuring a CDN will also improve performance. See '. '[[ %s | %s ]] for instructions.', $doc_href, $doc_name)) ->addExample('https://files.phabcdn.net/', pht('Valid Setting')), $this->newOption( 'security.hmac-key', 'string', '[D\t~Y7eNmnQGJ;rnH6aF;m2!vJ8@v8C=Cs:aQS\.Qw') ->setHidden(true) ->setSummary( pht('Key for HMAC digests.')) ->setDescription( pht( 'Default key for HMAC digests where the key is not important '. '(i.e., the hash itself is secret). You can change this if you '. 'want (to any other string), but doing so will break existing '. 'sessions and CSRF tokens.')), $this->newOption('security.require-https', 'bool', false) ->setLocked(true) ->setSummary( pht('Force users to connect via HTTPS instead of HTTP.')) ->setDescription( pht( "If the web server responds to both HTTP and HTTPS requests but ". "you want users to connect with only HTTPS, you can set this ". "to `true` to make Phabricator redirect HTTP requests to HTTPS.". "\n\n". "Normally, you should just configure your server not to accept ". "HTTP traffic, but this setting may be useful if you originally ". "used HTTP and have now switched to HTTPS but don't want to ". "break old links, or if your webserver sits behind a load ". "balancer which terminates HTTPS connections and you can not ". "reasonably configure more granular behavior there.". "\n\n". "IMPORTANT: Phabricator determines if a request is HTTPS or not ". "by examining the PHP `%s` variable. If you run ". "Apache/mod_php this will probably be set correctly for you ". "automatically, but if you run Phabricator as CGI/FCGI (e.g., ". "through nginx or lighttpd), you need to configure your web ". "server so that it passes the value correctly based on the ". "connection type.". "\n\n". "If you configure Phabricator in cluster mode, note that this ". "setting is ignored by intracluster requests.", "\$_SERVER['HTTPS']")) ->setBoolOptions( array( pht('Force HTTPS'), pht('Allow HTTP'), )), $this->newOption('security.require-multi-factor-auth', 'bool', false) ->setLocked(true) ->setSummary( pht('Require all users to configure multi-factor authentication.')) ->setDescription( pht( 'By default, Phabricator allows users to add multi-factor '. 'authentication to their accounts, but does not require it. '. 'By enabling this option, you can force all users to add '. 'at least one authentication factor before they can use their '. 'accounts.')) ->setBoolOptions( array( pht('Multi-Factor Required'), pht('Multi-Factor Optional'), )), $this->newOption( 'phabricator.csrf-key', 'string', '0b7ec0592e0a2829d8b71df2fa269b2c6172eca3') ->setHidden(true) ->setSummary( pht('Hashed with other inputs to generate CSRF tokens.')) ->setDescription( pht( 'This is hashed with other inputs to generate CSRF tokens. If '. 'you want, you can change it to some other string which is '. 'unique to your install. This will make your install more secure '. 'in a vague, mostly theoretical way. But it will take you like 3 '. 'seconds of mashing on your keyboard to set it up so you might '. 'as well.')), $this->newOption( 'phabricator.mail-key', 'string', '5ce3e7e8787f6e40dfae861da315a5cdf1018f12') ->setHidden(true) ->setSummary( pht('Hashed with other inputs to generate mail tokens.')) ->setDescription( pht( "This is hashed with other inputs to generate mail tokens. If ". "you want, you can change it to some other string which is ". "unique to your install. In particular, you will want to do ". "this if you accidentally send a bunch of mail somewhere you ". "shouldn't have, to invalidate all old reply-to addresses.")), $this->newOption( 'uri.allowed-protocols', 'set', array( 'http' => true, 'https' => true, 'mailto' => true, )) ->setSummary( pht('Determines which URI protocols are auto-linked.')) ->setDescription( pht( "When users write comments which have URIs, they'll be ". "automatically linked if the protocol appears in this set. This ". "whitelist is primarily to prevent security issues like ". "%s URIs.", 'javascript://')) ->addExample("http\nhttps", pht('Valid Setting')) ->setLocked(true), $this->newOption( 'uri.allowed-editor-protocols', 'set', array( 'http' => true, 'https' => true, // This handler is installed by Textmate. 'txmt' => true, // This handler is for MacVim. 'mvim' => true, // Unofficial handler for Vim. 'vim' => true, // Unofficial handler for Sublime. 'subl' => true, // Unofficial handler for Emacs. 'emacs' => true, // This isn't a standard handler installed by an application, but // is a reasonable name for a user-installed handler. 'editor' => true, )) ->setSummary(pht('Whitelists editor protocols for "Open in Editor".')) ->setDescription( pht( 'Users can configure a URI pattern to open files in a text '. 'editor. The URI must use a protocol on this whitelist.')) ->setLocked(true), $this->newOption( 'celerity.resource-hash', 'string', 'd9455ea150622ee044f7931dabfa52aa') ->setSummary( pht('An input to the hash function when building resource hashes.')) ->setDescription( pht( 'This value is an input to the hash function when building '. 'resource hashes. It has no security value, but if you '. 'accidentally poison user caches (by pushing a bad patch or '. 'having something go wrong with a CDN, e.g.) you can change this '. 'to something else and rebuild the Celerity map to break user '. 'caches. Unless you are doing Celerity development, it is '. 'exceptionally unlikely that you need to modify this.')), $this->newOption('remarkup.enable-embedded-youtube', 'bool', false) ->setBoolOptions( array( pht('Embed YouTube videos'), pht("Don't embed YouTube videos"), )) ->setSummary( pht('Determines whether or not YouTube videos get embedded.')) ->setDescription( pht( "If you enable this, linked YouTube videos will be embedded ". "inline. This has mild security implications (you'll leak ". "referrers to YouTube) and is pretty silly (but sort of ". "awesome).")), $this->newOption( 'security.outbound-blacklist', 'list', $default_address_blacklist) ->setLocked(true) ->setSummary( pht( 'Blacklist subnets to prevent user-initiated outbound '. 'requests.')) ->setDescription( pht( 'Phabricator users can make requests to other services from '. 'the Phabricator host in some circumstances (for example, by '. 'creating a repository with a remote URL or having Phabricator '. 'fetch an image from a remote server).'. "\n\n". 'This may represent a security vulnerability if services on '. 'the same subnet will accept commands or reveal private '. 'information over unauthenticated HTTP GET, based on the source '. 'IP address. In particular, all hosts in EC2 have access to '. 'such a service.'. "\n\n". 'This option defines a list of netblocks which Phabricator '. 'will decline to connect to. Generally, you should list all '. 'private IP space here.')) ->addExample(array('0.0.0.0/0'), pht('No Outbound Requests')), $this->newOption('security.strict-transport-security', 'bool', false) ->setLocked(true) ->setBoolOptions( array( pht('Use HSTS'), pht('Do Not Use HSTS'), )) ->setSummary(pht('Enable HTTP Strict Transport Security (HSTS).')) ->setDescription( pht( 'HTTP Strict Transport Security (HSTS) sends a header which '. 'instructs browsers that the site should only be accessed '. 'over HTTPS, never HTTP. This defuses an attack where an '. 'adversary gains access to your network, then proxies requests '. 'through an unsecured link.'. "\n\n". 'Do not enable this option if you serve (or plan to ever serve) '. 'unsecured content over plain HTTP. It is very difficult to '. 'undo this change once users\' browsers have accepted the '. 'setting.')), $this->newOption('keyring', $keyring_type, array()) ->setHidden(true) ->setSummary(pht('Configure master encryption keys.')) ->setDescription($keyring_description), ); } protected function didValidateOption( PhabricatorConfigOption $option, $value) { $key = $option->getKey(); if ($key == 'security.alternate-file-domain') { $uri = new PhutilURI($value); $protocol = $uri->getProtocol(); if ($protocol !== 'http' && $protocol !== 'https') { throw new PhabricatorConfigValidationException( pht( "Config option '%s' is invalid. The URI must start with ". "'%s' or '%s'.", $key, 'http://', 'https://')); } $domain = $uri->getDomain(); if (strpos($domain, '.') === false) { throw new PhabricatorConfigValidationException( pht( "Config option '%s' is invalid. The URI must contain a dot ('.'), ". "like '%s', not just a bare name like '%s'. ". "Some web browsers will not set cookies on domains with no TLD.", $key, 'http://example.com/', 'http://example/')); } $path = $uri->getPath(); if ($path !== '' && $path !== '/') { throw new PhabricatorConfigValidationException( pht( "Config option '%s' is invalid. The URI must NOT have a path, ". "e.g. '%s' is OK, but '%s' is not. Phabricator must be installed ". "on an entire domain; it can not be installed on a path.", $key, 'http://phabricator.example.com/', 'http://example.com/phabricator/')); } } } } diff --git a/src/infrastructure/env/PhabricatorEnv.php b/src/infrastructure/env/PhabricatorEnv.php index ae7256a490..15873a7f9c 100644 --- a/src/infrastructure/env/PhabricatorEnv.php +++ b/src/infrastructure/env/PhabricatorEnv.php @@ -1,927 +1,927 @@ overrideEnv('some.key', 'new-value-for-this-test'); * * // Some test which depends on the value of 'some.key'. * * } * * Your changes will persist until the `$env` object leaves scope or is * destroyed. * * You should //not// use this in normal code. * * * @task read Reading Configuration * @task uri URI Validation * @task test Unit Test Support * @task internal Internals */ final class PhabricatorEnv extends Phobject { private static $sourceStack; private static $repairSource; private static $overrideSource; private static $requestBaseURI; private static $cache; private static $localeCode; private static $readOnly; private static $readOnlyReason; const READONLY_CONFIG = 'config'; const READONLY_UNREACHABLE = 'unreachable'; const READONLY_SEVERED = 'severed'; const READONLY_MASTERLESS = 'masterless'; /** * @phutil-external-symbol class PhabricatorStartup */ public static function initializeWebEnvironment() { self::initializeCommonEnvironment(false); } public static function initializeScriptEnvironment($config_optional) { self::initializeCommonEnvironment($config_optional); // NOTE: This is dangerous in general, but we know we're in a script context // and are not vulnerable to CSRF. AphrontWriteGuard::allowDangerousUnguardedWrites(true); // There are several places where we log information (about errors, events, // service calls, etc.) for analysis via DarkConsole or similar. These are // useful for web requests, but grow unboundedly in long-running scripts and // daemons. Discard data as it arrives in these cases. PhutilServiceProfiler::getInstance()->enableDiscardMode(); DarkConsoleErrorLogPluginAPI::enableDiscardMode(); DarkConsoleEventPluginAPI::enableDiscardMode(); } private static function initializeCommonEnvironment($config_optional) { PhutilErrorHandler::initialize(); self::resetUmask(); self::buildConfigurationSourceStack($config_optional); // Force a valid timezone. If both PHP and Phabricator configuration are // invalid, use UTC. $tz = self::getEnvConfig('phabricator.timezone'); if ($tz) { @date_default_timezone_set($tz); } $ok = @date_default_timezone_set(date_default_timezone_get()); if (!$ok) { date_default_timezone_set('UTC'); } // Prepend '/support/bin' and append any paths to $PATH if we need to. $env_path = getenv('PATH'); $phabricator_path = dirname(phutil_get_library_root('phabricator')); $support_path = $phabricator_path.'/support/bin'; $env_path = $support_path.PATH_SEPARATOR.$env_path; $append_dirs = self::getEnvConfig('environment.append-paths'); if (!empty($append_dirs)) { $append_path = implode(PATH_SEPARATOR, $append_dirs); $env_path = $env_path.PATH_SEPARATOR.$append_path; } putenv('PATH='.$env_path); // Write this back into $_ENV, too, so ExecFuture picks it up when creating // subprocess environments. $_ENV['PATH'] = $env_path; // If an instance identifier is defined, write it into the environment so // it's available to subprocesses. $instance = self::getEnvConfig('cluster.instance'); if (strlen($instance)) { putenv('PHABRICATOR_INSTANCE='.$instance); $_ENV['PHABRICATOR_INSTANCE'] = $instance; } PhabricatorEventEngine::initialize(); // TODO: Add a "locale.default" config option once we have some reasonable // defaults which aren't silly nonsense. self::setLocaleCode('en_US'); } public static function beginScopedLocale($locale_code) { return new PhabricatorLocaleScopeGuard($locale_code); } public static function getLocaleCode() { return self::$localeCode; } public static function setLocaleCode($locale_code) { if (!$locale_code) { return; } if ($locale_code == self::$localeCode) { return; } try { $locale = PhutilLocale::loadLocale($locale_code); $translations = PhutilTranslation::getTranslationMapForLocale( $locale_code); $override = self::getEnvConfig('translation.override'); if (!is_array($override)) { $override = array(); } PhutilTranslator::getInstance() ->setLocale($locale) ->setTranslations($override + $translations); self::$localeCode = $locale_code; } catch (Exception $ex) { // Just ignore this; the user likely has an out-of-date locale code. } } private static function buildConfigurationSourceStack($config_optional) { self::dropConfigCache(); $stack = new PhabricatorConfigStackSource(); self::$sourceStack = $stack; $default_source = id(new PhabricatorConfigDefaultSource()) ->setName(pht('Global Default')); $stack->pushSource($default_source); $env = self::getSelectedEnvironmentName(); if ($env) { $stack->pushSource( id(new PhabricatorConfigFileSource($env)) ->setName(pht("File '%s'", $env))); } $stack->pushSource( id(new PhabricatorConfigLocalSource()) ->setName(pht('Local Config'))); // If the install overrides the database adapter, we might need to load // the database adapter class before we can push on the database config. // This config is locked and can't be edited from the web UI anyway. foreach (self::getEnvConfig('load-libraries') as $library) { phutil_load_library($library); } // Drop any class map caches, since they will have generated without // any classes from libraries. Without this, preflight setup checks can // cause generation of a setup check cache that omits checks defined in // libraries, for example. PhutilClassMapQuery::deleteCaches(); // If custom libraries specify config options, they won't get default // values as the Default source has already been loaded, so we get it to // pull in all options from non-phabricator libraries now they are loaded. $default_source->loadExternalOptions(); // If this install has site config sources, load them now. $site_sources = id(new PhutilClassMapQuery()) ->setAncestorClass('PhabricatorConfigSiteSource') ->setSortMethod('getPriority') ->execute(); foreach ($site_sources as $site_source) { $stack->pushSource($site_source); } $masters = PhabricatorDatabaseRef::getMasterDatabaseRefs(); if (!$masters) { self::setReadOnly(true, self::READONLY_MASTERLESS); } else { // If any master is severed, we drop to readonly mode. In theory we // could try to continue if we're only missing some applications, but // this is very complex and we're unlikely to get it right. foreach ($masters as $master) { // Give severed masters one last chance to get healthy. if ($master->isSevered()) { $master->checkHealth(); } if ($master->isSevered()) { self::setReadOnly(true, self::READONLY_SEVERED); break; } } } try { $stack->pushSource( id(new PhabricatorConfigDatabaseSource('default')) ->setName(pht('Database'))); } catch (AphrontSchemaQueryException $exception) { // If the database is not available, just skip this configuration // source. This happens during `bin/storage upgrade`, `bin/conf` before // schema setup, etc. } catch (PhabricatorClusterStrandedException $ex) { // This means we can't connect to any database host. That's fine as // long as we're running a setup script like `bin/storage`. if (!$config_optional) { throw $ex; } } } public static function repairConfig($key, $value) { if (!self::$repairSource) { self::$repairSource = id(new PhabricatorConfigDictionarySource(array())) ->setName(pht('Repaired Config')); self::$sourceStack->pushSource(self::$repairSource); } self::$repairSource->setKeys(array($key => $value)); self::dropConfigCache(); } public static function overrideConfig($key, $value) { if (!self::$overrideSource) { self::$overrideSource = id(new PhabricatorConfigDictionarySource(array())) ->setName(pht('Overridden Config')); self::$sourceStack->pushSource(self::$overrideSource); } self::$overrideSource->setKeys(array($key => $value)); self::dropConfigCache(); } public static function getUnrepairedEnvConfig($key, $default = null) { foreach (self::$sourceStack->getStack() as $source) { if ($source === self::$repairSource) { continue; } $result = $source->getKeys(array($key)); if ($result) { return $result[$key]; } } return $default; } public static function getSelectedEnvironmentName() { $env_var = 'PHABRICATOR_ENV'; $env = idx($_SERVER, $env_var); if (!$env) { $env = getenv($env_var); } if (!$env) { $env = idx($_ENV, $env_var); } if (!$env) { $root = dirname(phutil_get_library_root('phabricator')); $path = $root.'/conf/local/ENVIRONMENT'; if (Filesystem::pathExists($path)) { $env = trim(Filesystem::readFile($path)); } } return $env; } /* -( Reading Configuration )---------------------------------------------- */ /** * Get the current configuration setting for a given key. * * If the key is not found, then throw an Exception. * * @task read */ public static function getEnvConfig($key) { if (!self::$sourceStack) { throw new Exception( pht( 'Trying to read configuration "%s" before configuration has been '. 'initialized.', $key)); } if (isset(self::$cache[$key])) { return self::$cache[$key]; } if (array_key_exists($key, self::$cache)) { return self::$cache[$key]; } $result = self::$sourceStack->getKeys(array($key)); if (array_key_exists($key, $result)) { self::$cache[$key] = $result[$key]; return $result[$key]; } else { throw new Exception( pht( "No config value specified for key '%s'.", $key)); } } /** * Get the current configuration setting for a given key. If the key * does not exist, return a default value instead of throwing. This is * primarily useful for migrations involving keys which are slated for * removal. * * @task read */ public static function getEnvConfigIfExists($key, $default = null) { try { return self::getEnvConfig($key); } catch (Exception $ex) { return $default; } } /** * Get the fully-qualified URI for a path. * * @task read */ public static function getURI($path) { return rtrim(self::getAnyBaseURI(), '/').$path; } /** * Get the fully-qualified production URI for a path. * * @task read */ public static function getProductionURI($path) { // If we're passed a URI which already has a domain, simply return it // unmodified. In particular, files may have URIs which point to a CDN // domain. $uri = new PhutilURI($path); if ($uri->getDomain()) { return $path; } $production_domain = self::getEnvConfig('phabricator.production-uri'); if (!$production_domain) { $production_domain = self::getAnyBaseURI(); } return rtrim($production_domain, '/').$path; } public static function getAllowedURIs($path) { $uri = new PhutilURI($path); if ($uri->getDomain()) { return $path; } $allowed_uris = self::getEnvConfig('phabricator.allowed-uris'); $return = array(); foreach ($allowed_uris as $allowed_uri) { $return[] = rtrim($allowed_uri, '/').$path; } return $return; } /** * Get the fully-qualified production URI for a static resource path. * * @task read */ public static function getCDNURI($path) { $alt = self::getEnvConfig('security.alternate-file-domain'); if (!$alt) { $alt = self::getAnyBaseURI(); } $uri = new PhutilURI($alt); $uri->setPath($path); return (string)$uri; } /** * Get the fully-qualified production URI for a documentation resource. * * @task read */ public static function getDoclink($resource, $type = 'article') { $uri = new PhutilURI('https://secure.phabricator.com/diviner/find/'); $uri->setQueryParam('name', $resource); $uri->setQueryParam('type', $type); $uri->setQueryParam('jump', true); return (string)$uri; } /** * Build a concrete object from a configuration key. * * @task read */ public static function newObjectFromConfig($key, $args = array()) { $class = self::getEnvConfig($key); return newv($class, $args); } public static function getAnyBaseURI() { $base_uri = self::getEnvConfig('phabricator.base-uri'); if (!$base_uri) { $base_uri = self::getRequestBaseURI(); } if (!$base_uri) { throw new Exception( pht( "Define '%s' in your configuration to continue.", 'phabricator.base-uri')); } return $base_uri; } public static function getRequestBaseURI() { return self::$requestBaseURI; } public static function setRequestBaseURI($uri) { self::$requestBaseURI = $uri; } public static function isReadOnly() { if (self::$readOnly !== null) { return self::$readOnly; } return self::getEnvConfig('cluster.read-only'); } public static function setReadOnly($read_only, $reason) { self::$readOnly = $read_only; self::$readOnlyReason = $reason; } public static function getReadOnlyMessage() { $reason = self::getReadOnlyReason(); switch ($reason) { case self::READONLY_MASTERLESS: return pht( 'Phabricator is in read-only mode (no writable database '. 'is configured).'); case self::READONLY_UNREACHABLE: return pht( 'Phabricator is in read-only mode (unreachable master).'); case self::READONLY_SEVERED: return pht( 'Phabricator is in read-only mode (major interruption).'); } return pht('Phabricator is in read-only mode.'); } public static function getReadOnlyURI() { return urisprintf( '/readonly/%s/', self::getReadOnlyReason()); } public static function getReadOnlyReason() { if (!self::isReadOnly()) { return null; } if (self::$readOnlyReason !== null) { return self::$readOnlyReason; } return self::READONLY_CONFIG; } /* -( Unit Test Support )-------------------------------------------------- */ /** * @task test */ public static function beginScopedEnv() { return new PhabricatorScopedEnv(self::pushTestEnvironment()); } /** * @task test */ private static function pushTestEnvironment() { self::dropConfigCache(); $source = new PhabricatorConfigDictionarySource(array()); self::$sourceStack->pushSource($source); return spl_object_hash($source); } /** * @task test */ public static function popTestEnvironment($key) { self::dropConfigCache(); $source = self::$sourceStack->popSource(); $stack_key = spl_object_hash($source); if ($stack_key !== $key) { self::$sourceStack->pushSource($source); throw new Exception( pht( 'Scoped environments were destroyed in a different order than they '. 'were initialized.')); } } /* -( URI Validation )----------------------------------------------------- */ /** * Detect if a URI satisfies either @{method:isValidLocalURIForLink} or * @{method:isValidRemoteURIForLink}, i.e. is a page on this server or the * URI of some other resource which has a valid protocol. This rejects * garbage URIs and URIs with protocols which do not appear in the * `uri.allowed-protocols` configuration, notably 'javascript:' URIs. * * NOTE: This method is generally intended to reject URIs which it may be * unsafe to put in an "href" link attribute. * * @param string URI to test. * @return bool True if the URI identifies a web resource. * @task uri */ public static function isValidURIForLink($uri) { return self::isValidLocalURIForLink($uri) || self::isValidRemoteURIForLink($uri); } /** * Detect if a URI identifies some page on this server. * * NOTE: This method is generally intended to reject URIs which it may be * unsafe to issue a "Location:" redirect to. * * @param string URI to test. * @return bool True if the URI identifies a local page. * @task uri */ public static function isValidLocalURIForLink($uri) { $uri = (string)$uri; if (!strlen($uri)) { return false; } if (preg_match('/\s/', $uri)) { // PHP hasn't been vulnerable to header injection attacks for a bunch of // years, but we can safely reject these anyway since they're never valid. return false; } // Chrome (at a minimum) interprets backslashes in Location headers and the // URL bar as forward slashes. This is probably intended to reduce user // error caused by confusion over which key is "forward slash" vs "back // slash". // // However, it means a URI like "/\evil.com" is interpreted like // "//evil.com", which is a protocol relative remote URI. // // Since we currently never generate URIs with backslashes in them, reject // these unconditionally rather than trying to figure out how browsers will // interpret them. if (preg_match('/\\\\/', $uri)) { return false; } // Valid URIs must begin with '/', followed by the end of the string or some // other non-'/' character. This rejects protocol-relative URIs like // "//evil.com/evil_stuff/". return (bool)preg_match('@^/([^/]|$)@', $uri); } /** * Detect if a URI identifies some valid linkable remote resource. * * @param string URI to test. * @return bool True if a URI idenfies a remote resource with an allowed * protocol. * @task uri */ public static function isValidRemoteURIForLink($uri) { try { self::requireValidRemoteURIForLink($uri); return true; } catch (Exception $ex) { return false; } } /** * Detect if a URI identifies a valid linkable remote resource, throwing a * detailed message if it does not. * * A valid linkable remote resource can be safely linked or redirected to. * This is primarily a protocol whitelist check. * * @param string URI to test. * @return void * @task uri */ public static function requireValidRemoteURIForLink($raw_uri) { $uri = new PhutilURI($raw_uri); $proto = $uri->getProtocol(); if (!strlen($proto)) { throw new Exception( pht( 'URI "%s" is not a valid linkable resource. A valid linkable '. 'resource URI must specify a protocol.', $raw_uri)); } $protocols = self::getEnvConfig('uri.allowed-protocols'); if (!isset($protocols[$proto])) { throw new Exception( pht( 'URI "%s" is not a valid linkable resource. A valid linkable '. 'resource URI must use one of these protocols: %s.', $raw_uri, implode(', ', array_keys($protocols)))); } $domain = $uri->getDomain(); if (!strlen($domain)) { throw new Exception( pht( 'URI "%s" is not a valid linkable resource. A valid linkable '. 'resource URI must specify a domain.', $raw_uri)); } } /** * Detect if a URI identifies a valid fetchable remote resource. * * @param string URI to test. * @param list Allowed protocols. * @return bool True if the URI is a valid fetchable remote resource. * @task uri */ public static function isValidRemoteURIForFetch($uri, array $protocols) { try { self::requireValidRemoteURIForFetch($uri, $protocols); return true; } catch (Exception $ex) { return false; } } /** * Detect if a URI identifies a valid fetchable remote resource, throwing * a detailed message if it does not. * * A valid fetchable remote resource can be safely fetched using a request * originating on this server. This is a primarily an address check against * the outbound address blacklist. * * @param string URI to test. * @param list Allowed protocols. * @return pair Pre-resolved URI and domain. * @task uri */ public static function requireValidRemoteURIForFetch( - $uri, + $raw_uri, array $protocols) { - $uri = new PhutilURI($uri); + $uri = new PhutilURI($raw_uri); $proto = $uri->getProtocol(); if (!strlen($proto)) { throw new Exception( pht( 'URI "%s" is not a valid fetchable resource. A valid fetchable '. 'resource URI must specify a protocol.', - $uri)); + $raw_uri)); } $protocols = array_fuse($protocols); if (!isset($protocols[$proto])) { throw new Exception( pht( 'URI "%s" is not a valid fetchable resource. A valid fetchable '. 'resource URI must use one of these protocols: %s.', - $uri, + $raw_uri, implode(', ', array_keys($protocols)))); } $domain = $uri->getDomain(); if (!strlen($domain)) { throw new Exception( pht( 'URI "%s" is not a valid fetchable resource. A valid fetchable '. 'resource URI must specify a domain.', - $uri)); + $raw_uri)); } $addresses = gethostbynamel($domain); if (!$addresses) { throw new Exception( pht( 'URI "%s" is not a valid fetchable resource. The domain "%s" could '. 'not be resolved.', - $uri, + $raw_uri, $domain)); } foreach ($addresses as $address) { if (self::isBlacklistedOutboundAddress($address)) { throw new Exception( pht( 'URI "%s" is not a valid fetchable resource. The domain "%s" '. 'resolves to the address "%s", which is blacklisted for '. 'outbound requests.', - $uri, + $raw_uri, $domain, $address)); } } $resolved_uri = clone $uri; $resolved_uri->setDomain(head($addresses)); return array($resolved_uri, $domain); } /** * Determine if an IP address is in the outbound address blacklist. * * @param string IP address. * @return bool True if the address is blacklisted. */ public static function isBlacklistedOutboundAddress($address) { $blacklist = self::getEnvConfig('security.outbound-blacklist'); return PhutilCIDRList::newList($blacklist)->containsAddress($address); } public static function isClusterRemoteAddress() { $cluster_addresses = self::getEnvConfig('cluster.addresses'); if (!$cluster_addresses) { return false; } $address = idx($_SERVER, 'REMOTE_ADDR'); if (!$address) { throw new Exception( pht( 'Unable to test remote address against cluster whitelist: '. 'REMOTE_ADDR is not defined.')); } return self::isClusterAddress($address); } public static function isClusterAddress($address) { $cluster_addresses = self::getEnvConfig('cluster.addresses'); if (!$cluster_addresses) { throw new Exception( pht( 'Phabricator is not configured to serve cluster requests. '. 'Set `cluster.addresses` in the configuration to whitelist '. 'cluster hosts before sending requests that use a cluster '. 'authentication mechanism.')); } return PhutilCIDRList::newList($cluster_addresses) ->containsAddress($address); } /* -( Internals )---------------------------------------------------------- */ /** * @task internal */ public static function envConfigExists($key) { return array_key_exists($key, self::$sourceStack->getKeys(array($key))); } /** * @task internal */ public static function getAllConfigKeys() { return self::$sourceStack->getAllKeys(); } public static function getConfigSourceStack() { return self::$sourceStack; } /** * @task internal */ public static function overrideTestEnvConfig($stack_key, $key, $value) { $tmp = array(); // If we don't have the right key, we'll throw when popping the last // source off the stack. do { $source = self::$sourceStack->popSource(); array_unshift($tmp, $source); if (spl_object_hash($source) == $stack_key) { $source->setKeys(array($key => $value)); break; } } while (true); foreach ($tmp as $source) { self::$sourceStack->pushSource($source); } self::dropConfigCache(); } private static function dropConfigCache() { self::$cache = array(); } private static function resetUmask() { // Reset the umask to the common standard umask. The umask controls default // permissions when files are created and propagates to subprocesses. // "022" is the most common umask, but sometimes it is set to something // unusual by the calling environment. // Since various things rely on this umask to work properly and we are // not aware of any legitimate reasons to adjust it, unconditionally // normalize it until such reasons arise. See T7475 for discussion. umask(022); } /** * Get the path to an empty directory which is readable by all of the system * user accounts that Phabricator acts as. * * In some cases, a binary needs some valid HOME or CWD to continue, but not * all user accounts have valid home directories and even if they do they * may not be readable after a `sudo` operation. * * @return string Path to an empty directory suitable for use as a CWD. */ public static function getEmptyCWD() { $root = dirname(phutil_get_library_root('phabricator')); return $root.'/support/empty/'; } }