2011-01-16 22:51:39 +01:00
|
|
|
<?php
|
|
|
|
|
|
|
|
/**
|
2012-01-04 06:57:45 +01:00
|
|
|
*
|
|
|
|
* @task data Accessing Request Data
|
|
|
|
*
|
2011-01-16 22:51:39 +01:00
|
|
|
* @group aphront
|
|
|
|
*/
|
2012-03-13 19:18:11 +01:00
|
|
|
final class AphrontRequest {
|
2011-01-16 22:51:39 +01:00
|
|
|
|
2012-05-07 15:17:00 +02:00
|
|
|
// NOTE: These magic request-type parameters are automatically included in
|
2013-02-13 03:46:01 +01:00
|
|
|
// certain requests (e.g., by phabricator_form(), JX.Request,
|
2012-05-07 15:17:00 +02:00
|
|
|
// JX.Workflow, and ConduitClient) and help us figure out what sort of
|
|
|
|
// response the client expects.
|
|
|
|
|
2011-01-16 22:51:39 +01:00
|
|
|
const TYPE_AJAX = '__ajax__';
|
|
|
|
const TYPE_FORM = '__form__';
|
2012-01-15 20:06:13 +01:00
|
|
|
const TYPE_CONDUIT = '__conduit__';
|
2012-05-07 15:17:00 +02:00
|
|
|
const TYPE_WORKFLOW = '__wflow__';
|
Add passthru to AphrontRequest
Summary:
For transaction interfaces, I want to prompt the user when they take an action that has no effect, e.g.:
Action Has No Effect
You can not close this task, because someone else has already closed it.
Do you want to post your comment anyway?
[Cancel] [Post Comment]
We already do this for Differential, but it's all hard-coded. T912 is an open task for fixing this for Maniphest.
To do this in a general way, I want to embed the entire request in the dialog as hidden inputs, then add a "__continue__" key and resubmit the form. The endpoint will read this key the second time through and apply what effects it can (e.g., just post a comment).
This adds a mechanism for getting all the request data, minus "magic" like __dialog__ and __csrf__. We need to jump through some hoops because of how PHP encodes arrays.
Test Plan: Ran unit tests, built "no effect" dialogs on top of this.
Reviewers: btrahan, vrana
Reviewed By: btrahan
CC: aran
Maniphest Tasks: T912, T2104
Differential Revision: https://secure.phabricator.com/D4158
2012-12-12 02:27:02 +01:00
|
|
|
const TYPE_CONTINUE = '__continue__';
|
2012-12-21 14:51:33 +01:00
|
|
|
const TYPE_PREVIEW = '__preview__';
|
2011-01-16 22:51:39 +01:00
|
|
|
|
|
|
|
private $host;
|
|
|
|
private $path;
|
|
|
|
private $requestData;
|
2011-01-26 22:21:12 +01:00
|
|
|
private $user;
|
2011-02-02 22:48:52 +01:00
|
|
|
private $applicationConfiguration;
|
2011-01-31 20:55:26 +01:00
|
|
|
|
2011-02-02 22:48:52 +01:00
|
|
|
final public function __construct($host, $path) {
|
|
|
|
$this->host = $host;
|
|
|
|
$this->path = $path;
|
2011-01-31 20:55:26 +01:00
|
|
|
}
|
|
|
|
|
2011-02-02 22:48:52 +01:00
|
|
|
final public function setApplicationConfiguration(
|
|
|
|
$application_configuration) {
|
|
|
|
$this->applicationConfiguration = $application_configuration;
|
|
|
|
return $this;
|
2011-01-31 20:55:26 +01:00
|
|
|
}
|
2011-01-16 22:51:39 +01:00
|
|
|
|
2011-02-02 22:48:52 +01:00
|
|
|
final public function getApplicationConfiguration() {
|
|
|
|
return $this->applicationConfiguration;
|
2011-01-16 22:51:39 +01:00
|
|
|
}
|
|
|
|
|
Move skins toward modularization
Summary:
Two high-level things happening here:
- We no longer ever need to put meta-UI (content creation, editing, notices, etc.) on live blog views, since this is all in Phame now. I pulled this out.
- On the other hand, I pushed more routing/control logic into Skins and made the root skin a Controller instead of a View. This simplifies some of the code above skins, and the theory behind this is that it gives us greater flexibility to, e.g., put a glue layer between Phame and Wordpress templates or whatever else, and allows skins to handle routing and thus add pages like "About" or "Bio".
- I added a basic skin below the root skin which is more like the old root skin and has standard rendering hooks.
- "Ten Eleven" is a play on the popular (default?) Wordpress themes called "Twenty Ten", "Twenty Eleven" and "Twenty Twelve".
Test Plan: Viewed live blog and live posts. They aren't pretty, but they don't have extraneous resources.
Reviewers: btrahan
Reviewed By: btrahan
CC: aran
Maniphest Tasks: T1373
Differential Revision: https://secure.phabricator.com/D3714
2012-10-17 17:36:25 +02:00
|
|
|
final public function setPath($path) {
|
|
|
|
$this->path = $path;
|
|
|
|
return $this;
|
|
|
|
}
|
|
|
|
|
2012-01-04 06:57:45 +01:00
|
|
|
final public function getPath() {
|
|
|
|
return $this->path;
|
|
|
|
}
|
|
|
|
|
|
|
|
final public function getHost() {
|
2012-10-22 19:49:06 +02:00
|
|
|
// 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();
|
2012-01-04 06:57:45 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/* -( Accessing Request Data )--------------------------------------------- */
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
* @task data
|
|
|
|
*/
|
2011-01-16 22:51:39 +01:00
|
|
|
final public function setRequestData(array $request_data) {
|
|
|
|
$this->requestData = $request_data;
|
|
|
|
return $this;
|
|
|
|
}
|
|
|
|
|
2012-01-04 06:57:45 +01:00
|
|
|
|
|
|
|
/**
|
|
|
|
* @task data
|
|
|
|
*/
|
2011-12-08 06:41:33 +01:00
|
|
|
final public function getRequestData() {
|
|
|
|
return $this->requestData;
|
|
|
|
}
|
|
|
|
|
2011-01-16 22:51:39 +01:00
|
|
|
|
2012-01-04 06:57:45 +01:00
|
|
|
/**
|
|
|
|
* @task data
|
|
|
|
*/
|
2011-01-16 22:51:39 +01:00
|
|
|
final public function getInt($name, $default = null) {
|
|
|
|
if (isset($this->requestData[$name])) {
|
|
|
|
return (int)$this->requestData[$name];
|
|
|
|
} else {
|
|
|
|
return $default;
|
Fix "reply" link in Differential
Summary:
Fixes the issue caused by rPa0af5b66437719dba6136579c051982ab275e6a0. Prior to
that patch, isCommentInNewFile() returned $comment->getIsNewFile(). While this
was often the wrong value, it came from the database and was the integer 1 if
true.
After the patch, the function returns 'true' as a boolean, which is passed to JS
and then back to PHP, interpreted as an integer, and evaluates to 0.
To avoid this issue in general, provide an isBool() method on AphrontRequest
which interprets this correctly.
I will also revert the revert of rPa0af5b66437719dba6136579c051982ab275e6a0 when
I land this.
Test Plan:
Clicked "reply" on the right hand side of a diff, got a right-hand-side inline
comment.
Reviewed By: rm
Reviewers: tuomaspelkonen, jungejason, aran, rm
CC: simpkins, aran, epriestley, rm
Differential Revision: 250
2011-05-07 19:42:40 +02:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2012-01-04 06:57:45 +01:00
|
|
|
|
|
|
|
/**
|
|
|
|
* @task data
|
|
|
|
*/
|
Fix "reply" link in Differential
Summary:
Fixes the issue caused by rPa0af5b66437719dba6136579c051982ab275e6a0. Prior to
that patch, isCommentInNewFile() returned $comment->getIsNewFile(). While this
was often the wrong value, it came from the database and was the integer 1 if
true.
After the patch, the function returns 'true' as a boolean, which is passed to JS
and then back to PHP, interpreted as an integer, and evaluates to 0.
To avoid this issue in general, provide an isBool() method on AphrontRequest
which interprets this correctly.
I will also revert the revert of rPa0af5b66437719dba6136579c051982ab275e6a0 when
I land this.
Test Plan:
Clicked "reply" on the right hand side of a diff, got a right-hand-side inline
comment.
Reviewed By: rm
Reviewers: tuomaspelkonen, jungejason, aran, rm
CC: simpkins, aran, epriestley, rm
Differential Revision: 250
2011-05-07 19:42:40 +02:00
|
|
|
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;
|
2011-01-16 22:51:39 +01:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2012-01-04 06:57:45 +01:00
|
|
|
|
|
|
|
/**
|
|
|
|
* @task data
|
|
|
|
*/
|
2011-01-16 22:51:39 +01:00
|
|
|
final public function getStr($name, $default = null) {
|
|
|
|
if (isset($this->requestData[$name])) {
|
2011-02-05 21:20:18 +01:00
|
|
|
$str = (string)$this->requestData[$name];
|
|
|
|
// Normalize newline craziness.
|
|
|
|
$str = str_replace(
|
|
|
|
array("\r\n", "\r"),
|
|
|
|
array("\n", "\n"),
|
|
|
|
$str);
|
|
|
|
return $str;
|
2011-01-16 22:51:39 +01:00
|
|
|
} else {
|
|
|
|
return $default;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2012-01-04 06:57:45 +01:00
|
|
|
|
|
|
|
/**
|
|
|
|
* @task data
|
|
|
|
*/
|
2011-01-26 02:40:21 +01:00
|
|
|
final public function getArr($name, $default = array()) {
|
2011-01-16 22:51:39 +01:00
|
|
|
if (isset($this->requestData[$name]) &&
|
|
|
|
is_array($this->requestData[$name])) {
|
|
|
|
return $this->requestData[$name];
|
|
|
|
} else {
|
|
|
|
return $default;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2012-01-04 06:57:45 +01:00
|
|
|
|
|
|
|
/**
|
|
|
|
* @task data
|
|
|
|
*/
|
|
|
|
final public function getStrList($name, $default = array()) {
|
|
|
|
if (!isset($this->requestData[$name])) {
|
|
|
|
return $default;
|
|
|
|
}
|
|
|
|
$list = $this->getStr($name);
|
2012-09-21 00:23:38 +02:00
|
|
|
$list = preg_split('/[\s,]+/', $list, $limit = -1, PREG_SPLIT_NO_EMPTY);
|
2012-01-04 06:57:45 +01:00
|
|
|
return $list;
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
* @task data
|
|
|
|
*/
|
2011-01-16 22:51:39 +01:00
|
|
|
final public function getExists($name) {
|
|
|
|
return array_key_exists($name, $this->requestData);
|
|
|
|
}
|
|
|
|
|
Modernize Macro application
Summary: Adds feed, email, notifications, comments, partial editing, subscriptions, enable/disable, flags and crumbs to Macro.
Test Plan:
{F26839}
{F26840}
{F26841}
{F26842}
{F26843}
{F26844}
{F26845}
Reviewers: vrana, btrahan, chad
Reviewed By: vrana
CC: aran
Maniphest Tasks: T2157, T175, T2104
Differential Revision: https://secure.phabricator.com/D4141
2012-12-11 23:01:03 +01:00
|
|
|
final public function getFileExists($name) {
|
|
|
|
return isset($_FILES[$name]) &&
|
|
|
|
(idx($_FILES[$name], 'error') !== UPLOAD_ERR_NO_FILE);
|
|
|
|
}
|
|
|
|
|
2011-01-16 22:51:39 +01:00
|
|
|
final public function isHTTPPost() {
|
|
|
|
return ($_SERVER['REQUEST_METHOD'] == 'POST');
|
|
|
|
}
|
|
|
|
|
|
|
|
final public function isAjax() {
|
|
|
|
return $this->getExists(self::TYPE_AJAX);
|
|
|
|
}
|
|
|
|
|
2012-05-07 15:17:00 +02:00
|
|
|
final public function isJavelinWorkflow() {
|
|
|
|
return $this->getExists(self::TYPE_WORKFLOW);
|
|
|
|
}
|
|
|
|
|
2012-01-15 20:06:13 +01:00
|
|
|
final public function isConduit() {
|
|
|
|
return $this->getExists(self::TYPE_CONDUIT);
|
|
|
|
}
|
|
|
|
|
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
2011-08-02 05:23:01 +02:00
|
|
|
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
|
|
|
|
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
2011-08-02 05:23:01 +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)) {
|
2013-02-10 00:01:57 +01:00
|
|
|
$token = self::getHTTPHeader(self::getCSRFHeaderName());
|
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
|
|
|
}
|
|
|
|
|
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
2011-08-02 05:23:01 +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) {
|
2011-09-01 18:29:33 +02:00
|
|
|
|
|
|
|
// 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}.)";
|
|
|
|
}
|
|
|
|
|
2013-02-01 18:34:06 +01:00
|
|
|
// 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)).";
|
|
|
|
}
|
|
|
|
|
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
|
|
|
// 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 ".
|
2012-05-25 01:36:43 +02:00
|
|
|
"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".
|
2011-09-01 18:29:33 +02:00
|
|
|
$more_info);
|
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
|
|
|
}
|
|
|
|
|
|
|
|
return true;
|
2011-01-16 22:51:39 +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
2011-08-02 05:23:01 +02:00
|
|
|
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) {
|
Provide a setting which forces all file views to be served from an alternate
domain
Summary:
See D758, D759.
- Provide a strongly recommended setting which permits configuration of an
alternate domain.
- Lock cookies down better: set them on the exact domain, and use SSL-only if
the configuration is HTTPS.
- Prevent Phabriator from setting cookies on other domains.
This assumes D759 will land, it is not effective without that change.
Test Plan:
- Attempted to login from a different domain and was rejected.
- Logged out, logged back in normally.
- Put install in setup mode and verified it revealed a warning.
- Configured an alterate domain.
- Tried to view an image with an old URI, got a 400.
- Went to /files/ and verified links rendered to the alternate domain.
- Viewed an alternate domain file.
- Tried to view an alternate domain file without the secret key, got a 404.
Reviewers: andrewjcg, erling, aran, tuomaspelkonen, jungejason, codeblock
CC: aran
Differential Revision: 760
2011-08-02 07:24:00 +02:00
|
|
|
|
2013-01-22 22:57:02 +01:00
|
|
|
$is_secure = false;
|
Provide a setting which forces all file views to be served from an alternate
domain
Summary:
See D758, D759.
- Provide a strongly recommended setting which permits configuration of an
alternate domain.
- Lock cookies down better: set them on the exact domain, and use SSL-only if
the configuration is HTTPS.
- Prevent Phabriator from setting cookies on other domains.
This assumes D759 will land, it is not effective without that change.
Test Plan:
- Attempted to login from a different domain and was rejected.
- Logged out, logged back in normally.
- Put install in setup mode and verified it revealed a warning.
- Configured an alterate domain.
- Tried to view an image with an old URI, got a 400.
- Went to /files/ and verified links rendered to the alternate domain.
- Viewed an alternate domain file.
- Tried to view an alternate domain file without the secret key, got a 404.
Reviewers: andrewjcg, erling, aran, tuomaspelkonen, jungejason, codeblock
CC: aran
Differential Revision: 760
2011-08-02 07:24:00 +02:00
|
|
|
|
2013-01-22 22:57:02 +01:00
|
|
|
// 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.
|
Provide a setting which forces all file views to be served from an alternate
domain
Summary:
See D758, D759.
- Provide a strongly recommended setting which permits configuration of an
alternate domain.
- Lock cookies down better: set them on the exact domain, and use SSL-only if
the configuration is HTTPS.
- Prevent Phabriator from setting cookies on other domains.
This assumes D759 will land, it is not effective without that change.
Test Plan:
- Attempted to login from a different domain and was rejected.
- Logged out, logged back in normally.
- Put install in setup mode and verified it revealed a warning.
- Configured an alterate domain.
- Tried to view an image with an old URI, got a 400.
- Went to /files/ and verified links rendered to the alternate domain.
- Viewed an alternate domain file.
- Tried to view an alternate domain file without the secret key, got a 404.
Reviewers: andrewjcg, erling, aran, tuomaspelkonen, jungejason, codeblock
CC: aran
Differential Revision: 760
2011-08-02 07:24:00 +02:00
|
|
|
$base_uri = PhabricatorEnv::getEnvConfig('phabricator.base-uri');
|
2013-01-22 22:57:02 +01:00
|
|
|
if ($base_uri) {
|
|
|
|
$base_uri = new PhutilURI($base_uri);
|
Provide a setting which forces all file views to be served from an alternate
domain
Summary:
See D758, D759.
- Provide a strongly recommended setting which permits configuration of an
alternate domain.
- Lock cookies down better: set them on the exact domain, and use SSL-only if
the configuration is HTTPS.
- Prevent Phabriator from setting cookies on other domains.
This assumes D759 will land, it is not effective without that change.
Test Plan:
- Attempted to login from a different domain and was rejected.
- Logged out, logged back in normally.
- Put install in setup mode and verified it revealed a warning.
- Configured an alterate domain.
- Tried to view an image with an old URI, got a 400.
- Went to /files/ and verified links rendered to the alternate domain.
- Viewed an alternate domain file.
- Tried to view an alternate domain file without the secret key, got a 404.
Reviewers: andrewjcg, erling, aran, tuomaspelkonen, jungejason, codeblock
CC: aran
Differential Revision: 760
2011-08-02 07:24:00 +02:00
|
|
|
|
2013-01-22 22:57:02 +01:00
|
|
|
$base_domain = $base_uri->getDomain();
|
|
|
|
$base_protocol = $base_uri->getProtocol();
|
Provide a setting which forces all file views to be served from an alternate
domain
Summary:
See D758, D759.
- Provide a strongly recommended setting which permits configuration of an
alternate domain.
- Lock cookies down better: set them on the exact domain, and use SSL-only if
the configuration is HTTPS.
- Prevent Phabriator from setting cookies on other domains.
This assumes D759 will land, it is not effective without that change.
Test Plan:
- Attempted to login from a different domain and was rejected.
- Logged out, logged back in normally.
- Put install in setup mode and verified it revealed a warning.
- Configured an alterate domain.
- Tried to view an image with an old URI, got a 400.
- Went to /files/ and verified links rendered to the alternate domain.
- Viewed an alternate domain file.
- Tried to view an alternate domain file without the secret key, got a 404.
Reviewers: andrewjcg, erling, aran, tuomaspelkonen, jungejason, codeblock
CC: aran
Differential Revision: 760
2011-08-02 07:24:00 +02:00
|
|
|
|
2013-01-22 22:57:02 +01:00
|
|
|
$host = $this->getHost();
|
2012-10-01 06:19:35 +02:00
|
|
|
|
2013-01-22 22:57:02 +01:00
|
|
|
if ($base_domain != $host) {
|
|
|
|
throw new Exception(
|
|
|
|
"This install of Phabricator is configured as '{$base_domain}' but ".
|
|
|
|
"you are accessing it via '{$host}'. Access Phabricator via ".
|
|
|
|
"the primary configured domain.");
|
|
|
|
}
|
|
|
|
|
|
|
|
$is_secure = ($base_protocol == 'https');
|
|
|
|
} else {
|
|
|
|
$base_uri = new PhutilURI(PhabricatorEnv::getRequestBaseURI());
|
|
|
|
$base_domain = $base_uri->getDomain();
|
Provide a setting which forces all file views to be served from an alternate
domain
Summary:
See D758, D759.
- Provide a strongly recommended setting which permits configuration of an
alternate domain.
- Lock cookies down better: set them on the exact domain, and use SSL-only if
the configuration is HTTPS.
- Prevent Phabriator from setting cookies on other domains.
This assumes D759 will land, it is not effective without that change.
Test Plan:
- Attempted to login from a different domain and was rejected.
- Logged out, logged back in normally.
- Put install in setup mode and verified it revealed a warning.
- Configured an alterate domain.
- Tried to view an image with an old URI, got a 400.
- Went to /files/ and verified links rendered to the alternate domain.
- Viewed an alternate domain file.
- Tried to view an alternate domain file without the secret key, got a 404.
Reviewers: andrewjcg, erling, aran, tuomaspelkonen, jungejason, codeblock
CC: aran
Differential Revision: 760
2011-08-02 07:24:00 +02:00
|
|
|
}
|
|
|
|
|
2011-01-26 22:21:12 +01:00
|
|
|
if ($expire === null) {
|
|
|
|
$expire = time() + (60 * 60 * 24 * 365 * 5);
|
|
|
|
}
|
Provide a setting which forces all file views to be served from an alternate
domain
Summary:
See D758, D759.
- Provide a strongly recommended setting which permits configuration of an
alternate domain.
- Lock cookies down better: set them on the exact domain, and use SSL-only if
the configuration is HTTPS.
- Prevent Phabriator from setting cookies on other domains.
This assumes D759 will land, it is not effective without that change.
Test Plan:
- Attempted to login from a different domain and was rejected.
- Logged out, logged back in normally.
- Put install in setup mode and verified it revealed a warning.
- Configured an alterate domain.
- Tried to view an image with an old URI, got a 400.
- Went to /files/ and verified links rendered to the alternate domain.
- Viewed an alternate domain file.
- Tried to view an alternate domain file without the secret key, got a 404.
Reviewers: andrewjcg, erling, aran, tuomaspelkonen, jungejason, codeblock
CC: aran
Differential Revision: 760
2011-08-02 07:24:00 +02:00
|
|
|
|
2011-08-31 01:41:18 +02:00
|
|
|
setcookie(
|
|
|
|
$name,
|
|
|
|
$value,
|
|
|
|
$expire,
|
|
|
|
$path = '/',
|
|
|
|
$base_domain,
|
|
|
|
$is_secure,
|
|
|
|
$http_only = true);
|
2012-04-03 03:35:09 +02:00
|
|
|
|
|
|
|
return $this;
|
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__']);
|
2012-03-28 01:53:47 +02:00
|
|
|
$path = phutil_escape_uri($this->getPath());
|
|
|
|
return id(new PhutilURI($path))->setQueryParams($get);
|
2011-02-06 01:43:28 +01:00
|
|
|
}
|
|
|
|
|
2011-02-06 07:36:21 +01:00
|
|
|
final public function isDialogFormPost() {
|
|
|
|
return $this->isFormPost() && $this->getStr('__dialog__');
|
|
|
|
}
|
|
|
|
|
Track content sources (email, web, conduit, mobile) for replies
Summary:
When an object is updated, record the content source for the update. This mostly
isn't terribly useful but one concrete thing I want to do with it is let admins
audit via-email replies more easily since there are a bunch of options which let
you do hyjinx if you intentionally configure them insecurely. I think having a
little more auditability around this feature is generally good. At some point
I'm going to turn this into a link admins can click to see details.
It also allows us to see how frequently different mechanisms are used, and lets
you see if someone is at their desk or on a mobile or whatever, at least
indirectly.
The "tablet" and "mobile" sources are currently unused but I figured I'd throw
them in anyway. SMS support should definitely happen at some point.
Not 100% sure about the design for this, I might change it to plain text at some
point.
Test Plan: Updated objects and saw update sources rendered.
Reviewers: jungejason, tuomaspelkonen, aran
Reviewed By: jungejason
CC: aran, epriestley, jungejason
Differential Revision: 844
2011-08-22 19:25:45 +02:00
|
|
|
final public function getRemoteAddr() {
|
|
|
|
return $_SERVER['REMOTE_ADDR'];
|
|
|
|
}
|
|
|
|
|
2012-09-04 18:56:30 +02:00
|
|
|
public function isHTTPS() {
|
|
|
|
if (empty($_SERVER['HTTPS'])) {
|
|
|
|
return false;
|
|
|
|
}
|
|
|
|
if (!strcasecmp($_SERVER["HTTPS"], "off")) {
|
|
|
|
return false;
|
|
|
|
}
|
|
|
|
return true;
|
|
|
|
}
|
|
|
|
|
Add passthru to AphrontRequest
Summary:
For transaction interfaces, I want to prompt the user when they take an action that has no effect, e.g.:
Action Has No Effect
You can not close this task, because someone else has already closed it.
Do you want to post your comment anyway?
[Cancel] [Post Comment]
We already do this for Differential, but it's all hard-coded. T912 is an open task for fixing this for Maniphest.
To do this in a general way, I want to embed the entire request in the dialog as hidden inputs, then add a "__continue__" key and resubmit the form. The endpoint will read this key the second time through and apply what effects it can (e.g., just post a comment).
This adds a mechanism for getting all the request data, minus "magic" like __dialog__ and __csrf__. We need to jump through some hoops because of how PHP encodes arrays.
Test Plan: Ran unit tests, built "no effect" dialogs on top of this.
Reviewers: btrahan, vrana
Reviewed By: btrahan
CC: aran
Maniphest Tasks: T912, T2104
Differential Revision: https://secure.phabricator.com/D4158
2012-12-12 02:27:02 +01:00
|
|
|
public function isContinueRequest() {
|
|
|
|
return $this->isFormPost() && $this->getStr('__continue__');
|
|
|
|
}
|
|
|
|
|
2012-12-21 14:51:33 +01:00
|
|
|
public function isPreviewRequest() {
|
|
|
|
return $this->isFormPost() && $this->getStr('__preview__');
|
|
|
|
}
|
|
|
|
|
Add passthru to AphrontRequest
Summary:
For transaction interfaces, I want to prompt the user when they take an action that has no effect, e.g.:
Action Has No Effect
You can not close this task, because someone else has already closed it.
Do you want to post your comment anyway?
[Cancel] [Post Comment]
We already do this for Differential, but it's all hard-coded. T912 is an open task for fixing this for Maniphest.
To do this in a general way, I want to embed the entire request in the dialog as hidden inputs, then add a "__continue__" key and resubmit the form. The endpoint will read this key the second time through and apply what effects it can (e.g., just post a comment).
This adds a mechanism for getting all the request data, minus "magic" like __dialog__ and __csrf__. We need to jump through some hoops because of how PHP encodes arrays.
Test Plan: Ran unit tests, built "no effect" dialogs on top of this.
Reviewers: btrahan, vrana
Reviewed By: btrahan
CC: aran
Maniphest Tasks: T912, T2104
Differential Revision: https://secure.phabricator.com/D4158
2012-12-12 02:27:02 +01:00
|
|
|
/**
|
|
|
|
* 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<string, string> Original request parameters.
|
|
|
|
*/
|
|
|
|
public function getPassthroughRequestParameters() {
|
2012-12-21 14:57:14 +01:00
|
|
|
return self::flattenData($this->getPassthruRequestData());
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Get request data other than "magic" parameters.
|
|
|
|
*
|
|
|
|
* @return dict<string, wild> Request data, with magic filtered out.
|
|
|
|
*/
|
|
|
|
public function getPassthroughRequestData() {
|
|
|
|
$data = $this->getRequestData();
|
Add passthru to AphrontRequest
Summary:
For transaction interfaces, I want to prompt the user when they take an action that has no effect, e.g.:
Action Has No Effect
You can not close this task, because someone else has already closed it.
Do you want to post your comment anyway?
[Cancel] [Post Comment]
We already do this for Differential, but it's all hard-coded. T912 is an open task for fixing this for Maniphest.
To do this in a general way, I want to embed the entire request in the dialog as hidden inputs, then add a "__continue__" key and resubmit the form. The endpoint will read this key the second time through and apply what effects it can (e.g., just post a comment).
This adds a mechanism for getting all the request data, minus "magic" like __dialog__ and __csrf__. We need to jump through some hoops because of how PHP encodes arrays.
Test Plan: Ran unit tests, built "no effect" dialogs on top of this.
Reviewers: btrahan, vrana
Reviewed By: btrahan
CC: aran
Maniphest Tasks: T912, T2104
Differential Revision: https://secure.phabricator.com/D4158
2012-12-12 02:27:02 +01:00
|
|
|
|
|
|
|
// 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<string, wild> Data to flatten.
|
|
|
|
* @return dict<string, string> 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;
|
|
|
|
}
|
|
|
|
|
|
|
|
|
2013-02-10 00:01:57 +01:00
|
|
|
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);
|
|
|
|
}
|
|
|
|
|
2011-01-16 22:51:39 +01:00
|
|
|
}
|