diff --git a/src/applications/differential/field/DifferentialRevisionIDCommitMessageField.php b/src/applications/differential/field/DifferentialRevisionIDCommitMessageField.php
index 8c85553e71..ac8ba4ebd4 100644
--- a/src/applications/differential/field/DifferentialRevisionIDCommitMessageField.php
+++ b/src/applications/differential/field/DifferentialRevisionIDCommitMessageField.php
@@ -1,100 +1,86 @@
 <?php
 
 final class DifferentialRevisionIDCommitMessageField
   extends DifferentialCommitMessageField {
 
   const FIELDKEY = 'revisionID';
 
   public function getFieldName() {
     return pht('Differential Revision');
   }
 
   public function getFieldOrder() {
     return 200000;
   }
 
   public function isTemplateField() {
     return false;
   }
 
   public function parseFieldValue($value) {
     // If the complete commit message we are parsing has unrecognized custom
     // fields at the end, they can end up parsed into the field value for this
     // field. For example, if the message looks like this:
 
     // Differential Revision: xyz
     // Some-Other-Field: abc
 
     // ...we will receive "xyz\nSome-Other-Field: abc" as the field value for
     // this field. Ideally, the install would define these fields so they can
     // parse formally, but we can reasonably assume that only the first line
     // of any value we encounter actually contains a revision identifier, so
     // start by throwing away any other lines.
 
     $value = trim($value);
     $value = phutil_split_lines($value, false);
     $value = head($value);
     $value = trim($value);
 
     // If the value is just "D123" or similar, parse the ID from it directly.
     $matches = null;
     if (preg_match('/^[dD]([1-9]\d*)\z/', $value, $matches)) {
       return (int)$matches[1];
     }
 
     // Otherwise, try to extract a URI value.
     return self::parseRevisionIDFromURI($value);
   }
 
   private static function parseRevisionIDFromURI($uri_string) {
     $uri = new PhutilURI($uri_string);
     $path = $uri->getPath();
 
-    $matches = null;
-    if (preg_match('#^/D(\d+)$#', $path, $matches)) {
-      $id = (int)$matches[1];
-
-      $prod_uri = new PhutilURI(PhabricatorEnv::getProductionURI('/D'.$id));
-
-      // Make sure the URI is the same as our URI. Basically, we want to ignore
-      // commits from other Phabricator installs.
-      if ($uri->getDomain() == $prod_uri->getDomain()) {
-        return $id;
-      }
-
-      $allowed_uris = PhabricatorEnv::getAllowedURIs('/D'.$id);
-
-      foreach ($allowed_uris as $allowed_uri) {
-        if ($uri_string == $allowed_uri) {
-          return $id;
-        }
+    if (PhabricatorEnv::isSelfURI($uri_string)) {
+      $matches = null;
+      if (preg_match('#^/D(\d+)$#', $path, $matches)) {
+        return (int)$matches[1];
       }
     }
 
     return null;
   }
 
   public function readFieldValueFromObject(DifferentialRevision $revision) {
     return $revision->getID();
   }
 
   public function readFieldValueFromConduit($value) {
     if (is_int($value)) {
       $value = (string)$value;
     }
     return $this->readStringFieldValueFromConduit($value);
   }
 
   public function renderFieldValue($value) {
     if (!strlen($value)) {
       return null;
     }
 
     return PhabricatorEnv::getProductionURI('/D'.$value);
   }
 
   public function getFieldTransactions($value) {
     return array();
   }
 
 }
diff --git a/src/applications/differential/field/__tests__/DifferentialCommitMessageFieldTestCase.php b/src/applications/differential/field/__tests__/DifferentialCommitMessageFieldTestCase.php
index 295da8ca59..41fd2992f4 100644
--- a/src/applications/differential/field/__tests__/DifferentialCommitMessageFieldTestCase.php
+++ b/src/applications/differential/field/__tests__/DifferentialCommitMessageFieldTestCase.php
@@ -1,30 +1,31 @@
 <?php
 
 final class DifferentialCommitMessageFieldTestCase
   extends PhabricatorTestCase {
 
   public function testRevisionCommitMessageFieldParsing() {
     $base_uri = 'https://www.example.com/';
 
     $tests = array(
       'D123' => 123,
       'd123' => 123,
       "  \n  d123 \n " => 123,
       "D123\nSome-Custom-Field: The End" => 123,
       "{$base_uri}D123" => 123,
       "{$base_uri}D123\nSome-Custom-Field: The End" => 123,
+      'https://www.other.com/D123' => null,
     );
 
     $env = PhabricatorEnv::beginScopedEnv();
     $env->overrideEnvConfig('phabricator.base-uri', $base_uri);
 
     foreach ($tests as $input => $expect) {
       $actual = id(new DifferentialRevisionIDCommitMessageField())
         ->parseFieldValue($input);
       $this->assertEqual($expect, $actual, pht('Parse of: %s', $input));
     }
 
     unset($env);
   }
 
 }
diff --git a/src/infrastructure/env/PhabricatorEnv.php b/src/infrastructure/env/PhabricatorEnv.php
index b50c85c0f0..70ddb80630 100644
--- a/src/infrastructure/env/PhabricatorEnv.php
+++ b/src/infrastructure/env/PhabricatorEnv.php
@@ -1,940 +1,963 @@
 <?php
 
 /**
  * Manages the execution environment configuration, exposing APIs to read
  * configuration settings and other similar values that are derived directly
  * from configuration settings.
  *
  *
  * = Reading Configuration =
  *
  * The primary role of this class is to provide an API for reading
  * Phabricator configuration, @{method:getEnvConfig}:
  *
  *   $value = PhabricatorEnv::getEnvConfig('some.key', $default);
  *
  * The class also handles some URI construction based on configuration, via
  * the methods @{method:getURI}, @{method:getProductionURI},
  * @{method:getCDNURI}, and @{method:getDoclink}.
  *
  * For configuration which allows you to choose a class to be responsible for
  * some functionality (e.g., which mail adapter to use to deliver email),
  * @{method:newObjectFromConfig} provides a simple interface that validates
  * the configured value.
  *
  *
  * = Unit Test Support =
  *
  * In unit tests, you can use @{method:beginScopedEnv} to create a temporary,
  * mutable environment. The method returns a scope guard object which restores
  * the environment when it is destroyed. For example:
  *
  *   public function testExample() {
  *     $env = PhabricatorEnv::beginScopedEnv();
  *     $env->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;
+
+  public static function isSelfURI($raw_uri) {
+    $uri = new PhutilURI($raw_uri);
+
+    $host = $uri->getDomain();
+    if (!strlen($host)) {
+      return false;
     }
 
+    $host = phutil_utf8_strtolower($host);
+
+    $self_map = self::getSelfURIMap();
+    return isset($self_map[$host]);
+  }
+
+  private static function getSelfURIMap() {
+    $self_uris = array();
+    $self_uris[] = self::getProductionURI('/');
+    $self_uris[] = self::getURI('/');
+
     $allowed_uris = self::getEnvConfig('phabricator.allowed-uris');
-    $return = array();
     foreach ($allowed_uris as $allowed_uri) {
-      $return[] = rtrim($allowed_uri, '/').$path;
+      $self_uris[] = $allowed_uri;
     }
 
-    return $return;
-  }
+    $self_map = array();
+    foreach ($self_uris as $self_uri) {
+      $host = id(new PhutilURI($self_uri))->getDomain();
+      if (!strlen($host)) {
+        continue;
+      }
+
+      $host = phutil_utf8_strtolower($host);
+      $self_map[$host] = $host;
+    }
 
+    return $self_map;
+  }
 
   /**
    * 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<string> 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<string> Allowed protocols.
    * @return pair<string, string> Pre-resolved URI and domain.
    * @task uri
    */
   public static function requireValidRemoteURIForFetch(
     $raw_uri,
     array $protocols) {
 
     $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.',
           $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.',
           $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.',
           $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.',
           $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.',
             $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 = self::getRemoteAddress();
     if (!$address) {
       throw new Exception(
         pht(
           'Unable to test remote address against cluster whitelist: '.
           'REMOTE_ADDR is not defined or not valid.'));
     }
 
     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);
   }
 
   public static function getRemoteAddress() {
     $address = idx($_SERVER, 'REMOTE_ADDR');
     if (!$address) {
       return null;
     }
 
     try {
       return PhutilIPAddress::newAddress($address);
     } catch (Exception $ex) {
       return null;
     }
   }
 
 /* -(  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/';
   }
 
 
 }
diff --git a/src/infrastructure/env/__tests__/PhabricatorEnvTestCase.php b/src/infrastructure/env/__tests__/PhabricatorEnvTestCase.php
index d47258822b..f73299aa12 100644
--- a/src/infrastructure/env/__tests__/PhabricatorEnvTestCase.php
+++ b/src/infrastructure/env/__tests__/PhabricatorEnvTestCase.php
@@ -1,221 +1,256 @@
 <?php
 
 final class PhabricatorEnvTestCase extends PhabricatorTestCase {
 
   public function testLocalURIForLink() {
     $map = array(
       '/'                     => true,
       '/D123'                 => true,
       '/path/to/something/'   => true,
       "/path/to/\nHeader: x"  => false,
       'http://evil.com/'      => false,
       '//evil.com/evil/'      => false,
       'javascript:lol'        => false,
       ''                      => false,
       null                    => false,
       '/\\evil.com'           => false,
     );
 
     foreach ($map as $uri => $expect) {
       $this->assertEqual(
         $expect,
         PhabricatorEnv::isValidLocalURIForLink($uri),
         pht('Valid local resource: %s', $uri));
     }
   }
 
   public function testRemoteURIForLink() {
     $map = array(
       'http://example.com/' => true,
       'derp://example.com/' => false,
       'javascript:alert(1)' => false,
       'http://127.0.0.1/' => true,
       'http://169.254.169.254/latest/meta-data/hostname' => true,
     );
 
     foreach ($map as $uri => $expect) {
       $this->assertEqual(
         $expect,
         PhabricatorEnv::isValidRemoteURIForLink($uri),
         pht('Valid linkable remote URI: %s', $uri));
     }
   }
 
   public function testRemoteURIForFetch() {
     $map = array(
       'http://example.com/' => true,
 
       // No domain or protocol.
       '' => false,
 
       // No domain.
       'http://' => false,
 
       // No protocol.
       'evil.com' => false,
 
       // No protocol.
       '//evil.com' => false,
 
       // Bad protocol.
       'javascript://evil.com/' => false,
       'file:///etc/shadow' => false,
 
       // Unresolvable hostname.
       'http://u1VcxwUp368SIFzl7rkWWg23KM5JPB2kTHHngxjXCQc.zzz/' => false,
 
       // Domains explicitly in blacklisted IP space.
       'http://127.0.0.1/' => false,
       'http://169.254.169.254/latest/meta-data/hostname' => false,
 
       // Domain resolves into blacklisted IP space.
       'http://localhost/' => false,
     );
 
     $protocols = array('http', 'https');
 
     foreach ($map as $uri => $expect) {
       $this->assertEqual(
         $expect,
         PhabricatorEnv::isValidRemoteURIForFetch($uri, $protocols),
         pht('Valid fetchable remote URI: %s', $uri));
     }
   }
 
   public function testDictionarySource() {
     $source = new PhabricatorConfigDictionarySource(array('x' => 1));
 
     $this->assertEqual(
       array(
         'x' => 1,
       ),
       $source->getKeys(array('x', 'z')));
 
     $source->setKeys(array('z' => 2));
 
     $this->assertEqual(
       array(
         'x' => 1,
         'z' => 2,
       ),
       $source->getKeys(array('x', 'z')));
 
     $source->setKeys(array('x' => 3));
 
     $this->assertEqual(
       array(
         'x' => 3,
         'z' => 2,
       ),
       $source->getKeys(array('x', 'z')));
 
     $source->deleteKeys(array('x'));
 
     $this->assertEqual(
       array(
         'z' => 2,
       ),
       $source->getKeys(array('x', 'z')));
   }
 
   public function testStackSource() {
     $s1 = new PhabricatorConfigDictionarySource(array('x' => 1));
     $s2 = new PhabricatorConfigDictionarySource(array('x' => 2));
 
     $stack = new PhabricatorConfigStackSource();
 
     $this->assertEqual(array(), $stack->getKeys(array('x')));
 
     $stack->pushSource($s1);
     $this->assertEqual(array('x' => 1), $stack->getKeys(array('x')));
 
     $stack->pushSource($s2);
     $this->assertEqual(array('x' => 2), $stack->getKeys(array('x')));
 
     $stack->setKeys(array('x' => 3));
     $this->assertEqual(array('x' => 3), $stack->getKeys(array('x')));
 
     $stack->popSource();
     $this->assertEqual(array('x' => 1), $stack->getKeys(array('x')));
 
     $stack->popSource();
     $this->assertEqual(array(), $stack->getKeys(array('x')));
 
     $caught = null;
     try {
       $stack->popSource();
     } catch (Exception $ex) {
       $caught = $ex;
     }
 
     $this->assertTrue($caught instanceof Exception);
   }
 
   public function testOverrides() {
     $outer = PhabricatorEnv::beginScopedEnv();
 
       $outer->overrideEnvConfig('test.value', 1);
       $this->assertEqual(1, PhabricatorEnv::getEnvConfig('test.value'));
 
       $inner = PhabricatorEnv::beginScopedEnv();
         $inner->overrideEnvConfig('test.value', 2);
         $this->assertEqual(2, PhabricatorEnv::getEnvConfig('test.value'));
       if (phutil_is_hiphop_runtime()) {
         $inner->__destruct();
       }
       unset($inner);
 
       $this->assertEqual(1, PhabricatorEnv::getEnvConfig('test.value'));
     if (phutil_is_hiphop_runtime()) {
       $outer->__destruct();
     }
     unset($outer);
   }
 
   public function testOverrideOrder() {
     $outer = PhabricatorEnv::beginScopedEnv();
     $inner = PhabricatorEnv::beginScopedEnv();
 
     $caught = null;
     try {
       $outer->__destruct();
     } catch (Exception $ex) {
       $caught = $ex;
     }
 
     $this->assertTrue(
       $caught instanceof Exception,
       pht(
         'Destroying a scoped environment which is not on the top of the '.
         'stack should throw.'));
 
     if (phutil_is_hiphop_runtime()) {
       $inner->__destruct();
     }
     unset($inner);
 
     if (phutil_is_hiphop_runtime()) {
       $outer->__destruct();
     }
     unset($outer);
   }
 
   public function testGetEnvExceptions() {
     $caught = null;
     try {
       PhabricatorEnv::getEnvConfig('not.a.real.config.option');
     } catch (Exception $ex) {
       $caught = $ex;
     }
     $this->assertTrue($caught instanceof Exception);
 
     $caught = null;
     try {
       PhabricatorEnv::getEnvConfig('test.value');
     } catch (Exception $ex) {
       $caught = $ex;
     }
     $this->assertFalse($caught instanceof Exception);
   }
 
+  public function testSelfURI() {
+    $base_uri = 'https://allowed.example.com/';
+
+    $allowed_uris = array(
+      'https://old.example.com/',
+    );
+
+    $env = PhabricatorEnv::beginScopedEnv();
+    $env->overrideEnvConfig('phabricator.base-uri', $base_uri);
+    $env->overrideEnvConfig('phabricator.allowed-uris', $allowed_uris);
+
+    $map = array(
+      'https://allowed.example.com/' => true,
+      'https://allowed.example.com' => true,
+      'https://allowed.EXAMPLE.com' => true,
+      'http://allowed.example.com/' => true,
+      'https://allowed.example.com/path/to/resource.png' => true,
+
+      'https://old.example.com/' => true,
+      'https://old.example.com' => true,
+      'https://old.EXAMPLE.com' => true,
+      'http://old.example.com/' => true,
+      'https://old.example.com/path/to/resource.png' => true,
+
+      'https://other.example.com/' => false,
+    );
+
+    foreach ($map as $input => $expect) {
+      $this->assertEqual(
+        $expect,
+        PhabricatorEnv::isSelfURI($input),
+        pht('Is self URI? %s', $input));
+    }
+  }
+
 }