diff --git a/src/__celerity_resource_map__.php b/src/__celerity_resource_map__.php index 2f6d320118..ffd9140dda 100644 --- a/src/__celerity_resource_map__.php +++ b/src/__celerity_resource_map__.php @@ -738,7 +738,7 @@ celerity_register_resource_map(array( ), 'javelin-behavior-refresh-csrf' => array( - 'uri' => '/res/39aa51f7/rsrc/js/application/core/behavior-refresh-csrf.js', + 'uri' => '/res/a8204a73/rsrc/js/application/core/behavior-refresh-csrf.js', 'type' => 'js', 'requires' => array( diff --git a/src/aphront/request/AphrontRequest.php b/src/aphront/request/AphrontRequest.php index 363633fd74..30a23b238a 100644 --- a/src/aphront/request/AphrontRequest.php +++ b/src/aphront/request/AphrontRequest.php @@ -117,15 +117,33 @@ class AphrontRequest { return $this->getExists(self::TYPE_AJAX); } - final public function isFormPost() { - $post = $this->getExists(self::TYPE_FORM) && - $this->isHTTPPost(); + public static function getCSRFTokenName() { + return '__csrf__'; + } - if (!$post) { - return false; + 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); } - $valid = $this->getUser()->validateCSRFToken($this->getStr('__csrf__')); + $valid = $this->getUser()->validateCSRFToken($token); 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, @@ -143,6 +161,17 @@ class AphrontRequest { 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); } diff --git a/src/applications/files/controller/dropupload/PhabricatorFileDropUploadController.php b/src/applications/files/controller/dropupload/PhabricatorFileDropUploadController.php index 820482f39e..4e8d48f2cf 100644 --- a/src/applications/files/controller/dropupload/PhabricatorFileDropUploadController.php +++ b/src/applications/files/controller/dropupload/PhabricatorFileDropUploadController.php @@ -22,6 +22,9 @@ class PhabricatorFileDropUploadController extends PhabricatorFileController { $request = $this->getRequest(); $user = $request->getUser(); + // NOTE: Throws if valid CSRF token is not present in the request. + $request->validateCSRF(); + $data = file_get_contents('php://input'); $name = $request->getStr('name'); diff --git a/src/infrastructure/javelin/markup/__init__.php b/src/infrastructure/javelin/markup/__init__.php index 790f5404cc..eee8eff821 100644 --- a/src/infrastructure/javelin/markup/__init__.php +++ b/src/infrastructure/javelin/markup/__init__.php @@ -6,6 +6,7 @@ +phutil_require_module('phabricator', 'aphront/request'); phutil_require_module('phabricator', 'infrastructure/celerity/api'); phutil_require_module('phutil', 'markup'); diff --git a/src/infrastructure/javelin/markup/markup.php b/src/infrastructure/javelin/markup/markup.php index 15c1db7b9a..f6feb76595 100644 --- a/src/infrastructure/javelin/markup/markup.php +++ b/src/infrastructure/javelin/markup/markup.php @@ -56,7 +56,7 @@ function phabricator_render_form(PhabricatorUser $user, $attributes, $content) { 'input', array( 'type' => 'hidden', - 'name' => '__csrf__', + 'name' => AphrontRequest::getCSRFTokenName(), 'value' => $user->getCSRFToken(), )). phutil_render_tag( diff --git a/src/view/form/base/AphrontFormView.php b/src/view/form/base/AphrontFormView.php index 9adb7f4d27..cfef0f1f64 100644 --- a/src/view/form/base/AphrontFormView.php +++ b/src/view/form/base/AphrontFormView.php @@ -92,7 +92,7 @@ final class AphrontFormView extends AphrontView { $data = $this->data + array( '__form__' => 1, - '__csrf__' => $this->user->getCSRFToken(), + AphrontRequest::getCSRFTokenName() => $this->user->getCSRFToken(), ); $inputs = array(); foreach ($data as $key => $value) { diff --git a/src/view/form/base/__init__.php b/src/view/form/base/__init__.php index 2b61fad475..ff4720b1d4 100644 --- a/src/view/form/base/__init__.php +++ b/src/view/form/base/__init__.php @@ -6,6 +6,7 @@ +phutil_require_module('phabricator', 'aphront/request'); phutil_require_module('phabricator', 'infrastructure/celerity/api'); phutil_require_module('phabricator', 'infrastructure/javelin/api'); phutil_require_module('phabricator', 'infrastructure/javelin/markup'); diff --git a/src/view/page/standard/PhabricatorStandardPageView.php b/src/view/page/standard/PhabricatorStandardPageView.php index 78a80561d2..9ac48deaba 100644 --- a/src/view/page/standard/PhabricatorStandardPageView.php +++ b/src/view/page/standard/PhabricatorStandardPageView.php @@ -121,8 +121,23 @@ class PhabricatorStandardPageView extends AphrontPageView { require_celerity_resource('phabricator-core-buttons-css'); require_celerity_resource('phabricator-standard-page-view'); + $current_token = null; + $request = $this->getRequest(); + if ($request) { + $user = $request->getUser(); + if ($user) { + $current_token = $user->getCSRFToken(); + } + } + Javelin::initBehavior('workflow', array()); - Javelin::initBehavior('refresh-csrf', array()); + Javelin::initBehavior( + 'refresh-csrf', + array( + 'tokenName' => AphrontRequest::getCSRFTokenName(), + 'header' => AphrontRequest::getCSRFHeaderName(), + 'current' => $current_token, + )); Javelin::initBehavior( 'phabricator-keyboard-shortcuts', array( diff --git a/src/view/page/standard/__init__.php b/src/view/page/standard/__init__.php index 7db98f4535..6acc29d440 100644 --- a/src/view/page/standard/__init__.php +++ b/src/view/page/standard/__init__.php @@ -6,6 +6,7 @@ +phutil_require_module('phabricator', 'aphront/request'); phutil_require_module('phabricator', 'applications/people/storage/preferences'); phutil_require_module('phabricator', 'infrastructure/celerity/api'); phutil_require_module('phabricator', 'infrastructure/env'); diff --git a/webroot/rsrc/js/application/core/behavior-refresh-csrf.js b/webroot/rsrc/js/application/core/behavior-refresh-csrf.js index 74d568c54e..06baa873ba 100644 --- a/webroot/rsrc/js/application/core/behavior-refresh-csrf.js +++ b/webroot/rsrc/js/application/core/behavior-refresh-csrf.js @@ -27,18 +27,26 @@ */ JX.behavior('refresh-csrf', function(config) { + var current_token = config.current; + function refresh_csrf() { new JX.Request('/login/refresh/', function(r) { - var inputs = JX.DOM.scry(document.body, 'input'); - for (var ii = 0; ii < inputs.length; ii++) { - if (inputs[ii].name == '__csrf__') { - inputs[ii].value = r.token; - } + current_token = r.token; + var inputs = JX.DOM.scry(document.body, 'input'); + for (var ii = 0; ii < inputs.length; ii++) { + if (inputs[ii].name == config.tokenName) { + inputs[ii].value = r.token; } - }) - .send(); + } + }) + .send(); } // Refresh the CSRF tokens every 55 minutes. setInterval(refresh_csrf, 1000 * 60 * 55); + + // Additionally, add the CSRF token as an HTTP header to every AJAX request. + JX.Request.listen('open', function(r) { + r.getTransport().setRequestHeader(config.header, current_token); + }); });