host = $host; $this->path = $path; } final public function setApplicationConfiguration( $application_configuration) { $this->applicationConfiguration = $application_configuration; return $this; } final public function getApplicationConfiguration() { return $this->applicationConfiguration; } final public function setPath($path) { $this->path = $path; return $this; } final public function getPath() { return $this->path; } final public function getHost() { // The "Host" header may include a port number, or may be a malicious // header in the form "realdomain.com:ignored@evil.com". Invoke the full // parser to extract the real domain correctly. See here for coverage of // a similar issue in Django: // // https://www.djangoproject.com/weblog/2012/oct/17/security/ $uri = new PhutilURI('http://'.$this->host); return $uri->getDomain(); } /* -( Accessing Request Data )--------------------------------------------- */ /** * @task data */ final public function setRequestData(array $request_data) { $this->requestData = $request_data; return $this; } /** * @task data */ final public function getRequestData() { return $this->requestData; } /** * @task data */ final public function getInt($name, $default = null) { if (isset($this->requestData[$name])) { return (int)$this->requestData[$name]; } else { return $default; } } /** * @task data */ final public function getBool($name, $default = null) { if (isset($this->requestData[$name])) { if ($this->requestData[$name] === 'true') { return true; } else if ($this->requestData[$name] === 'false') { return false; } else { return (bool)$this->requestData[$name]; } } else { return $default; } } /** * @task data */ final public function getStr($name, $default = null) { if (isset($this->requestData[$name])) { $str = (string)$this->requestData[$name]; // Normalize newline craziness. $str = str_replace( array("\r\n", "\r"), array("\n", "\n"), $str); return $str; } else { return $default; } } /** * @task data */ final public function getArr($name, $default = array()) { if (isset($this->requestData[$name]) && is_array($this->requestData[$name])) { return $this->requestData[$name]; } else { return $default; } } /** * @task data */ final public function getStrList($name, $default = array()) { if (!isset($this->requestData[$name])) { return $default; } $list = $this->getStr($name); $list = preg_split('/[\s,]+/', $list, $limit = -1, PREG_SPLIT_NO_EMPTY); return $list; } /** * @task data */ final public function getExists($name) { return array_key_exists($name, $this->requestData); } final public function getFileExists($name) { return isset($_FILES[$name]) && (idx($_FILES[$name], 'error') !== UPLOAD_ERR_NO_FILE); } final public function isHTTPGet() { return ($_SERVER['REQUEST_METHOD'] == 'GET'); } final public function isHTTPPost() { return ($_SERVER['REQUEST_METHOD'] == 'POST'); } final public function isAjax() { return $this->getExists(self::TYPE_AJAX); } final public function isJavelinWorkflow() { return $this->getExists(self::TYPE_WORKFLOW); } final public function isConduit() { return $this->getExists(self::TYPE_CONDUIT); } public static function getCSRFTokenName() { return '__csrf__'; } public static function getCSRFHeaderName() { return 'X-Phabricator-Csrf'; } final public function validateCSRF() { $token_name = self::getCSRFTokenName(); $token = $this->getStr($token_name); // No token in the request, check the HTTP header which is added for Ajax // requests. if (empty($token)) { $token = self::getHTTPHeader(self::getCSRFHeaderName()); } $valid = $this->getUser()->validateCSRFToken($token); if (!$valid) { // Add some diagnostic details so we can figure out if some CSRF issues // are JS problems or people accessing Ajax URIs directly with their // browsers. if ($token) { $token_info = "with an invalid CSRF token"; } else { $token_info = "without a CSRF token"; } if ($this->isAjax()) { $more_info = "(This was an Ajax request, {$token_info}.)"; } else { $more_info = "(This was a web request, {$token_info}.)"; } // Give a more detailed explanation of how to avoid the exception // in developer mode. if (PhabricatorEnv::getEnvConfig('phabricator.developer-mode')) { $more_info = $more_info . "To avoid this error, use phabricator_form() to construct forms. " . "If you are already using phabricator_form(), make sure the form " . "'action' uses a relative URI (i.e., begins with a '/'). Forms " . "using absolute URIs do not include CSRF tokens, to prevent " . "leaking tokens to external sites.\n\n" . "If this page performs writes which do not require CSRF " . "protection (usually, filling caches or logging), you can use " . "AphrontWriteGuard::beginScopedUnguardedWrites() to temporarily " . "bypass CSRF protection while writing. You should use this only " . "for writes which can not be protected with normal CSRF " . "mechanisms.\n\n" . "Some UI elements (like PhabricatorActionListView) also have " . "methods which will allow you to render links as forms (like " . "setRenderAsForm(true))."; } // This should only be able to happen if you load a form, pull your // internet for 6 hours, and then reconnect and immediately submit, // but give the user some indication of what happened since the workflow // is incredibly confusing otherwise. throw new AphrontCSRFException( "The form you just submitted did not include a valid CSRF token. ". "This token is a technical security measure which prevents a ". "certain type of login hijacking attack. However, the token can ". "become invalid if you leave a page open for more than six hours ". "without a connection to the internet. To fix this problem: reload ". "the page, and then resubmit it. All data inserted to the form will ". "be lost in some browsers so copy them somewhere before reloading.\n\n". $more_info); } return true; } final public function isFormPost() { $post = $this->getExists(self::TYPE_FORM) && $this->isHTTPPost(); if (!$post) { return false; } return $this->validateCSRF(); } final public function getCookie($name, $default = null) { return idx($_COOKIE, $name, $default); } final public function clearCookie($name) { $this->setCookie($name, '', time() - (60 * 60 * 24 * 30)); unset($_COOKIE[$name]); } final public function setCookie($name, $value, $expire = null) { $is_secure = false; // If a base URI has been configured, ensure cookies are only set on that // domain. Also, use the URI protocol to control SSL-only cookies. $base_uri = PhabricatorEnv::getEnvConfig('phabricator.base-uri'); if ($base_uri) { $alternates = PhabricatorEnv::getEnvConfig('phabricator.allowed-uris'); $allowed_uris = array_merge( array($base_uri), $alternates); $host = $this->getHost(); $match = null; foreach ($allowed_uris as $allowed_uri) { $uri = new PhutilURI($allowed_uri); $domain = $uri->getDomain(); if ($host == $domain) { $match = $uri; break; } } if ($match === null) { if (count($allowed_uris) > 1) { throw new Exception( pht( 'This Phabricator install is configured as "%s", but you are '. 'accessing it via "%s". Access Phabricator via the primary '. 'configured domain, or one of the permitted alternate '. 'domains: %s. Phabricator will not set cookies on other domains '. 'for security reasons.', $base_uri, $host, implode(', ', $alternates))); } else { throw new Exception( pht( 'This Phabricator install is configured as "%s", but you are '. 'accessing it via "%s". Acccess Phabricator via the primary '. 'configured domain. Phabricator will not set cookies on other '. 'domains for security reasons.', $base_uri, $host)); } } $base_domain = $match->getDomain(); $is_secure = ($match->getProtocol() == 'https'); } else { $base_uri = new PhutilURI(PhabricatorEnv::getRequestBaseURI()); $base_domain = $base_uri->getDomain(); } if ($expire === null) { $expire = time() + (60 * 60 * 24 * 365 * 5); } setcookie( $name, $value, $expire, $path = '/', $base_domain, $is_secure, $http_only = true); $_COOKIE[$name] = $value; return $this; } final public function setUser($user) { $this->user = $user; return $this; } final public function getUser() { return $this->user; } final public function getRequestURI() { $get = $_GET; unset($get['__path__']); $path = phutil_escape_uri($this->getPath()); return id(new PhutilURI($path))->setQueryParams($get); } final public function isDialogFormPost() { return $this->isFormPost() && $this->getStr('__dialog__'); } final public function getRemoteAddr() { return $_SERVER['REMOTE_ADDR']; } public function isHTTPS() { if (empty($_SERVER['HTTPS'])) { return false; } if (!strcasecmp($_SERVER["HTTPS"], "off")) { return false; } return true; } public function isContinueRequest() { return $this->isFormPost() && $this->getStr('__continue__'); } public function isPreviewRequest() { return $this->isFormPost() && $this->getStr('__preview__'); } /** * Get application request parameters in a flattened form suitable for * inclusion in an HTTP request, excluding parameters with special meanings. * This is primarily useful if you want to ask the user for more input and * then resubmit their request. * * @return dict Original request parameters. */ public function getPassthroughRequestParameters() { return self::flattenData($this->getPassthroughRequestData()); } /** * Get request data other than "magic" parameters. * * @return dict Request data, with magic filtered out. */ public function getPassthroughRequestData() { $data = $this->getRequestData(); // Remove magic parameters like __dialog__ and __ajax__. foreach ($data as $key => $value) { if (!strncmp($key, '__', 2)) { unset($data[$key]); } } return $data; } /** * Flatten an array of key-value pairs (possibly including arrays as values) * into a list of key-value pairs suitable for submitting via HTTP request * (with arrays flattened). * * @param dict Data to flatten. * @return dict Flat data suitable for inclusion in an HTTP * request. */ public static function flattenData(array $data) { $result = array(); foreach ($data as $key => $value) { if (is_array($value)) { foreach (self::flattenData($value) as $fkey => $fvalue) { $fkey = '['.preg_replace('/(?=\[)|$/', ']', $fkey, $limit = 1); $result[$key.$fkey] = $fvalue; } } else { $result[$key] = (string)$value; } } ksort($result); return $result; } public static function getHTTPHeader($name, $default = null) { // PHP mangles HTTP headers by uppercasing them and replacing hyphens with // underscores, then prepending 'HTTP_'. $php_index = strtoupper($name); $php_index = str_replace('-', '_', $php_index); $php_index = 'HTTP_'.$php_index; return idx($_SERVER, $php_index, $default); } }