mirror of
https://we.phorge.it/source/phorge.git
synced 2024-11-29 10:12:41 +01:00
Upgrade sessions digests to HMAC256, retaining compatibility with old digests
Summary: Ref T13222. Ref T13225. We store a digest of the session key in the session table (not the session key itself) so that users with access to this table can't easily steal sessions by just setting their cookies to values from the table. Users with access to the database can //probably// do plenty of other bad stuff (e.g., T13134 mentions digesting Conduit tokens) but there's very little cost to storing digests instead of live tokens. We currently digest session keys with HMAC-SHA1. This is fine, but HMAC-SHA256 is better. Upgrade: - Always write new digests. - We still match sessions with either digest. - When we read a session with an old digest, upgrade it to a new digest. In a few months we can throw away the old code. When we do, installs that skip upgrades for a long time may suffer a one-time logout, but I'll note this in the changelog. We could avoid this by storing `hmac256(hmac1(key))` instead and re-hashing in a migration, but I think the cost of a one-time logout for some tiny subset of users is very low, and worth keeping things simpler in the long run. Test Plan: - Hit a page with an old session, got a session upgrade. - Reviewed sessions in Settings. - Reviewed user logs. - Logged out. - Logged in. - Terminated other sessions individually. - Terminated all other sessions. - Spot checked session table for general sanity. Reviewers: amckinley Reviewed By: amckinley Subscribers: PHID-OPKG-gm6ozazyms6q6i22gyam Maniphest Tasks: T13225, T13222 Differential Revision: https://secure.phabricator.com/D19883
This commit is contained in:
parent
c58506aeaa
commit
1d34238dc9
13 changed files with 84 additions and 42 deletions
|
@ -0,0 +1,2 @@
|
||||||
|
ALTER TABLE {$NAMESPACE}_user.phabricator_session
|
||||||
|
CHANGE sessionKey sessionKey VARBINARY(64) NOT NULL;
|
|
@ -0,0 +1,2 @@
|
||||||
|
ALTER TABLE {$NAMESPACE}_user.user_log
|
||||||
|
CHANGE session session VARBINARY(64);
|
|
@ -1,22 +1,7 @@
|
||||||
<?php
|
<?php
|
||||||
|
|
||||||
$table = new PhabricatorUser();
|
// See T13225. Long ago, this upgraded session key storage from unhashed to
|
||||||
$table->openTransaction();
|
// HMAC-SHA1 here. We later upgraded storage to HMAC-SHA256, so this is initial
|
||||||
$conn = $table->establishConnection('w');
|
// upgrade is now fairly pointless. Dropping this migration entirely only logs
|
||||||
|
// users out of installs that waited more than 5 years to upgrade, which seems
|
||||||
$sessions = queryfx_all(
|
// like a reasonable behavior.
|
||||||
$conn,
|
|
||||||
'SELECT userPHID, type, sessionKey FROM %T FOR UPDATE',
|
|
||||||
PhabricatorUser::SESSION_TABLE);
|
|
||||||
|
|
||||||
foreach ($sessions as $session) {
|
|
||||||
queryfx(
|
|
||||||
$conn,
|
|
||||||
'UPDATE %T SET sessionKey = %s WHERE userPHID = %s AND type = %s',
|
|
||||||
PhabricatorUser::SESSION_TABLE,
|
|
||||||
PhabricatorHash::weakDigest($session['sessionKey']),
|
|
||||||
$session['userPHID'],
|
|
||||||
$session['type']);
|
|
||||||
}
|
|
||||||
|
|
||||||
$table->saveTransaction();
|
|
||||||
|
|
|
@ -16,8 +16,9 @@ final class PhabricatorAuthTerminateSessionController
|
||||||
$query->withIDs(array($id));
|
$query->withIDs(array($id));
|
||||||
}
|
}
|
||||||
|
|
||||||
$current_key = PhabricatorHash::weakDigest(
|
$current_key = PhabricatorAuthSession::newSessionDigest(
|
||||||
$request->getCookie(PhabricatorCookies::COOKIE_SESSION));
|
new PhutilOpaqueEnvelope(
|
||||||
|
$request->getCookie(PhabricatorCookies::COOKIE_SESSION)));
|
||||||
|
|
||||||
$sessions = $query->execute();
|
$sessions = $query->execute();
|
||||||
foreach ($sessions as $key => $session) {
|
foreach ($sessions as $key => $session) {
|
||||||
|
|
|
@ -56,7 +56,8 @@ final class PhabricatorAuthUnlinkController
|
||||||
|
|
||||||
id(new PhabricatorAuthSessionEngine())->terminateLoginSessions(
|
id(new PhabricatorAuthSessionEngine())->terminateLoginSessions(
|
||||||
$viewer,
|
$viewer,
|
||||||
$request->getCookie(PhabricatorCookies::COOKIE_SESSION));
|
new PhutilOpaqueEnvelope(
|
||||||
|
$request->getCookie(PhabricatorCookies::COOKIE_SESSION)));
|
||||||
|
|
||||||
return id(new AphrontRedirectResponse())->setURI($this->getDoneURI());
|
return id(new AphrontRedirectResponse())->setURI($this->getDoneURI());
|
||||||
}
|
}
|
||||||
|
|
|
@ -109,14 +109,19 @@ final class PhabricatorAuthSessionEngine extends Phobject {
|
||||||
|
|
||||||
$session_table = new PhabricatorAuthSession();
|
$session_table = new PhabricatorAuthSession();
|
||||||
$user_table = new PhabricatorUser();
|
$user_table = new PhabricatorUser();
|
||||||
$conn_r = $session_table->establishConnection('r');
|
$conn = $session_table->establishConnection('r');
|
||||||
$session_key = PhabricatorHash::weakDigest($session_token);
|
|
||||||
|
|
||||||
$cache_parts = $this->getUserCacheQueryParts($conn_r);
|
// TODO: See T13225. We're moving sessions to a more modern digest
|
||||||
|
// algorithm, but still accept older cookies for compatibility.
|
||||||
|
$session_key = PhabricatorAuthSession::newSessionDigest(
|
||||||
|
new PhutilOpaqueEnvelope($session_token));
|
||||||
|
$weak_key = PhabricatorHash::weakDigest($session_token);
|
||||||
|
|
||||||
|
$cache_parts = $this->getUserCacheQueryParts($conn);
|
||||||
list($cache_selects, $cache_joins, $cache_map, $types_map) = $cache_parts;
|
list($cache_selects, $cache_joins, $cache_map, $types_map) = $cache_parts;
|
||||||
|
|
||||||
$info = queryfx_one(
|
$info = queryfx_one(
|
||||||
$conn_r,
|
$conn,
|
||||||
'SELECT
|
'SELECT
|
||||||
s.id AS s_id,
|
s.id AS s_id,
|
||||||
s.phid AS s_phid,
|
s.phid AS s_phid,
|
||||||
|
@ -125,21 +130,28 @@ final class PhabricatorAuthSessionEngine extends Phobject {
|
||||||
s.highSecurityUntil AS s_highSecurityUntil,
|
s.highSecurityUntil AS s_highSecurityUntil,
|
||||||
s.isPartial AS s_isPartial,
|
s.isPartial AS s_isPartial,
|
||||||
s.signedLegalpadDocuments as s_signedLegalpadDocuments,
|
s.signedLegalpadDocuments as s_signedLegalpadDocuments,
|
||||||
|
IF(s.sessionKey = %P, 1, 0) as s_weak,
|
||||||
u.*
|
u.*
|
||||||
%Q
|
%Q
|
||||||
FROM %T u JOIN %T s ON u.phid = s.userPHID
|
FROM %R u JOIN %R s ON u.phid = s.userPHID
|
||||||
AND s.type = %s AND s.sessionKey = %P %Q',
|
AND s.type = %s AND s.sessionKey IN (%P, %P) %Q',
|
||||||
|
new PhutilOpaqueEnvelope($weak_key),
|
||||||
$cache_selects,
|
$cache_selects,
|
||||||
$user_table->getTableName(),
|
$user_table,
|
||||||
$session_table->getTableName(),
|
$session_table,
|
||||||
$session_type,
|
$session_type,
|
||||||
new PhutilOpaqueEnvelope($session_key),
|
new PhutilOpaqueEnvelope($session_key),
|
||||||
|
new PhutilOpaqueEnvelope($weak_key),
|
||||||
$cache_joins);
|
$cache_joins);
|
||||||
|
|
||||||
if (!$info) {
|
if (!$info) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// TODO: Remove this, see T13225.
|
||||||
|
$is_weak = (bool)$info['s_weak'];
|
||||||
|
unset($info['s_weak']);
|
||||||
|
|
||||||
$session_dict = array(
|
$session_dict = array(
|
||||||
'userPHID' => $info['phid'],
|
'userPHID' => $info['phid'],
|
||||||
'sessionKey' => $session_key,
|
'sessionKey' => $session_key,
|
||||||
|
@ -202,6 +214,19 @@ final class PhabricatorAuthSessionEngine extends Phobject {
|
||||||
unset($unguarded);
|
unset($unguarded);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// TODO: Remove this, see T13225.
|
||||||
|
if ($is_weak) {
|
||||||
|
$unguarded = AphrontWriteGuard::beginScopedUnguardedWrites();
|
||||||
|
$conn_w = $session_table->establishConnection('w');
|
||||||
|
queryfx(
|
||||||
|
$conn_w,
|
||||||
|
'UPDATE %T SET sessionKey = %P WHERE id = %d',
|
||||||
|
$session->getTableName(),
|
||||||
|
new PhutilOpaqueEnvelope($session_key),
|
||||||
|
$session->getID());
|
||||||
|
unset($unguarded);
|
||||||
|
}
|
||||||
|
|
||||||
$user->attachSession($session);
|
$user->attachSession($session);
|
||||||
return $user;
|
return $user;
|
||||||
}
|
}
|
||||||
|
@ -241,7 +266,8 @@ final class PhabricatorAuthSessionEngine extends Phobject {
|
||||||
// This has a side effect of validating the session type.
|
// This has a side effect of validating the session type.
|
||||||
$session_ttl = PhabricatorAuthSession::getSessionTypeTTL($session_type);
|
$session_ttl = PhabricatorAuthSession::getSessionTypeTTL($session_type);
|
||||||
|
|
||||||
$digest_key = PhabricatorHash::weakDigest($session_key);
|
$digest_key = PhabricatorAuthSession::newSessionDigest(
|
||||||
|
new PhutilOpaqueEnvelope($session_key));
|
||||||
|
|
||||||
// Logging-in users don't have CSRF stuff yet, so we have to unguard this
|
// Logging-in users don't have CSRF stuff yet, so we have to unguard this
|
||||||
// write.
|
// write.
|
||||||
|
@ -299,7 +325,7 @@ final class PhabricatorAuthSessionEngine extends Phobject {
|
||||||
*/
|
*/
|
||||||
public function terminateLoginSessions(
|
public function terminateLoginSessions(
|
||||||
PhabricatorUser $user,
|
PhabricatorUser $user,
|
||||||
$except_session = null) {
|
PhutilOpaqueEnvelope $except_session = null) {
|
||||||
|
|
||||||
$sessions = id(new PhabricatorAuthSessionQuery())
|
$sessions = id(new PhabricatorAuthSessionQuery())
|
||||||
->setViewer($user)
|
->setViewer($user)
|
||||||
|
@ -307,7 +333,8 @@ final class PhabricatorAuthSessionEngine extends Phobject {
|
||||||
->execute();
|
->execute();
|
||||||
|
|
||||||
if ($except_session !== null) {
|
if ($except_session !== null) {
|
||||||
$except_session = PhabricatorHash::weakDigest($except_session);
|
$except_session = PhabricatorAuthSession::newSessionDigest(
|
||||||
|
$except_session);
|
||||||
}
|
}
|
||||||
|
|
||||||
foreach ($sessions as $key => $session) {
|
foreach ($sessions as $key => $session) {
|
||||||
|
|
|
@ -91,7 +91,8 @@ final class PhabricatorAuthSessionQuery
|
||||||
if ($this->sessionKeys !== null) {
|
if ($this->sessionKeys !== null) {
|
||||||
$hashes = array();
|
$hashes = array();
|
||||||
foreach ($this->sessionKeys as $session_key) {
|
foreach ($this->sessionKeys as $session_key) {
|
||||||
$hashes[] = PhabricatorHash::weakDigest($session_key);
|
$hashes[] = PhabricatorAuthSession::newSessionDigest(
|
||||||
|
new PhutilOpaqueEnvelope($session_key));
|
||||||
}
|
}
|
||||||
$where[] = qsprintf(
|
$where[] = qsprintf(
|
||||||
$conn,
|
$conn,
|
||||||
|
|
|
@ -6,6 +6,8 @@ final class PhabricatorAuthSession extends PhabricatorAuthDAO
|
||||||
const TYPE_WEB = 'web';
|
const TYPE_WEB = 'web';
|
||||||
const TYPE_CONDUIT = 'conduit';
|
const TYPE_CONDUIT = 'conduit';
|
||||||
|
|
||||||
|
const SESSION_DIGEST_KEY = 'session.digest';
|
||||||
|
|
||||||
protected $userPHID;
|
protected $userPHID;
|
||||||
protected $type;
|
protected $type;
|
||||||
protected $sessionKey;
|
protected $sessionKey;
|
||||||
|
@ -17,13 +19,19 @@ final class PhabricatorAuthSession extends PhabricatorAuthDAO
|
||||||
|
|
||||||
private $identityObject = self::ATTACHABLE;
|
private $identityObject = self::ATTACHABLE;
|
||||||
|
|
||||||
|
public static function newSessionDigest(PhutilOpaqueEnvelope $session_token) {
|
||||||
|
return PhabricatorHash::digestWithNamedKey(
|
||||||
|
$session_token->openEnvelope(),
|
||||||
|
self::SESSION_DIGEST_KEY);
|
||||||
|
}
|
||||||
|
|
||||||
protected function getConfiguration() {
|
protected function getConfiguration() {
|
||||||
return array(
|
return array(
|
||||||
self::CONFIG_TIMESTAMPS => false,
|
self::CONFIG_TIMESTAMPS => false,
|
||||||
self::CONFIG_AUX_PHID => true,
|
self::CONFIG_AUX_PHID => true,
|
||||||
self::CONFIG_COLUMN_SCHEMA => array(
|
self::CONFIG_COLUMN_SCHEMA => array(
|
||||||
'type' => 'text32',
|
'type' => 'text32',
|
||||||
'sessionKey' => 'bytes40',
|
'sessionKey' => 'text64',
|
||||||
'sessionStart' => 'epoch',
|
'sessionStart' => 'epoch',
|
||||||
'sessionExpires' => 'epoch',
|
'sessionExpires' => 'epoch',
|
||||||
'highSecurityUntil' => 'epoch?',
|
'highSecurityUntil' => 'epoch?',
|
||||||
|
|
|
@ -150,7 +150,7 @@ final class PhabricatorUserLog extends PhabricatorUserDAO
|
||||||
'actorPHID' => 'phid?',
|
'actorPHID' => 'phid?',
|
||||||
'action' => 'text64',
|
'action' => 'text64',
|
||||||
'remoteAddr' => 'text64',
|
'remoteAddr' => 'text64',
|
||||||
'session' => 'bytes40?',
|
'session' => 'text64?',
|
||||||
),
|
),
|
||||||
self::CONFIG_KEY_SCHEMA => array(
|
self::CONFIG_KEY_SCHEMA => array(
|
||||||
'actorPHID' => array(
|
'actorPHID' => array(
|
||||||
|
|
|
@ -193,7 +193,8 @@ final class PhabricatorMultiFactorSettingsPanel
|
||||||
|
|
||||||
id(new PhabricatorAuthSessionEngine())->terminateLoginSessions(
|
id(new PhabricatorAuthSessionEngine())->terminateLoginSessions(
|
||||||
$user,
|
$user,
|
||||||
$request->getCookie(PhabricatorCookies::COOKIE_SESSION));
|
new PhutilOpaqueEnvelope(
|
||||||
|
$request->getCookie(PhabricatorCookies::COOKIE_SESSION)));
|
||||||
|
|
||||||
return id(new AphrontRedirectResponse())
|
return id(new AphrontRedirectResponse())
|
||||||
->setURI($this->getPanelURI('?id='.$config->getID()));
|
->setURI($this->getPanelURI('?id='.$config->getID()));
|
||||||
|
|
|
@ -121,7 +121,8 @@ final class PhabricatorPasswordSettingsPanel extends PhabricatorSettingsPanel {
|
||||||
|
|
||||||
id(new PhabricatorAuthSessionEngine())->terminateLoginSessions(
|
id(new PhabricatorAuthSessionEngine())->terminateLoginSessions(
|
||||||
$user,
|
$user,
|
||||||
$request->getCookie(PhabricatorCookies::COOKIE_SESSION));
|
new PhutilOpaqueEnvelope(
|
||||||
|
$request->getCookie(PhabricatorCookies::COOKIE_SESSION)));
|
||||||
|
|
||||||
return id(new AphrontRedirectResponse())->setURI($next);
|
return id(new AphrontRedirectResponse())->setURI($next);
|
||||||
}
|
}
|
||||||
|
|
|
@ -44,8 +44,9 @@ final class PhabricatorSessionsSettingsPanel extends PhabricatorSettingsPanel {
|
||||||
->withPHIDs($identity_phids)
|
->withPHIDs($identity_phids)
|
||||||
->execute();
|
->execute();
|
||||||
|
|
||||||
$current_key = PhabricatorHash::weakDigest(
|
$current_key = PhabricatorAuthSession::newSessionDigest(
|
||||||
$request->getCookie(PhabricatorCookies::COOKIE_SESSION));
|
new PhutilOpaqueEnvelope(
|
||||||
|
$request->getCookie(PhabricatorCookies::COOKIE_SESSION)));
|
||||||
|
|
||||||
$rows = array();
|
$rows = array();
|
||||||
$rowc = array();
|
$rowc = array();
|
||||||
|
|
|
@ -187,6 +187,16 @@ final class PhabricatorHash extends Phobject {
|
||||||
}
|
}
|
||||||
|
|
||||||
public static function digestHMACSHA256($message, $key) {
|
public static function digestHMACSHA256($message, $key) {
|
||||||
|
if (!is_string($message)) {
|
||||||
|
throw new Exception(
|
||||||
|
pht('HMAC-SHA256 can only digest strings.'));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!is_string($key)) {
|
||||||
|
throw new Exception(
|
||||||
|
pht('HMAC-SHA256 keys must be strings.'));
|
||||||
|
}
|
||||||
|
|
||||||
if (!strlen($key)) {
|
if (!strlen($key)) {
|
||||||
throw new Exception(
|
throw new Exception(
|
||||||
pht('HMAC-SHA256 requires a nonempty key.'));
|
pht('HMAC-SHA256 requires a nonempty key.'));
|
||||||
|
@ -194,7 +204,9 @@ final class PhabricatorHash extends Phobject {
|
||||||
|
|
||||||
$result = hash_hmac('sha256', $message, $key, $raw_output = false);
|
$result = hash_hmac('sha256', $message, $key, $raw_output = false);
|
||||||
|
|
||||||
if ($result === false) {
|
// Although "hash_hmac()" is documented as returning `false` when it fails,
|
||||||
|
// it can also return `null` if you pass an object as the "$message".
|
||||||
|
if ($result === false || $result === null) {
|
||||||
throw new Exception(
|
throw new Exception(
|
||||||
pht('Unable to compute HMAC-SHA256 digest of message.'));
|
pht('Unable to compute HMAC-SHA256 digest of message.'));
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in a new issue