1<?php 2 3namespace Elgg\Security; 4 5use Elgg\Config; 6use Elgg\CsrfException; 7use Elgg\Request; 8use Elgg\TimeUsing; 9use ElggCrypto; 10use ElggSession; 11 12/** 13 * CSRF Protection 14 */ 15class Csrf { 16 17 use TimeUsing; 18 19 /** 20 * @var Config 21 */ 22 protected $config; 23 24 /** 25 * @var ElggSession 26 */ 27 protected $session; 28 29 /** 30 * @var ElggCrypto 31 */ 32 protected $crypto; 33 34 /** 35 * @var HmacFactory 36 */ 37 protected $hmac; 38 39 /** 40 * Constructor 41 * 42 * @param Config $config Elgg config 43 * @param ElggSession $session Session 44 * @param ElggCrypto $crypto Crypto service 45 * @param HmacFactory $hmac HMAC service 46 */ 47 public function __construct( 48 Config $config, 49 ElggSession $session, 50 ElggCrypto $crypto, 51 HmacFactory $hmac 52 ) { 53 54 $this->config = $config; 55 $this->session = $session; 56 $this->crypto = $crypto; 57 $this->hmac = $hmac; 58 } 59 60 /** 61 * Validate CSRF tokens present in the request 62 * 63 * @param Request $request Request 64 * 65 * @return void 66 * @throws CsrfException 67 */ 68 public function validate(Request $request) { 69 $token = $request->getParam('__elgg_token'); 70 $ts = $request->getParam('__elgg_ts'); 71 72 $session_id = $this->session->getID(); 73 74 if (($token) && ($ts) && ($session_id)) { 75 if ($this->validateTokenOwnership($token, $ts)) { 76 if ($this->validateTokenTimestamp($ts)) { 77 // We have already got this far, so unless anything 78 // else says something to the contrary we assume we're ok 79 $returnval = $request->elgg()->hooks->trigger('action_gatekeeper:permissions:check', 'all', [ 80 'token' => $token, 81 'time' => $ts 82 ], true); 83 84 if ($returnval) { 85 return; 86 } else { 87 throw new CsrfException($request->elgg()->echo('actiongatekeeper:pluginprevents')); 88 } 89 } else { 90 // this is necessary because of #5133 91 if ($request->isXhr()) { 92 throw new CsrfException($request->elgg()->echo( 93 'js:security:token_refresh_failed', 94 [$this->config->wwwroot] 95 )); 96 } else { 97 throw new CsrfException($request->elgg()->echo('actiongatekeeper:timeerror')); 98 } 99 } 100 } else { 101 // this is necessary because of #5133 102 if ($request->isXhr()) { 103 throw new CsrfException($request->elgg()->echo('js:security:token_refresh_failed', [$this->config->wwwroot])); 104 } else { 105 throw new CsrfException($request->elgg()->echo('actiongatekeeper:tokeninvalid')); 106 } 107 } 108 } else { 109 $error_msg = $request->elgg()->echo('actiongatekeeper:missingfields'); 110 throw new CsrfException($request->elgg()->echo($error_msg)); 111 } 112 } 113 114 /** 115 * Basic token validation 116 * 117 * @param string $token Token 118 * @param int $ts Timestamp 119 * 120 * @return bool 121 * 122 * @internal 123 */ 124 public function isValidToken($token, $ts) { 125 return $this->validateTokenOwnership($token, $ts) && $this->validateTokenTimestamp($ts); 126 } 127 128 /** 129 * Is the token timestamp within acceptable range? 130 * 131 * @param int $ts timestamp from the CSRF token 132 * 133 * @return bool 134 */ 135 protected function validateTokenTimestamp($ts) { 136 $timeout = $this->getActionTokenTimeout(); 137 $now = $this->getCurrentTime()->getTimestamp(); 138 139 return ($timeout == 0 || ($ts > $now - $timeout) && ($ts < $now + $timeout)); 140 } 141 142 /** 143 * Returns the action token timeout in seconds 144 * 145 * @return int number of seconds that action token is valid 146 * 147 * @see Csrf::validateActionToken 148 * @internal 149 * @since 1.9.0 150 */ 151 public function getActionTokenTimeout() { 152 // default to 2 hours 153 $timeout = 2; 154 if ($this->config->hasValue('action_token_timeout')) { 155 // timeout set in config 156 $timeout = $this->config->action_token_timeout; 157 } 158 159 $hour = 60 * 60; 160 161 return (int) ((float) $timeout * $hour); 162 } 163 164 /** 165 * Was the given token generated for the session defined by session_token? 166 * 167 * @param string $token CSRF token 168 * @param int $timestamp Unix time 169 * @param string $session_token Session-specific token 170 * 171 * @return bool 172 * @internal 173 */ 174 public function validateTokenOwnership($token, $timestamp, $session_token = '') { 175 $required_token = $this->generateActionToken($timestamp, $session_token); 176 177 return $this->crypto->areEqual($token, $required_token); 178 } 179 180 /** 181 * Generate a token from a session token (specifying the user), the timestamp, and the site key. 182 * 183 * @param int $timestamp Unix timestamp 184 * @param string $session_token Session-specific token 185 * 186 * @return false|string 187 * @internal 188 */ 189 public function generateActionToken($timestamp, $session_token = '') { 190 if (!$session_token) { 191 $session_token = $this->session->get('__elgg_session'); 192 if (!$session_token) { 193 return false; 194 } 195 } 196 197 return $this->hmac 198 ->getHmac([(int) $timestamp, $session_token], 'md5') 199 ->getToken(); 200 } 201 202} 203