@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
at upstream/main 122 lines 3.0 kB view raw
1<?php 2 3final class PhabricatorAuthCSRFEngine extends Phobject { 4 5 private $salt; 6 private $secret; 7 8 public function setSalt($salt) { 9 $this->salt = $salt; 10 return $this; 11 } 12 13 public function getSalt() { 14 return $this->salt; 15 } 16 17 public function setSecret(PhutilOpaqueEnvelope $secret) { 18 $this->secret = $secret; 19 return $this; 20 } 21 22 public function getSecret() { 23 return $this->secret; 24 } 25 26 public function newSalt() { 27 $salt_length = $this->getSaltLength(); 28 return Filesystem::readRandomCharacters($salt_length); 29 } 30 31 public function newToken() { 32 $salt = $this->getSalt(); 33 34 if (!$salt) { 35 throw new PhutilInvalidStateException('setSalt'); 36 } 37 38 $token = $this->newRawToken($salt); 39 $prefix = $this->getBREACHPrefix(); 40 41 return sprintf('%s%s%s', $prefix, $salt, $token); 42 } 43 44 public function isValidToken($token) { 45 $salt_length = $this->getSaltLength(); 46 47 // We expect a BREACH-mitigating token. See T3684. 48 $breach_prefix = $this->getBREACHPrefix(); 49 $breach_prelen = strlen($breach_prefix); 50 if ( 51 $token === null || 52 strncmp($token, $breach_prefix, $breach_prelen) !== 0 53 ) { 54 return false; 55 } 56 57 $salt = substr($token, $breach_prelen, $salt_length); 58 $token = substr($token, $breach_prelen + $salt_length); 59 60 foreach ($this->getWindowOffsets() as $offset) { 61 $expect_token = $this->newRawToken($salt, $offset); 62 if (phutil_hashes_are_identical($expect_token, $token)) { 63 return true; 64 } 65 } 66 67 return false; 68 } 69 70 private function newRawToken($salt, $offset = 0) { 71 $now = PhabricatorTime::getNow(); 72 $cycle_frequency = $this->getCycleFrequency(); 73 74 $time_block = (int)floor($now / $cycle_frequency); 75 $time_block = $time_block + $offset; 76 77 $secret = $this->getSecret(); 78 if (!$secret) { 79 throw new PhutilInvalidStateException('setSecret'); 80 } 81 $secret = $secret->openEnvelope(); 82 83 $hash = PhabricatorHash::digestWithNamedKey( 84 $secret.$time_block.$salt, 85 'csrf'); 86 87 return substr($hash, 0, $this->getTokenLength()); 88 } 89 90 private function getBREACHPrefix() { 91 return 'B@'; 92 } 93 94 private function getSaltLength() { 95 return 8; 96 } 97 98 private function getTokenLength() { 99 return 16; 100 } 101 102 private function getCycleFrequency() { 103 return phutil_units('1 hour in seconds'); 104 } 105 106 private function getWindowOffsets() { 107 // We accept some tokens from the recent past and near future. Users may 108 // have older tokens if they close their laptop and open it up again 109 // later. Users may have newer tokens if there are multiple web hosts with 110 // a bit of clock skew. 111 112 // Javascript on the client tries to keep CSRF tokens up to date, but 113 // it may fail, and it doesn't run if the user closes their laptop. 114 115 // The window during which our tokens remain valid is generally more 116 // conservative than other platforms. For example, Rails uses "session 117 // duration" and Django uses "forever". 118 119 return range(-6, 1); 120 } 121 122}