mirror of
https://we.phorge.it/source/phorge.git
synced 2024-11-22 23:02:42 +01:00
Prevent CSRF uploads via /file/dropupload/
Summary: We don't currently validate CSRF tokens on this workflow. This allows an attacker to upload arbitrary files on the user's behalf. Although I believe the tight list of servable mime-types means that's more or less the end of the attack, this is still a vulnerability. In the long term, the right solution is probably to pass CSRF tokens on all Ajax requests in an HTTP header (or just a GET param) or something like that. However, this endpoint is unique and this is the quickest and most direct way to close the hole. Test Plan: - Drop-uploaded files to Files, Maniphest, Phriction and Differential. - Modified CSRF vaidator to use __csrf__.'x' and verified uploads and form submissions don't work. Reviewers: andrewjcg, aran, jungejason, tuomaspelkonen, erling Commenters: andrewjcg, pedram CC: aran, epriestley, andrewjcg, pedram Differential Revision: 758
This commit is contained in:
parent
735847865c
commit
3aa17c7443
10 changed files with 75 additions and 17 deletions
|
@ -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(
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
|
|
@ -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');
|
||||
|
||||
|
|
|
@ -6,6 +6,7 @@
|
|||
|
||||
|
||||
|
||||
phutil_require_module('phabricator', 'aphront/request');
|
||||
phutil_require_module('phabricator', 'infrastructure/celerity/api');
|
||||
|
||||
phutil_require_module('phutil', 'markup');
|
||||
|
|
|
@ -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(
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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');
|
||||
|
|
|
@ -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(
|
||||
|
|
|
@ -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');
|
||||
|
|
|
@ -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);
|
||||
});
|
||||
});
|
||||
|
|
Loading…
Reference in a new issue