1
0
Fork 0
mirror of https://we.phorge.it/source/phorge.git synced 2024-11-11 09:22:40 +01:00
phorge-phorge/src/aphront/request/AphrontRequest.php

256 lines
7.1 KiB
PHP
Raw Normal View History

<?php
/*
* Copyright 2011 Facebook, Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
/**
* @group aphront
*/
class AphrontRequest {
const TYPE_AJAX = '__ajax__';
const TYPE_FORM = '__form__';
private $host;
private $path;
private $requestData;
2011-01-26 22:21:12 +01:00
private $user;
private $env;
2011-02-02 22:48:52 +01:00
private $applicationConfiguration;
2011-02-02 22:48:52 +01:00
final public function __construct($host, $path) {
$this->host = $host;
$this->path = $path;
}
2011-02-02 22:48:52 +01:00
final public function setApplicationConfiguration(
$application_configuration) {
$this->applicationConfiguration = $application_configuration;
return $this;
}
2011-02-02 22:48:52 +01:00
final public function getApplicationConfiguration() {
return $this->applicationConfiguration;
}
final public function setRequestData(array $request_data) {
$this->requestData = $request_data;
return $this;
}
final public function getPath() {
return $this->path;
}
final public function getHost() {
return $this->host;
}
final public function getInt($name, $default = null) {
if (isset($this->requestData[$name])) {
return (int)$this->requestData[$name];
} else {
return $default;
}
}
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;
}
}
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;
}
}
2011-01-26 02:40:21 +01:00
final public function getArr($name, $default = array()) {
if (isset($this->requestData[$name]) &&
is_array($this->requestData[$name])) {
return $this->requestData[$name];
} else {
return $default;
}
}
final public function getExists($name) {
return array_key_exists($name, $this->requestData);
}
final public function isHTTPPost() {
return ($_SERVER['REQUEST_METHOD'] == 'POST');
}
final public function isAjax() {
return $this->getExists(self::TYPE_AJAX);
}
public static function getCSRFTokenName() {
return '__csrf__';
}
Fix conservative CSRF token cycling limit Summary: We currently cycle CSRF tokens every hour and check for the last two valid ones. This means that a form could go stale in as little as an hour, and is certainly stale after two. When a stale form is submitted, you basically get a terrible heisen-state where some of your data might persist if you're lucky but more likely it all just vanishes. The .js file below outlines some more details. This is a pretty terrible UX and we don't need to be as conservative about CSRF validation as we're being. Remedy this problem by: - Accepting the last 6 CSRF tokens instead of the last 1 (i.e., pages are valid for at least 6 hours, and for as long as 7). - Using JS to refresh the CSRF token every 55 minutes (i.e., pages connected to the internet are valid indefinitely). - Showing the user an explicit message about what went wrong when CSRF validation fails so the experience is less bewildering. They should now only be able to submit with a bad CSRF token if: - They load a page, disconnect from the internet for 7 hours, reconnect, and submit the form within 55 minutes; or - They are actually the victim of a CSRF attack. We could eventually fix the first one by tracking reconnects, which might be "free" once the notification server gets built. It will probably never be an issue in practice. Test Plan: - Reduced CSRF cycle frequency to 2 seconds, submitted a form after 15 seconds, got the CSRF exception. - Reduced csrf-refresh cycle frequency to 3 seconds, submitted a form after 15 seconds, got a clean form post. - Added debugging code the the csrf refresh to make sure it was doing sensible things (pulling different tokens, finding all the inputs). Reviewed By: aran Reviewers: tuomaspelkonen, jungejason, aran CC: aran, epriestley Differential Revision: 660
2011-07-13 23:05:18 +02:00
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)) {
// PHP mangles HTTP headers by uppercasing them and replacing hyphens with
// underscores, then prepending 'HTTP_'.
$php_index = self::getCSRFHeaderName();
$php_index = strtoupper($php_index);
$php_index = str_replace('-', '_', $php_index);
$php_index = 'HTTP_'.$php_index;
$token = idx($_SERVER, $php_index);
Fix conservative CSRF token cycling limit Summary: We currently cycle CSRF tokens every hour and check for the last two valid ones. This means that a form could go stale in as little as an hour, and is certainly stale after two. When a stale form is submitted, you basically get a terrible heisen-state where some of your data might persist if you're lucky but more likely it all just vanishes. The .js file below outlines some more details. This is a pretty terrible UX and we don't need to be as conservative about CSRF validation as we're being. Remedy this problem by: - Accepting the last 6 CSRF tokens instead of the last 1 (i.e., pages are valid for at least 6 hours, and for as long as 7). - Using JS to refresh the CSRF token every 55 minutes (i.e., pages connected to the internet are valid indefinitely). - Showing the user an explicit message about what went wrong when CSRF validation fails so the experience is less bewildering. They should now only be able to submit with a bad CSRF token if: - They load a page, disconnect from the internet for 7 hours, reconnect, and submit the form within 55 minutes; or - They are actually the victim of a CSRF attack. We could eventually fix the first one by tracking reconnects, which might be "free" once the notification server gets built. It will probably never be an issue in practice. Test Plan: - Reduced CSRF cycle frequency to 2 seconds, submitted a form after 15 seconds, got the CSRF exception. - Reduced csrf-refresh cycle frequency to 3 seconds, submitted a form after 15 seconds, got a clean form post. - Added debugging code the the csrf refresh to make sure it was doing sensible things (pulling different tokens, finding all the inputs). Reviewed By: aran Reviewers: tuomaspelkonen, jungejason, aran CC: aran, epriestley Differential Revision: 660
2011-07-13 23:05:18 +02:00
}
$valid = $this->getUser()->validateCSRFToken($token);
Fix conservative CSRF token cycling limit Summary: We currently cycle CSRF tokens every hour and check for the last two valid ones. This means that a form could go stale in as little as an hour, and is certainly stale after two. When a stale form is submitted, you basically get a terrible heisen-state where some of your data might persist if you're lucky but more likely it all just vanishes. The .js file below outlines some more details. This is a pretty terrible UX and we don't need to be as conservative about CSRF validation as we're being. Remedy this problem by: - Accepting the last 6 CSRF tokens instead of the last 1 (i.e., pages are valid for at least 6 hours, and for as long as 7). - Using JS to refresh the CSRF token every 55 minutes (i.e., pages connected to the internet are valid indefinitely). - Showing the user an explicit message about what went wrong when CSRF validation fails so the experience is less bewildering. They should now only be able to submit with a bad CSRF token if: - They load a page, disconnect from the internet for 7 hours, reconnect, and submit the form within 55 minutes; or - They are actually the victim of a CSRF attack. We could eventually fix the first one by tracking reconnects, which might be "free" once the notification server gets built. It will probably never be an issue in practice. Test Plan: - Reduced CSRF cycle frequency to 2 seconds, submitted a form after 15 seconds, got the CSRF exception. - Reduced csrf-refresh cycle frequency to 3 seconds, submitted a form after 15 seconds, got a clean form post. - Added debugging code the the csrf refresh to make sure it was doing sensible things (pulling different tokens, finding all the inputs). Reviewed By: aran Reviewers: tuomaspelkonen, jungejason, aran CC: aran, epriestley Differential Revision: 660
2011-07-13 23:05:18 +02:00
if (!$valid) {
// 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.");
}
return true;
}
final public function isFormPost() {
$post = $this->getExists(self::TYPE_FORM) &&
$this->isHTTPPost();
if (!$post) {
return false;
}
return $this->validateCSRF();
}
2011-01-26 22:21:12 +01:00
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));
}
final public function setCookie($name, $value, $expire = null) {
// Ensure cookies are only set on the configured domain.
$base_uri = PhabricatorEnv::getEnvConfig('phabricator.base-uri');
$base_uri = new PhutilURI($base_uri);
$base_domain = $base_uri->getDomain();
$base_protocol = $base_uri->getProtocol();
// The "Host" header may include a port number; if so, ignore it. We can't
// use PhutilURI since there's no URI scheme.
list($actual_host) = explode(':', $this->getHost(), 2);
if ($base_domain != $actual_host) {
throw new Exception(
"This install of Phabricator is configured as '{$base_domain}' but ".
"you are accessing it via '{$actual_host}'. Access Phabricator via ".
"the primary configured domain.");
}
2011-01-26 22:21:12 +01:00
if ($expire === null) {
$expire = time() + (60 * 60 * 24 * 365 * 5);
}
if ($value == '') {
// NOTE: If we're clearing the cookie, also clear it on the entire
// domain and both HTTP/HTTPS versions. This allows us to clear older
// cookies which we didn't scope as tightly. Eventually we could remove
// this, although it doesn't really hurt us. Basically, we're just making
// really sure that cookies get cleared when we try to clear them.
$secure_options = array(true, false);
$domain_options = array('', $base_domain);
} else {
// Otherwise, when setting cookies, set only one tightly-scoped cookie.
$is_secure = ($base_protocol == 'https');
$secure_options = array($is_secure);
$domain_options = array($base_domain);
}
foreach ($secure_options as $cookie_secure) {
foreach ($domain_options as $cookie_domain) {
setcookie(
$name,
$value,
$expire,
$path = '/',
$cookie_domain,
$cookie_secure,
$http_only = true);
}
}
2011-01-26 22:21:12 +01:00
}
final public function setUser($user) {
$this->user = $user;
return $this;
}
final public function getUser() {
return $this->user;
}
2011-02-06 01:43:28 +01:00
final public function getRequestURI() {
$get = $_GET;
unset($get['__path__']);
return id(new PhutilURI($this->getPath()))->setQueryParams($get);
}
2011-02-06 07:36:21 +01:00
final public function isDialogFormPost() {
return $this->isFormPost() && $this->getStr('__dialog__');
}
}