Page MenuHomestyx hydra

No OneTemporary

diff --git a/src/applications/auth/constants/PhabricatorCookies.php b/src/applications/auth/constants/PhabricatorCookies.php
index 19e27a1591..0bb34569f3 100644
--- a/src/applications/auth/constants/PhabricatorCookies.php
+++ b/src/applications/auth/constants/PhabricatorCookies.php
@@ -1,48 +1,130 @@
<?php
/**
* Consolidates Phabricator application cookies, including registration
* and session management.
+ *
+ * @task next Next URI Cookie
*/
final class PhabricatorCookies extends Phobject {
/**
* Stores the login username for password authentication. This is just a
* display value for convenience, used to prefill the login form. It is not
* authoritative.
*/
const COOKIE_USERNAME = 'phusr';
/**
* Stores the user's current session ID. This is authoritative and establishes
* the user's identity.
*/
const COOKIE_SESSION = 'phsid';
/**
* Stores a secret used during new account registration to prevent an attacker
* from tricking a victim into registering an account which is linked to
* credentials the attacker controls.
*/
const COOKIE_REGISTRATION = 'phreg';
/**
* Stores a secret used during OAuth2 handshakes to prevent various attacks
* where an attacker hands a victim a URI corresponding to the middle of an
* OAuth2 workflow and we might otherwise do something sketchy. Particularly,
* this corresponds to the OAuth2 "code".
*/
const COOKIE_CLIENTID = 'phcid';
/**
* Stores the URI to redirect the user to after login. This allows users to
* visit a path like `/feed/`, be prompted to login, and then be redirected
* back to `/feed/` after the workflow completes.
*/
const COOKIE_NEXTURI = 'next_uri';
+
+/* -( Next URI Cookie )---------------------------------------------------- */
+
+
+ /**
+ * Set the Next URI cookie. We only write the cookie if it wasn't recently
+ * written, to avoid writing over a real URI with a bunch of "humans.txt"
+ * stuff. See T3793 for discussion.
+ *
+ * @param AphrontRequest Request to write to.
+ * @param string URI to write.
+ * @param bool Write this cookie even if we have a fresh
+ * cookie already.
+ * @return void
+ *
+ * @task next
+ */
+ public static function setNextURICookie(
+ AphrontRequest $request,
+ $next_uri,
+ $force = false) {
+
+ if (!$force) {
+ $cookie_value = $request->getCookie(self::COOKIE_NEXTURI);
+ list($set_at, $current_uri) = self::parseNextURICookie($cookie_value);
+
+ // If the cookie was set within the last 2 minutes, don't overwrite it.
+ // Primarily, this prevents browser requests for resources which do not
+ // exist (like "humans.txt" and various icons) from overwriting a normal
+ // URI like "/feed/".
+ if ($set_at > (time() - 120)) {
+ return;
+ }
+ }
+
+ $new_value = time().','.$next_uri;
+ $request->setCookie(self::COOKIE_NEXTURI, $new_value);
+ }
+
+
+ /**
+ * Read the URI out of the Next URI cookie.
+ *
+ * @param AphrontRequest Request to examine.
+ * @return string|null Next URI cookie's URI value.
+ *
+ * @task next
+ */
+ public static function getNextURICookie(AphrontRequest $request) {
+ $cookie_value = $request->getCookie(self::COOKIE_NEXTURI);
+ list($set_at, $next_uri) = self::parseNExtURICookie($cookie_value);
+
+ return $next_uri;
+ }
+
+
+ /**
+ * Parse a Next URI cookie into its components.
+ *
+ * @param string Raw cookie value.
+ * @return list<string> List of timestamp and URI.
+ *
+ * @task next
+ */
+ private static function parseNextURICookie($cookie) {
+ // Old cookies look like: /uri
+ // New cookies look like: timestamp,/uri
+
+ if (!strlen($cookie)) {
+ return null;
+ }
+
+ if (strpos($cookie, ',') !== false) {
+ list($timestamp, $uri) = explode(',', $cookie, 2);
+ return array((int)$timestamp, $uri);
+ }
+
+ return array(0, $cookie);
+ }
+
}
diff --git a/src/applications/auth/controller/PhabricatorAuthStartController.php b/src/applications/auth/controller/PhabricatorAuthStartController.php
index 223acaa757..3c1cb7491d 100644
--- a/src/applications/auth/controller/PhabricatorAuthStartController.php
+++ b/src/applications/auth/controller/PhabricatorAuthStartController.php
@@ -1,215 +1,214 @@
<?php
final class PhabricatorAuthStartController
extends PhabricatorAuthController {
public function shouldRequireLogin() {
return false;
}
public function processRequest() {
$request = $this->getRequest();
$viewer = $request->getUser();
if ($viewer->isLoggedIn()) {
// Kick the user home if they are already logged in.
return id(new AphrontRedirectResponse())->setURI('/');
}
if ($request->isAjax()) {
return $this->processAjaxRequest();
}
if ($request->isConduit()) {
return $this->processConduitRequest();
}
// If the user gets this far, they aren't logged in, so if they have a
// user session token we can conclude that it's invalid: if it was valid,
// they'd have been logged in above and never made it here. Try to clear
// it and warn the user they may need to nuke their cookies.
$session_token = $request->getCookie(PhabricatorCookies::COOKIE_SESSION);
if (strlen($session_token)) {
$kind = PhabricatorAuthSessionEngine::getSessionKindFromToken(
$session_token);
switch ($kind) {
case PhabricatorAuthSessionEngine::KIND_ANONYMOUS:
// If this is an anonymous session. It's expected that they won't
// be logged in, so we can just continue.
break;
default:
// The session cookie is invalid, so clear it.
$request->clearCookie(PhabricatorCookies::COOKIE_USERNAME);
$request->clearCookie(PhabricatorCookies::COOKIE_SESSION);
return $this->renderError(
pht(
"Your login session is invalid. Try reloading the page and ".
"logging in again. If that does not work, clear your browser ".
"cookies."));
}
}
$providers = PhabricatorAuthProvider::getAllEnabledProviders();
foreach ($providers as $key => $provider) {
if (!$provider->shouldAllowLogin()) {
unset($providers[$key]);
}
}
if (!$providers) {
if ($this->isFirstTimeSetup()) {
// If this is a fresh install, let the user register their admin
// account.
return id(new AphrontRedirectResponse())
->setURI($this->getApplicationURI('/register/'));
}
return $this->renderError(
pht(
"This Phabricator install is not configured with any enabled ".
"authentication providers which can be used to log in. If you ".
"have accidentally locked yourself out by disabling all providers, ".
"you can use `phabricator/bin/auth recover <username>` to ".
"recover access to an administrative account."));
}
$next_uri = $request->getStr('next');
if (!$next_uri) {
$next_uri_path = $this->getRequest()->getPath();
if ($next_uri_path == '/auth/start/') {
$next_uri = '/';
} else {
$next_uri = $this->getRequest()->getRequestURI();
}
}
if (!$request->isFormPost()) {
- $request->setCookie(
- PhabricatorCookies::COOKIE_NEXTURI,
- $next_uri);
+ PhabricatorCookies::setNextURICookie($request, $next_uri);
+
$request->setCookie(
PhabricatorCookies::COOKIE_CLIENTID,
Filesystem::readRandomCharacters(16));
}
$not_buttons = array();
$are_buttons = array();
$providers = msort($providers, 'getLoginOrder');
foreach ($providers as $provider) {
if ($provider->isLoginFormAButton()) {
$are_buttons[] = $provider->buildLoginForm($this);
} else {
$not_buttons[] = $provider->buildLoginForm($this);
}
}
$out = array();
$out[] = $not_buttons;
if ($are_buttons) {
require_celerity_resource('auth-css');
foreach ($are_buttons as $key => $button) {
$are_buttons[$key] = phutil_tag(
'div',
array(
'class' => 'phabricator-login-button mmb',
),
$button);
}
// If we only have one button, add a second pretend button so that we
// always have two columns. This makes it easier to get the alignments
// looking reasonable.
if (count($are_buttons) == 1) {
$are_buttons[] = null;
}
$button_columns = id(new AphrontMultiColumnView())
->setFluidLayout(true);
$are_buttons = array_chunk($are_buttons, ceil(count($are_buttons) / 2));
foreach ($are_buttons as $column) {
$button_columns->addColumn($column);
}
$out[] = phutil_tag(
'div',
array(
'class' => 'phabricator-login-buttons',
),
$button_columns);
}
$login_message = PhabricatorEnv::getEnvConfig('auth.login-message');
$login_message = phutil_safe_html($login_message);
$crumbs = $this->buildApplicationCrumbs();
$crumbs->addTextCrumb(pht('Login'));
return $this->buildApplicationPage(
array(
$crumbs,
$login_message,
$out,
),
array(
'title' => pht('Login to Phabricator'),
'device' => true,
));
}
private function processAjaxRequest() {
$request = $this->getRequest();
$viewer = $request->getUser();
// We end up here if the user clicks a workflow link that they need to
// login to use. We give them a dialog saying "You need to login...".
if ($request->isDialogFormPost()) {
return id(new AphrontRedirectResponse())->setURI(
$request->getRequestURI());
}
$dialog = new AphrontDialogView();
$dialog->setUser($viewer);
$dialog->setTitle(pht('Login Required'));
$dialog->appendChild(pht('You must login to continue.'));
$dialog->addSubmitButton(pht('Login'));
$dialog->addCancelButton('/');
return id(new AphrontDialogResponse())->setDialog($dialog);
}
private function processConduitRequest() {
$request = $this->getRequest();
$viewer = $request->getUser();
// A common source of errors in Conduit client configuration is getting
// the request path wrong. The client will end up here, so make some
// effort to give them a comprehensible error message.
$request_path = $this->getRequest()->getPath();
$conduit_path = '/api/<method>';
$example_path = '/api/conduit.ping';
$message = pht(
'ERROR: You are making a Conduit API request to "%s", but the correct '.
'HTTP request path to use in order to access a COnduit method is "%s" '.
'(for example, "%s"). Check your configuration.',
$request_path,
$conduit_path,
$example_path);
return id(new AphrontPlainTextResponse())->setContent($message);
}
protected function renderError($message) {
return $this->renderErrorPage(
pht('Authentication Failure'),
array($message));
}
}
diff --git a/src/applications/auth/controller/PhabricatorAuthValidateController.php b/src/applications/auth/controller/PhabricatorAuthValidateController.php
index 8454ba24a2..18524163e0 100644
--- a/src/applications/auth/controller/PhabricatorAuthValidateController.php
+++ b/src/applications/auth/controller/PhabricatorAuthValidateController.php
@@ -1,73 +1,73 @@
<?php
final class PhabricatorAuthValidateController
extends PhabricatorAuthController {
public function shouldRequireLogin() {
return false;
}
public function processRequest() {
$request = $this->getRequest();
$viewer = $request->getUser();
$failures = array();
if (!strlen($request->getStr('expect'))) {
return $this->renderErrors(
array(
pht(
'Login validation is missing expected parameter ("%s").',
'phusr')));
}
$expect_phusr = $request->getStr('expect');
$actual_phusr = $request->getCookie(PhabricatorCookies::COOKIE_USERNAME);
if ($actual_phusr != $expect_phusr) {
if ($actual_phusr) {
$failures[] = pht(
"Attempted to set '%s' cookie to '%s', but your browser sent back ".
"a cookie with the value '%s'. Clear your browser's cookies and ".
"try again.",
'phusr',
$expect_phusr,
$actual_phusr);
} else {
$failures[] = pht(
"Attempted to set '%s' cookie to '%s', but your browser did not ".
"accept the cookie. Check that cookies are enabled, clear them, ".
"and try again.",
'phusr',
$expect_phusr);
}
}
if (!$failures) {
if (!$viewer->getPHID()) {
$failures[] = pht(
"Login cookie was set correctly, but your login session is not ".
"valid. Try clearing cookies and logging in again.");
}
}
if ($failures) {
return $this->renderErrors($failures);
}
- $next = $request->getCookie(PhabricatorCookies::COOKIE_NEXTURI);
+ $next = PhabricatorCookies::getNextURICookie($request);
$request->clearCookie(PhabricatorCookies::COOKIE_NEXTURI);
if (!PhabricatorEnv::isValidLocalWebResource($next)) {
$next = '/';
}
return id(new AphrontRedirectResponse())->setURI($next);
}
private function renderErrors(array $messages) {
return $this->renderErrorPage(
pht('Login Failure'),
$messages);
}
}
diff --git a/src/applications/auth/controller/PhabricatorEmailTokenController.php b/src/applications/auth/controller/PhabricatorEmailTokenController.php
index f1288761be..b4f998113e 100644
--- a/src/applications/auth/controller/PhabricatorEmailTokenController.php
+++ b/src/applications/auth/controller/PhabricatorEmailTokenController.php
@@ -1,93 +1,93 @@
<?php
final class PhabricatorEmailTokenController
extends PhabricatorAuthController {
private $token;
public function shouldRequireLogin() {
return false;
}
public function willProcessRequest(array $data) {
$this->token = $data['token'];
}
public function processRequest() {
$request = $this->getRequest();
$token = $this->token;
$email = $request->getStr('email');
// NOTE: We need to bind verification to **addresses**, not **users**,
// because we verify addresses when they're used to login this way, and if
// we have a user-based verification you can:
//
// - Add some address you do not own;
// - request a password reset;
// - change the URI in the email to the address you don't own;
// - login via the email link; and
// - get a "verified" address you don't control.
$target_email = id(new PhabricatorUserEmail())->loadOneWhere(
'address = %s',
$email);
$target_user = null;
if ($target_email) {
$target_user = id(new PhabricatorUser())->loadOneWhere(
'phid = %s',
$target_email->getUserPHID());
}
if (!$target_email ||
!$target_user ||
!$target_user->validateEmailToken($target_email, $token)) {
$view = new AphrontRequestFailureView();
$view->setHeader(pht('Unable to Login'));
$view->appendChild(phutil_tag('p', array(), pht(
'The authentication information in the link you clicked is '.
'invalid or out of date. Make sure you are copy-and-pasting the '.
'entire link into your browser. You can try again, or request '.
'a new email.')));
$view->appendChild(phutil_tag_div(
'aphront-failure-continue',
phutil_tag(
'a',
array('class' => 'button', 'href' => '/login/email/'),
pht('Send Another Email'))));
return $this->buildStandardPageResponse(
$view,
array(
'title' => pht('Login Failure'),
));
}
// Verify email so that clicking the link in the "Welcome" email is good
// enough, without requiring users to go through a second round of email
// verification.
$unguarded = AphrontWriteGuard::beginScopedUnguardedWrites();
$target_email->setIsVerified(1);
$target_email->save();
unset($unguarded);
$next = '/';
if (!PhabricatorAuthProviderPassword::getPasswordProvider()) {
$next = '/settings/panel/external/';
} else if (PhabricatorEnv::getEnvConfig('account.editable')) {
$next = (string)id(new PhutilURI('/settings/panel/password/'))
->setQueryParams(
array(
'token' => $token,
'email' => $email,
));
}
- $request->setCookie(PhabricatorCookies::COOKIE_NEXTURI, $next);
+ PhabricatorCookies::setNextURICookie($request, $next, $force = true);
return $this->loginUser($target_user);
}
}
diff --git a/src/applications/base/controller/Phabricator404Controller.php b/src/applications/base/controller/Phabricator404Controller.php
index e47c51fd4b..1aa785a040 100644
--- a/src/applications/base/controller/Phabricator404Controller.php
+++ b/src/applications/base/controller/Phabricator404Controller.php
@@ -1,38 +1,9 @@
<?php
final class Phabricator404Controller extends PhabricatorController {
- public function shouldRequireLogin() {
-
- // NOTE: See T2102 for discussion. When a logged-out user requests a page,
- // we give them a login form and set a `next_uri` cookie so we send them
- // back to the page they requested after they login. However, some browsers
- // or extensions request resources which may not exist (like
- // "apple-touch-icon.png" and "humans.txt") and these requests may overwrite
- // the stored "next_uri" after the login page loads. Our options for dealing
- // with this are all bad:
- //
- // 1. We can't put the URI in the form because some login methods (OAuth2)
- // issue redirects to third-party sites. After T1536 we might be able
- // to.
- // 2. We could set the cookie only if it doesn't exist, but then a user who
- // declines to login will end up in the wrong place if they later do
- // login.
- // 3. We can blacklist all the resources browsers request, but this is a
- // mess.
- // 4. We can just allow users to access the 404 page without login, so
- // requesting bad URIs doesn't set the cookie.
- //
- // This implements (4). The main downside is that an attacker can now detect
- // if a URI is routable (i.e., some application is installed) by testing for
- // 404 vs login. If possible, we should implement T1536 in such a way that
- // we can pass the next URI through the login process.
-
- return false;
- }
-
public function processRequest() {
return new Aphront404Response();
}
}

File Metadata

Mime Type
text/x-diff
Expires
Mon, Dec 1, 12:51 PM (1 d, 18 h)
Storage Engine
blob
Storage Format
Raw Data
Storage Handle
430459
Default Alt Text
(18 KB)

Event Timeline