public function validateTransactions($object, array $xactions) { $viewer = $this->getActor(); $errors = array(); $ics_type = PhabricatorCalendarICSURIImportEngine::ENGINETYPE; $import_type = $object->getEngine()->getImportEngineType(); if ($import_type != $ics_type) { if (!$xactions) { return $errors; } $errors[] = $this->newInvalidError(pht('You can not attach an ICS URI to an import type other than ' . 'an ICS URI import (type is "%s").', $import_type)); return $errors; } $new_value = $object->getParameter(self::PARAMKEY_URI); foreach ($xactions as $xaction) { $new_value = $xaction->getNewValue(); if (!strlen($new_value)) { continue; } try { PhabricatorEnv::requireValidRemoteURIForFetch($new_value, array('http', 'https')); } catch (Exception $ex) { $errors[] = $this->newInvalidError($ex->getMessage(), $xaction); } } if (!strlen($new_value)) { $errors[] = $this->newRequiredError(pht('You must select an ".ics" URI to import.')); } return $errors; }
/** * Download a remote resource over HTTP and save the response body as a file. * * This method respects `security.outbound-blacklist`, and protects against * HTTP redirection (by manually following "Location" headers and verifying * each destination). It does not protect against DNS rebinding. See * discussion in T6755. */ public static function newFromFileDownload($uri, array $params = array()) { $timeout = 5; $redirects = array(); $current = $uri; while (true) { try { if (count($redirects) > 10) { throw new Exception(pht('Too many redirects trying to fetch remote URI.')); } $resolved = PhabricatorEnv::requireValidRemoteURIForFetch($current, array('http', 'https')); list($resolved_uri, $resolved_domain) = $resolved; $current = new PhutilURI($current); if ($current->getProtocol() == 'http') { // For HTTP, we can use a pre-resolved URI to defuse DNS rebinding. $fetch_uri = $resolved_uri; $fetch_host = $resolved_domain; } else { // For HTTPS, we can't: cURL won't verify the SSL certificate if // the domain has been replaced with an IP. But internal services // presumably will not have valid certificates for rebindable // domain names on attacker-controlled domains, so the DNS rebinding // attack should generally not be possible anyway. $fetch_uri = $current; $fetch_host = null; } $future = id(new HTTPSFuture($fetch_uri))->setFollowLocation(false)->setTimeout($timeout); if ($fetch_host !== null) { $future->addHeader('Host', $fetch_host); } list($status, $body, $headers) = $future->resolve(); if ($status->isRedirect()) { // This is an HTTP 3XX status, so look for a "Location" header. $location = null; foreach ($headers as $header) { list($name, $value) = $header; if (phutil_utf8_strtolower($name) == 'location') { $location = $value; break; } } // HTTP 3XX status with no "Location" header, just treat this like // a normal HTTP error. if ($location === null) { throw $status; } if (isset($redirects[$location])) { throw new Exception(pht('Encountered loop while following redirects.')); } $redirects[$location] = $location; $current = $location; // We'll fall off the bottom and go try this URI now. } else { if ($status->isError()) { // This is something other than an HTTP 2XX or HTTP 3XX status, so // just bail out. throw $status; } else { // This is HTTP 2XX, so use the response body to save the // file data. $params = $params + array('name' => basename($uri)); return self::newFromFileData($body, $params); } } } catch (Exception $ex) { if ($redirects) { throw new PhutilProxyException(pht('Failed to fetch remote URI "%s" after following %s redirect(s) ' . '(%s): %s', $uri, new PhutilNumber(count($redirects)), implode(' > ', array_keys($redirects)), $ex->getMessage()), $ex); } else { throw $ex; } } } }