Page MenuHomePhabricator

3stage.patch

Authored By
bzimport
Nov 22 2014, 12:58 AM
Size
13 KB
Referenced Files
None
Subscribers
None

3stage.patch

From 456e712d199243b65abb38de503fb5ef4a294649 Mon Sep 17 00:00:00 2001
From: csteipp <csteipp@wikimedia.org>
Date: Thu, 11 Oct 2012 08:48:41 -0700
Subject: [PATCH] (bug 40747) Fix login csrf
Have attacked wikis call back to the authenticating wiki to setup the
CentralAuth session in memcached, to prevent login CSRF.
SpecialAutoLogin now redirects the browser to the central wiki, which
redirects the user back to the attached wiki to finish setting cookies
and show the login icon. Each stage of the login uses a unique token,
and compares the requests action with the current session / username.
---
CentralAuthHooks.php | 7 +-
CentralAuthUser.php | 45 +++++++---
specials/SpecialAutoLogin.php | 199 ++++++++++++++++++++++++++++++++++-------
3 files changed, 202 insertions(+), 49 deletions(-)
diff --git a/CentralAuthHooks.php b/CentralAuthHooks.php
index cd63b49..530d880 100644
--- a/CentralAuthHooks.php
+++ b/CentralAuthHooks.php
@@ -172,7 +172,8 @@ class CentralAuthHooks {
'userName' => $user->getName(),
'token' => $centralUser->getAuthToken(),
'remember' => $user->getOption( 'rememberpassword' ),
- 'wiki' => $wiki
+ 'wiki' => $wiki,
+ 'srcwiki' => wfWikiID()
);
$loginToken = MWCryptRand::generateHex( 32 );
@@ -181,7 +182,7 @@ class CentralAuthHooks {
$wiki = WikiMap::getWiki( $wiki );
// Use WikiReference::getFullUrl(), returns a protocol-relative URL if needed
- $url = wfAppendQuery( $wiki->getFullUrl( 'Special:AutoLogin' ), "token=$loginToken" );
+ $url = wfAppendQuery( $wiki->getFullUrl( 'Special:AutoLogin' ), "token=$loginToken&stage=1" );
$inject_html .= Xml::element( 'img',
array(
@@ -702,7 +703,7 @@ class CentralAuthHooks {
} else {
$remember = false;
}
- $centralUser->setGlobalCookies( $remember );
+ $centralUser->setGlobalCookies( $remember, true );
return true;
}
diff --git a/CentralAuthUser.php b/CentralAuthUser.php
index 9440cfb..aefdd7f 100644
--- a/CentralAuthUser.php
+++ b/CentralAuthUser.php
@@ -1809,7 +1809,7 @@ class CentralAuthUser extends AuthPluginUser {
*/
static function setCookie( $name, $value, $exp = -1 ) {
global $wgCentralAuthCookiePrefix, $wgCentralAuthCookieDomain, $wgCookieSecure,
- $wgCookieExpiration, $wgCookieHttpOnly;
+ $wgCookieExpiration, $wgCookieHttpOnly, $wgCookiePath;
if ( $exp == -1 ) {
$exp = time() + $wgCookieExpiration;
@@ -1823,7 +1823,7 @@ class CentralAuthUser extends AuthPluginUser {
setcookie( $wgCentralAuthCookiePrefix . $name,
$value,
$exp,
- '/',
+ $wgCookiePath,
$wgCentralAuthCookieDomain,
$wgCookieSecure,
$wgCookieHttpOnly );
@@ -1842,29 +1842,43 @@ class CentralAuthUser extends AuthPluginUser {
* Called on login.
*
* @param $remember Bool|User
+ * @param $refesh Bool Refresh the SessionID when setting cookie
* @return string Session ID
*/
- function setGlobalCookies( $remember = false ) {
+ function setGlobalCookies( $remember = false, $refresh = false ) {
if ( $remember instanceof User ) {
// Older code passed a user object here. Be kind and do what they meant to do.
$remember = $remember->getOption( 'rememberpassword' );
}
+ self::setCookie( 'User', $this->mName );
+
+ if ( $remember ) {
+ self::setCookie( 'Token', $this->getAuthToken() );
+ } else {
+ $this->clearCookie( 'Token' );
+ }
+
+ $session = $this->getSessionDataArray();
+
+ return self::setSession( $session, $refresh );
+ }
+
+ /**
+ * Get the array of session data, representing this user
+ *
+ * @return array
+ */
+ function getSessionDataArray() {
$session = array();
$exp = time() + 86400;
$session['user'] = $this->mName;
- self::setCookie( 'User', $this->mName );
$session['token'] = $this->getAuthToken();
$session['expiry'] = $exp;
$session['auto-create-blacklist'] = array();
- if ( $remember ) {
- self::setCookie( 'Token', $this->getAuthToken() );
- } else {
- $this->clearCookie( 'Token' );
- }
- return self::setSession( $session );
+ return $session;
}
/**
@@ -2178,15 +2192,22 @@ class CentralAuthUser extends AuthPluginUser {
* Set the central session data
*
* @param $data Array
+ * @param $refresh Bool - Force a refresh of the session id (prevent session
+ * fixation). Should be called once each time user logs in.
+ * @param $foreignId String - SessionId key to populate (e.g., to set
+ * session data for foreign wiki)
* @return string
*/
- static function setSession( $data ) {
+ static function setSession( $data, $refresh = false, $foreignId = false ) {
global $wgCentralAuthCookies, $wgCentralAuthCookiePrefix;
global $wgMemc;
if ( !$wgCentralAuthCookies ) {
return null;
}
- if ( !isset( $_COOKIE[$wgCentralAuthCookiePrefix . 'Session'] ) ) {
+
+ if ( $foreignId !== false ) {
+ $id = $foreignId;
+ } elseif ( $refresh || !isset( $_COOKIE[$wgCentralAuthCookiePrefix . 'Session'] ) ) {
$id = MWCryptRand::generateHex( 32 );
self::setCookie( 'Session', $id, 0 );
} else {
diff --git a/specials/SpecialAutoLogin.php b/specials/SpecialAutoLogin.php
index 87b08a2..0b4ea3a 100644
--- a/specials/SpecialAutoLogin.php
+++ b/specials/SpecialAutoLogin.php
@@ -3,6 +3,12 @@
/**
* Unlisted Special page to set requisite cookies for being logged into this wiki.
*
+ * To prevent CSRF of the login or logout:
+ * - Logout requires an auth token
+ * - Login is done in 3 phases:
+ * - stage 1 - remote wiki: sets central session cookie, passes session id to stage 2
+ * - stage 2 - central wiki: uses session_id to setup cached user data
+ * - stage 3 - remote wiki: Sets Username/Token cookies for remember-me
* @ingroup Extensions
*/
class SpecialAutoLogin extends UnlistedSpecialPage {
@@ -13,71 +19,196 @@ class SpecialAutoLogin extends UnlistedSpecialPage {
function execute( $par ) {
global $wgMemc;
- $tempToken = $this->getRequest()->getVal( 'token' );
+ $token = $this->getRequest()->getVal( 'token' );
+ $stage = $this->getRequest()->getInt( 'stage', 1 );
$logout = $this->getRequest()->getBool( 'logout' );
- # Don't cache error messages
$this->getOutput()->enableClientCache( false );
- if ( strlen( $tempToken ) == 0 ) {
+ if ( strlen( $token ) == 0 ) {
$this->setHeaders();
$this->getOutput()->addWikiMsg( 'centralauth-autologin-desc' );
return;
}
- $key = CentralAuthUser::memcKey( 'login-token', $tempToken );
- $data = $wgMemc->get( $key );
- $wgMemc->delete( $key );
+ $tokenKey = CentralAuthUser::memcKey( 'login-token', $token );
+ $tokenData = $wgMemc->get( $tokenKey );
+ $wgMemc->delete( $tokenKey );
- if ( !$data ) {
- $msg = 'Token is invalid or has expired';
- wfDebug( __METHOD__ . ": $msg\n" );
+ if ( $tokenData === false || count( $tokenData ) < 1 ) {
+ //Invalid or already deleted token
+ wfDebug( __METHOD__ . " Recieved invalid token ($token, $stage, $logout)\n" );
$this->setHeaders();
- $this->getOutput()->addWikiText( $msg );
+ $this->getOutput()->addWikiMsg( 'centralauth-token-mismatch' );
return;
}
- $userName = $data['userName'];
- $token = $data['token'];
- $remember = $data['remember'];
- if ( $data['wiki'] != wfWikiID() ) {
- $msg = 'Bad token (wrong wiki)';
- wfDebug( __METHOD__ . ": $msg\n" );
- $this->setHeaders();
- $this->getOutput()->addWikiText( $msg );
+ if ( $logout ) {
+ $currentSession = CentralAuthUser::getSession();
+ $centralUser = new CentralAuthUser( $currentSession['user'] );
+
+ if ( $currentSession['user'] === $tokenData['userName']
+ && $centralUser->authenticateWithToken( $tokenData['token'] )
+ ) {
+ $centralUser->deleteGlobalCookies();
+ }
+
+ $this->showIcon();
return;
}
- $centralUser = new CentralAuthUser( $userName );
- $loginResult = $centralUser->authenticateWithToken( $token );
- if ( $loginResult != 'ok' ) {
- $msg = "Bad token: $loginResult";
- wfDebug( __METHOD__ . ": $msg\n" );
- $this->setHeaders();
- $this->getOutput()->addWikiText( $msg );
+ if ( $stage === 1 ) {
+
+ /* Stage1: Executed by the attached (remote) wiki.
+ * - Sets the centralauth_Session cookie, but points to an invalid session object.
+ * - Deletes centralauth_User, centralauth_Token cookies, so attacker can't forge rememberme
+ */
+
+ //Check for existing CentralAuth Session
+ $currentSession = CentralAuthUser::getSession();
+ if ( count( $currentSession ) && $currentSession['user'] != $tokenData['userName'] ) {
+ //An attacker might be trying mess with our session?
+ wfDebug( __METHOD__ . " Recieved login token but already logged in as a different user '{$currentSession['user']}' != '{$tokenData['userName']}'\n" );
+ $this->setHeaders();
+ $this->getOutput()->addWikiMsg( 'centralauth-token-mismatch' );
+ return;
+ }
+
+ // Clear any exiting cookies, if this was not the original wiki. Otherwise,
+ // if phase3 fails, the session and token may not match
+ if ( $tokenData['srcwiki'] != wfWikiID() ) {
+ CentralAuthUser::setCookie( 'User', '', - 86400 );
+ CentralAuthUser::setCookie( 'Token', '', - 86400 );
+ }
+
+ // Start centralauth session, refreshing session_id (bug 40962)
+ $invalidSessionData = array( 'stage' => 1 );
+ $centralSessionId = CentralAuthUser::setSession( $invalidSessionData, true );
+
+ $stageOneTokenData = array(
+ 'sessionId' => $centralSessionId,
+ 'wiki' => wfWikiID()
+ );
+ $stageOneTokenData = array_merge( $stageOneTokenData, $tokenData );
+
+ $tokenStageTwo = MWCryptRand::generateHex( 32 );
+ $key = CentralAuthUser::memcKey( 'login-token', $tokenStageTwo );
+ $wgMemc->set( $key, $stageOneTokenData, 60 );
+
+ // Redirect back to source wiki
+ $srcwiki = WikiMap::getWiki( $tokenData['srcwiki'] );
+ $url = wfAppendQuery( $srcwiki->getFullUrl( 'Special:AutoLogin' ), "token=$tokenStageTwo&stage=2" );
+ $this->getOutput()->redirect( $url );
+
return;
- }
- // Auth OK.
- if ( $logout ) {
- $centralUser->deleteGlobalCookies();
- } else {
- $centralUser->setGlobalCookies( $remember );
+ } elseif ( $stage === 2 ) {
+
+ // Stage 2 is run on the central wiki that initially authenticated the user.
+
+ global $wgCentralAuthCookiePrefix;
+
+ $foreignSessionId = $tokenData[ 'sessionId' ];
+ $foreignWiki = $tokenData[ 'wiki' ];
+ $originWiki = $tokenData['srcwiki'];
+ $userName = $tokenData['userName'];
+ $userToken = $tokenData['token'];
+ $remember = $tokenData['remember'];
+
+ /* The CentralAuth Session cookie hasn't been set on this wiki yet, so we
+ * verify the username agaist either the CentralAuth User cookie, or the
+ * local wiki UserName cookie.
+ * This check prevents CSRF agsinst stage2 messing with the session
+ */
+ if ( isset( $_COOKIE[ $wgCentralAuthCookiePrefix . 'User' ] ) ) {
+ $currentUsername = $_COOKIE[ $wgCentralAuthCookiePrefix . 'User' ];
+ } else {
+ $currentUsername = $this->getRequest()->getCookie('UserName');
+ }
+
+ if ( $currentUsername != $userName ) {
+ $msg = 'Bad token (wrong user)';
+ wfDebug( __METHOD__ . ": $msg\n" );
+ $this->setHeaders();
+ $this->getOutput()->addWikiText( $msg );
+ return;
+ }
+
+ if ( $originWiki != wfWikiID() ) {
+ $msg = 'Bad token (wrong wiki)';
+ wfDebug( __METHOD__ . ": $msg\n" );
+ $this->setHeaders();
+ $this->getOutput()->addWikiText( $msg );
+ return;
+ }
+
+ // Make sure the passed in token authenticates. Probably redundant.
+ $centralUser = new CentralAuthUser( $userName );
+ $loginResult = $centralUser->authenticateWithToken( $userToken );
+
+ if ( $loginResult != 'ok' ) {
+ $msg = "Bad token: $loginResult";
+ wfDebug( __METHOD__ . ": $msg\n" );
+ $this->setHeaders();
+ $this->getOutput()->addWikiText( $msg );
+ return;
+ }
+
+ // Auth OK.
+ $centralData = $centralUser->getSessionDataArray();
+ $centralUser->setSession( $centralData, false, $foreignSessionId );
+
+ $stageThreeData = array(
+ 'userName' => $userName,
+ 'remember' => $remember
+ );
+
+ $tokenStageThree = MWCryptRand::generateHex( 32 );
+ $key = CentralAuthUser::memcKey( 'login-token', $tokenStageThree );
+ $wgMemc->set( $key, $stageThreeData, 60 );
+
+ $wiki = WikiMap::getWiki( $foreignWiki );
+ $url = wfAppendQuery( $wiki->getFullUrl( 'Special:AutoLogin' ), "token=$tokenStageThree&stage=3" );
+ $this->getOutput()->redirect( $url );
+
+ } elseif ( $stage === 3 ) {
+
+ /* Executed on the remote/attached wiki. At this point, the CentralAuth
+ * Session cookie is in place, pointing to a valid session.
+ * Set User/Token cookies for remember-me feature and show icon
+ */
+
+ $currentSession = CentralAuthUser::getSession();
+
+ if ( $tokenData['userName'] === $currentSession['user'] ) {
+ $centralUser = new CentralAuthUser( $tokenData['userName'] );
+ $centralUser->setGlobalCookies( $tokenData['remember'] );
+ } else {
+ $msg = 'Bad token (wrong user)';
+ wfDebug( __METHOD__ . ": $msg\n" );
+ $this->setHeaders();
+ $this->getOutput()->addWikiText( $msg );
+ return;
+ }
+
+ $this->showIcon();
}
+ }
- $this->getOutput()->disable();
+ private function showIcon() {
+ global $wgCentralAuthLoginIcon;
+ $this->getOutput()->disable();
wfResetOutputBuffers();
header( 'Cache-Control: no-cache' );
header( 'Content-Type: image/png' );
-
- global $wgCentralAuthLoginIcon;
if ( $wgCentralAuthLoginIcon ) {
readfile( $wgCentralAuthLoginIcon );
} else {
readfile( __DIR__ . '/1x1.png' );
}
}
+
}
--
1.7.5.4

File Metadata

Mime Type
text/x-diff
Storage Engine
blob
Storage Format
Raw Data
Storage Handle
9408
Default Alt Text
3stage.patch (13 KB)

Event Timeline