@recaptime-dev's working patches + fork for Phorge, a community fork of Phabricator. (Upstream dev and stable branches are at upstream/main and upstream/stable respectively.) hq.recaptime.dev/wiki/Phorge
phorge phabricator
1
fork

Configure Feed

Select the types of activity you want to include in your feed.

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

+84 -42
+2
resources/sql/autopatches/20181213.auth.04.longerhashes.sql
··· 1 + ALTER TABLE {$NAMESPACE}_user.phabricator_session 2 + CHANGE sessionKey sessionKey VARBINARY(64) NOT NULL;
+2
resources/sql/autopatches/20181213.auth.05.longerloghashes.sql
··· 1 + ALTER TABLE {$NAMESPACE}_user.user_log 2 + CHANGE session session VARBINARY(64);
+5 -20
resources/sql/patches/20130530.sessionhash.php
··· 1 1 <?php 2 2 3 - $table = new PhabricatorUser(); 4 - $table->openTransaction(); 5 - $conn = $table->establishConnection('w'); 6 - 7 - $sessions = queryfx_all( 8 - $conn, 9 - 'SELECT userPHID, type, sessionKey FROM %T FOR UPDATE', 10 - PhabricatorUser::SESSION_TABLE); 11 - 12 - foreach ($sessions as $session) { 13 - queryfx( 14 - $conn, 15 - 'UPDATE %T SET sessionKey = %s WHERE userPHID = %s AND type = %s', 16 - PhabricatorUser::SESSION_TABLE, 17 - PhabricatorHash::weakDigest($session['sessionKey']), 18 - $session['userPHID'], 19 - $session['type']); 20 - } 21 - 22 - $table->saveTransaction(); 3 + // See T13225. Long ago, this upgraded session key storage from unhashed to 4 + // HMAC-SHA1 here. We later upgraded storage to HMAC-SHA256, so this is initial 5 + // upgrade is now fairly pointless. Dropping this migration entirely only logs 6 + // users out of installs that waited more than 5 years to upgrade, which seems 7 + // like a reasonable behavior.
+3 -2
src/applications/auth/controller/PhabricatorAuthTerminateSessionController.php
··· 16 16 $query->withIDs(array($id)); 17 17 } 18 18 19 - $current_key = PhabricatorHash::weakDigest( 20 - $request->getCookie(PhabricatorCookies::COOKIE_SESSION)); 19 + $current_key = PhabricatorAuthSession::newSessionDigest( 20 + new PhutilOpaqueEnvelope( 21 + $request->getCookie(PhabricatorCookies::COOKIE_SESSION))); 21 22 22 23 $sessions = $query->execute(); 23 24 foreach ($sessions as $key => $session) {
+2 -1
src/applications/auth/controller/PhabricatorAuthUnlinkController.php
··· 56 56 57 57 id(new PhabricatorAuthSessionEngine())->terminateLoginSessions( 58 58 $viewer, 59 - $request->getCookie(PhabricatorCookies::COOKIE_SESSION)); 59 + new PhutilOpaqueEnvelope( 60 + $request->getCookie(PhabricatorCookies::COOKIE_SESSION))); 60 61 61 62 return id(new AphrontRedirectResponse())->setURI($this->getDoneURI()); 62 63 }
+38 -11
src/applications/auth/engine/PhabricatorAuthSessionEngine.php
··· 109 109 110 110 $session_table = new PhabricatorAuthSession(); 111 111 $user_table = new PhabricatorUser(); 112 - $conn_r = $session_table->establishConnection('r'); 113 - $session_key = PhabricatorHash::weakDigest($session_token); 112 + $conn = $session_table->establishConnection('r'); 113 + 114 + // TODO: See T13225. We're moving sessions to a more modern digest 115 + // algorithm, but still accept older cookies for compatibility. 116 + $session_key = PhabricatorAuthSession::newSessionDigest( 117 + new PhutilOpaqueEnvelope($session_token)); 118 + $weak_key = PhabricatorHash::weakDigest($session_token); 114 119 115 - $cache_parts = $this->getUserCacheQueryParts($conn_r); 120 + $cache_parts = $this->getUserCacheQueryParts($conn); 116 121 list($cache_selects, $cache_joins, $cache_map, $types_map) = $cache_parts; 117 122 118 123 $info = queryfx_one( 119 - $conn_r, 124 + $conn, 120 125 'SELECT 121 126 s.id AS s_id, 122 127 s.phid AS s_phid, ··· 125 130 s.highSecurityUntil AS s_highSecurityUntil, 126 131 s.isPartial AS s_isPartial, 127 132 s.signedLegalpadDocuments as s_signedLegalpadDocuments, 133 + IF(s.sessionKey = %P, 1, 0) as s_weak, 128 134 u.* 129 135 %Q 130 - FROM %T u JOIN %T s ON u.phid = s.userPHID 131 - AND s.type = %s AND s.sessionKey = %P %Q', 136 + FROM %R u JOIN %R s ON u.phid = s.userPHID 137 + AND s.type = %s AND s.sessionKey IN (%P, %P) %Q', 138 + new PhutilOpaqueEnvelope($weak_key), 132 139 $cache_selects, 133 - $user_table->getTableName(), 134 - $session_table->getTableName(), 140 + $user_table, 141 + $session_table, 135 142 $session_type, 136 143 new PhutilOpaqueEnvelope($session_key), 144 + new PhutilOpaqueEnvelope($weak_key), 137 145 $cache_joins); 138 146 139 147 if (!$info) { 140 148 return null; 141 149 } 142 150 151 + // TODO: Remove this, see T13225. 152 + $is_weak = (bool)$info['s_weak']; 153 + unset($info['s_weak']); 154 + 143 155 $session_dict = array( 144 156 'userPHID' => $info['phid'], 145 157 'sessionKey' => $session_key, ··· 202 214 unset($unguarded); 203 215 } 204 216 217 + // TODO: Remove this, see T13225. 218 + if ($is_weak) { 219 + $unguarded = AphrontWriteGuard::beginScopedUnguardedWrites(); 220 + $conn_w = $session_table->establishConnection('w'); 221 + queryfx( 222 + $conn_w, 223 + 'UPDATE %T SET sessionKey = %P WHERE id = %d', 224 + $session->getTableName(), 225 + new PhutilOpaqueEnvelope($session_key), 226 + $session->getID()); 227 + unset($unguarded); 228 + } 229 + 205 230 $user->attachSession($session); 206 231 return $user; 207 232 } ··· 241 266 // This has a side effect of validating the session type. 242 267 $session_ttl = PhabricatorAuthSession::getSessionTypeTTL($session_type); 243 268 244 - $digest_key = PhabricatorHash::weakDigest($session_key); 269 + $digest_key = PhabricatorAuthSession::newSessionDigest( 270 + new PhutilOpaqueEnvelope($session_key)); 245 271 246 272 // Logging-in users don't have CSRF stuff yet, so we have to unguard this 247 273 // write. ··· 299 325 */ 300 326 public function terminateLoginSessions( 301 327 PhabricatorUser $user, 302 - $except_session = null) { 328 + PhutilOpaqueEnvelope $except_session = null) { 303 329 304 330 $sessions = id(new PhabricatorAuthSessionQuery()) 305 331 ->setViewer($user) ··· 307 333 ->execute(); 308 334 309 335 if ($except_session !== null) { 310 - $except_session = PhabricatorHash::weakDigest($except_session); 336 + $except_session = PhabricatorAuthSession::newSessionDigest( 337 + $except_session); 311 338 } 312 339 313 340 foreach ($sessions as $key => $session) {
+2 -1
src/applications/auth/query/PhabricatorAuthSessionQuery.php
··· 91 91 if ($this->sessionKeys !== null) { 92 92 $hashes = array(); 93 93 foreach ($this->sessionKeys as $session_key) { 94 - $hashes[] = PhabricatorHash::weakDigest($session_key); 94 + $hashes[] = PhabricatorAuthSession::newSessionDigest( 95 + new PhutilOpaqueEnvelope($session_key)); 95 96 } 96 97 $where[] = qsprintf( 97 98 $conn,
+9 -1
src/applications/auth/storage/PhabricatorAuthSession.php
··· 6 6 const TYPE_WEB = 'web'; 7 7 const TYPE_CONDUIT = 'conduit'; 8 8 9 + const SESSION_DIGEST_KEY = 'session.digest'; 10 + 9 11 protected $userPHID; 10 12 protected $type; 11 13 protected $sessionKey; ··· 17 19 18 20 private $identityObject = self::ATTACHABLE; 19 21 22 + public static function newSessionDigest(PhutilOpaqueEnvelope $session_token) { 23 + return PhabricatorHash::digestWithNamedKey( 24 + $session_token->openEnvelope(), 25 + self::SESSION_DIGEST_KEY); 26 + } 27 + 20 28 protected function getConfiguration() { 21 29 return array( 22 30 self::CONFIG_TIMESTAMPS => false, 23 31 self::CONFIG_AUX_PHID => true, 24 32 self::CONFIG_COLUMN_SCHEMA => array( 25 33 'type' => 'text32', 26 - 'sessionKey' => 'bytes40', 34 + 'sessionKey' => 'text64', 27 35 'sessionStart' => 'epoch', 28 36 'sessionExpires' => 'epoch', 29 37 'highSecurityUntil' => 'epoch?',
+1 -1
src/applications/people/storage/PhabricatorUserLog.php
··· 150 150 'actorPHID' => 'phid?', 151 151 'action' => 'text64', 152 152 'remoteAddr' => 'text64', 153 - 'session' => 'bytes40?', 153 + 'session' => 'text64?', 154 154 ), 155 155 self::CONFIG_KEY_SCHEMA => array( 156 156 'actorPHID' => array(
+2 -1
src/applications/settings/panel/PhabricatorMultiFactorSettingsPanel.php
··· 193 193 194 194 id(new PhabricatorAuthSessionEngine())->terminateLoginSessions( 195 195 $user, 196 - $request->getCookie(PhabricatorCookies::COOKIE_SESSION)); 196 + new PhutilOpaqueEnvelope( 197 + $request->getCookie(PhabricatorCookies::COOKIE_SESSION))); 197 198 198 199 return id(new AphrontRedirectResponse()) 199 200 ->setURI($this->getPanelURI('?id='.$config->getID()));
+2 -1
src/applications/settings/panel/PhabricatorPasswordSettingsPanel.php
··· 121 121 122 122 id(new PhabricatorAuthSessionEngine())->terminateLoginSessions( 123 123 $user, 124 - $request->getCookie(PhabricatorCookies::COOKIE_SESSION)); 124 + new PhutilOpaqueEnvelope( 125 + $request->getCookie(PhabricatorCookies::COOKIE_SESSION))); 125 126 126 127 return id(new AphrontRedirectResponse())->setURI($next); 127 128 }
+3 -2
src/applications/settings/panel/PhabricatorSessionsSettingsPanel.php
··· 44 44 ->withPHIDs($identity_phids) 45 45 ->execute(); 46 46 47 - $current_key = PhabricatorHash::weakDigest( 48 - $request->getCookie(PhabricatorCookies::COOKIE_SESSION)); 47 + $current_key = PhabricatorAuthSession::newSessionDigest( 48 + new PhutilOpaqueEnvelope( 49 + $request->getCookie(PhabricatorCookies::COOKIE_SESSION))); 49 50 50 51 $rows = array(); 51 52 $rowc = array();
+13 -1
src/infrastructure/util/PhabricatorHash.php
··· 187 187 } 188 188 189 189 public static function digestHMACSHA256($message, $key) { 190 + if (!is_string($message)) { 191 + throw new Exception( 192 + pht('HMAC-SHA256 can only digest strings.')); 193 + } 194 + 195 + if (!is_string($key)) { 196 + throw new Exception( 197 + pht('HMAC-SHA256 keys must be strings.')); 198 + } 199 + 190 200 if (!strlen($key)) { 191 201 throw new Exception( 192 202 pht('HMAC-SHA256 requires a nonempty key.')); ··· 194 204 195 205 $result = hash_hmac('sha256', $message, $key, $raw_output = false); 196 206 197 - if ($result === false) { 207 + // Although "hash_hmac()" is documented as returning `false` when it fails, 208 + // it can also return `null` if you pass an object as the "$message". 209 + if ($result === false || $result === null) { 198 210 throw new Exception( 199 211 pht('Unable to compute HMAC-SHA256 digest of message.')); 200 212 }