ActionsService::validateTokenTimestamp()   A
last analyzed

Complexity

Conditions 3
Paths 3

Size

Total Lines 5

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 12

Importance

Changes 0
Metric Value
cc 3
nc 3
nop 1
dl 0
loc 5
ccs 0
cts 4
cp 0
crap 12
rs 10
c 0
b 0
f 0
1
<?php
2
namespace Elgg;
3
4
/**
5
 * WARNING: API IN FLUX. DO NOT USE DIRECTLY.
6
 *
7
 * Use the elgg_* versions instead.
8
 *
9
 * @access private
10
 * 
11
 * @package    Elgg.Core
12
 * @subpackage Actions
13
 * @since      1.9.0
14
 */
15
class ActionsService {
16
	
17
	/**
18
	 * Registered actions storage
19
	 * @var array
20
	 */
21
	private $actions = array();
22
23
	/** 
24
	 * The current action being processed
25
	 * @var string 
26
	 */
27
	private $currentAction = null;
28
	
29
	/**
30
	 * @see action
31
	 * @access private
32
	 */
33
	public function execute($action, $forwarder = "") {
34
		$action = rtrim($action, '/');
35
		$this->currentAction = $action;
36
	
37
		// @todo REMOVE THESE ONCE #1509 IS IN PLACE.
38
		// Allow users to disable plugins without a token in order to
39
		// remove plugins that are incompatible.
40
		// Login and logout are for convenience.
41
		// file/download (see #2010)
42
		$exceptions = array(
43
			'admin/plugins/disable',
44
			'logout',
45
			'file/download',
46
		);
47
	
48
		if (!in_array($action, $exceptions)) {
49
			// All actions require a token.
50
			$this->gatekeeper($action);
51
		}
52
	
53
		$forwarder = str_replace(_elgg_services()->config->getSiteUrl(), "", $forwarder);
54
		$forwarder = str_replace("http://", "", $forwarder);
55
		$forwarder = str_replace("@", "", $forwarder);
56
		if (substr($forwarder, 0, 1) == "/") {
57
			$forwarder = substr($forwarder, 1);
58
		}
59
	
60
		if (!isset($this->actions[$action])) {
61
			register_error(_elgg_services()->translator->translate('actionundefined', array($action)));
62
		} elseif (!_elgg_services()->session->isAdminLoggedIn() && ($this->actions[$action]['access'] === 'admin')) {
63
			register_error(_elgg_services()->translator->translate('actionunauthorized'));
64
		} elseif (!_elgg_services()->session->isLoggedIn() && ($this->actions[$action]['access'] !== 'public')) {
65
			register_error(_elgg_services()->translator->translate('actionloggedout'));
66
		} else {
67
			// To quietly cancel the action file, return a falsey value in the "action" hook.
68
			if (_elgg_services()->hooks->trigger('action', $action, null, true)) {
69
				if (is_file($this->actions[$action]['file']) && is_readable($this->actions[$action]['file'])) {
70
					self::includeFile($this->actions[$action]['file']);
71
				} else {
72
					register_error(_elgg_services()->translator->translate('actionnotfound', array($action)));
73
				}
74
			}
75
		}
76
	
77
		$forwarder = empty($forwarder) ? REFERER : $forwarder;
78
		forward($forwarder);
79
	}
80
81
	/**
82
	 * Include an action file with isolated scope
83
	 *
84
	 * @param string $file File to be interpreted by PHP
85
	 * @return void
86
	 */
87
	protected static function includeFile($file) {
88
		include $file;
89
	}
90
	
91
	/**
92
	 * @see elgg_register_action
93
	 * @access private
94
	 */
95 2
	public function register($action, $filename = "", $access = 'logged_in') {
96
		// plugins are encouraged to call actions with a trailing / to prevent 301
97
		// redirects but we store the actions without it
98 2
		$action = rtrim($action, '/');
99
	
100 2
		if (empty($filename)) {
101
			
102
			$path = _elgg_services()->config->get('path');
103
			if ($path === null) {
104
				$path = "";
105
			}
106
	
107
			$filename = $path . "actions/" . $action . ".php";
108
		}
109
	
110 2
		$this->actions[$action] = array(
111 2
			'file' => $filename,
112 2
			'access' => $access,
113
		);
114 2
		return true;
115
	}
116
	
117
	/**
118
	 * @see elgg_unregister_action
119
	 * @access private
120
	 */
121 1
	public function unregister($action) {
122 1
		if (isset($this->actions[$action])) {
123 1
			unset($this->actions[$action]);
124 1
			return true;
125
		} else {
126 1
			return false;
127
		}
128
	}
129
130
	/**
131
	 * @see validate_action_token
132
	 * @access private
133
	 */
134
	public function validateActionToken($visible_errors = true, $token = null, $ts = null) {
135
		if (!$token) {
136
			$token = get_input('__elgg_token');
137
		}
138
	
139
		if (!$ts) {
140
			$ts = get_input('__elgg_ts');
141
		}
142
143
		$session_id = _elgg_services()->session->getId();
144
	
145
		if (($token) && ($ts) && ($session_id)) {
146
			if ($this->validateTokenOwnership($token, $ts)) {
147
				if ($this->validateTokenTimestamp($ts)) {
148
					// We have already got this far, so unless anything
149
					// else says something to the contrary we assume we're ok
150
					$returnval = _elgg_services()->hooks->trigger('action_gatekeeper:permissions:check', 'all', array(
151
						'token' => $token,
152
						'time' => $ts
153
					), true);
154
155
					if ($returnval) {
156
						return true;
157
					} else if ($visible_errors) {
158
						register_error(_elgg_services()->translator->translate('actiongatekeeper:pluginprevents'));
159
					}
160 View Code Duplication
				} else if ($visible_errors) {
161
					// this is necessary because of #5133
162
					if (elgg_is_xhr()) {
163
						register_error(_elgg_services()->translator->translate('js:security:token_refresh_failed', array(_elgg_services()->config->getSiteUrl())));
164
					} else {
165
						register_error(_elgg_services()->translator->translate('actiongatekeeper:timeerror'));
166
					}
167
				}
168 View Code Duplication
			} else if ($visible_errors) {
169
				// this is necessary because of #5133
170
				if (elgg_is_xhr()) {
171
					register_error(_elgg_services()->translator->translate('js:security:token_refresh_failed', array(_elgg_services()->config->getSiteUrl())));
172
				} else {
173
					register_error(_elgg_services()->translator->translate('actiongatekeeper:tokeninvalid'));
174
				}
175
			}
176
		} else {
177
			$req = _elgg_services()->request;
178
			$length = $req->server->get('CONTENT_LENGTH');
179
			$post_count = count($req->request);
180
			if ($length && $post_count < 1) {
181
				// The size of $_POST or uploaded file has exceed the size limit
182
				$error_msg = _elgg_services()->hooks->trigger('action_gatekeeper:upload_exceeded_msg', 'all', array(
183
					'post_size' => $length,
184
					'visible_errors' => $visible_errors,
185
				), _elgg_services()->translator->translate('actiongatekeeper:uploadexceeded'));
186
			} else {
187
				$error_msg = _elgg_services()->translator->translate('actiongatekeeper:missingfields');
188
			}
189
			if ($visible_errors) {
190
				register_error($error_msg);
191
			}
192
		}
193
194
		return false;
195
	}
196
197
	/**
198
	 * Is the token timestamp within acceptable range?
199
	 * 
200
	 * @param int $ts timestamp from the CSRF token
201
	 * 
202
	 * @return bool
203
	 */
204
	protected function validateTokenTimestamp($ts) {
205
		$timeout = $this->getActionTokenTimeout();
206
		$now = time();
207
		return ($timeout == 0 || ($ts > $now - $timeout) && ($ts < $now + $timeout));
208
	}
209
210
	/**
211
	 * @see \Elgg\ActionsService::validateActionToken
212
	 * @access private
213
	 * @since 1.9.0
214
	 * @return int number of seconds that action token is valid
215
	 */
216
	public function getActionTokenTimeout() {
217
		if (($timeout = _elgg_services()->config->get('action_token_timeout')) === null) {
218
			// default to 2 hours
219
			$timeout = 2;
220
		}
221
		$hour = 60 * 60;
222
		return (int)((float)$timeout * $hour);
223
	}
224
225
	/**
226
	 * @see action_gatekeeper
227
	 * @access private
228
	 */
229
	public function gatekeeper($action) {
230
		if ($action === 'login') {
231
			if ($this->validateActionToken(false)) {
232
				return true;
233
			}
234
235
			$token = get_input('__elgg_token');
236
			$ts = (int)get_input('__elgg_ts');
237
			if ($token && $this->validateTokenTimestamp($ts)) {
238
				// The tokens are present and the time looks valid: this is probably a mismatch due to the 
239
				// login form being on a different domain.
240
				register_error(_elgg_services()->translator->translate('actiongatekeeper:crosssitelogin'));
241
242
				forward('login', 'csrf');
243
			}
244
245
			// let the validator send an appropriate msg
246
			$this->validateActionToken();
247
248
		} else if ($this->validateActionToken()) {
249
			return true;
250
		}
251
252
		forward(REFERER, 'csrf');
253
	}
254
255
	/**
256
	 * Was the given token generated for the session defined by session_token?
257
	 *
258
	 * @param string $token         CSRF token
259
	 * @param int    $timestamp     Unix time
260
	 * @param string $session_token Session-specific token
261
	 *
262
	 * @return bool
263
	 * @access private
264
	 */
265
	public function validateTokenOwnership($token, $timestamp, $session_token = '') {
266
		$required_token = $this->generateActionToken($timestamp, $session_token);
267
268
		return _elgg_services()->crypto->areEqual($token, $required_token);
0 ignored issues
show
Security Bug introduced by
It seems like $required_token defined by $this->generateActionTok...estamp, $session_token) on line 266 can also be of type false; however, ElggCrypto::areEqual() does only seem to accept string, did you maybe forget to handle an error condition?

This check looks for type mismatches where the missing type is false. This is usually indicative of an error condtion.

Consider the follow example

<?php

function getDate($date)
{
    if ($date !== null) {
        return new DateTime($date);
    }

    return false;
}

This function either returns a new DateTime object or false, if there was an error. This is a typical pattern in PHP programming to show that an error has occurred without raising an exception. The calling code should check for this returned false before passing on the value to another function or method that may not be able to handle a false.

Loading history...
269
	}
270
	
271
	/**
272
	 * Generate a token from a session token (specifying the user), the timestamp, and the site key.
273
	 *
274
	 * @see generate_action_token
275
	 *
276
	 * @param int    $timestamp     Unix timestamp
277
	 * @param string $session_token Session-specific token
278
	 *
279
	 * @return string
280
	 * @access private
281
	 */
282
	public function generateActionToken($timestamp, $session_token = '') {
283
		if (!$session_token) {
284
			$session_token = elgg_get_session()->get('__elgg_session');
285
			if (!$session_token) {
286
				return false;
287
			}
288
		}
289
290
		return _elgg_services()->crypto->getHmac([(int)$timestamp, $session_token], 'md5')
291
			->getToken();
292
	}
293
	
294
	/**
295
	 * @see elgg_action_exists
296
	 * @access private
297
	 */
298 3
	public function exists($action) {
299 3
		return (isset($this->actions[$action]) && file_exists($this->actions[$action]['file']));
300
	}
301
	
302
	/**
303
	 * @see ajax_forward_hook
304
	 * @access private
305
	 */
306
	public function ajaxForwardHook($hook, $reason, $return, $params) {
307
		if (elgg_is_xhr()) {
308
			// always pass the full structure to avoid boilerplate JS code.
309
			$params = array_merge($params, array(
310
				'output' => '',
311
				'status' => 0,
312
				'system_messages' => array(
313
					'error' => array(),
314
					'success' => array()
315
				)
316
			));
317
	
318
			//grab any data echo'd in the action
319
			$output = ob_get_clean();
320
	
321
			//Avoid double-encoding in case data is json
322
			$json = json_decode($output);
323
			if (isset($json)) {
324
				$params['output'] = $json;
325
			} else {
326
				$params['output'] = $output;
327
			}
328
	
329
			//Grab any system messages so we can inject them via ajax too
330
			$system_messages = _elgg_services()->systemMessages->dumpRegister();
331
	
332
			if (isset($system_messages['success'])) {
333
				$params['system_messages']['success'] = $system_messages['success'];
334
			}
335
	
336
			if (isset($system_messages['error'])) {
337
				$params['system_messages']['error'] = $system_messages['error'];
338
				$params['status'] = -1;
339
			}
340
341
			if ($reason == 'walled_garden') {
342
				$reason = '403';
343
			}
344
			$httpCodes = array(
345
				'400' => 'Bad Request',
346
				'401' => 'Unauthorized',
347
				'403' => 'Forbidden',
348
				'404' => 'Not Found',
349
				'407' => 'Proxy Authentication Required',
350
				'500' => 'Internal Server Error',
351
				'503' => 'Service Unavailable',
352
			);
353
354
			if (isset($httpCodes[$reason])) {
355
				header("HTTP/1.1 $reason {$httpCodes[$reason]}", true);
356
			}
357
358
			$context = array('action' => $this->currentAction);
359
			$params = _elgg_services()->hooks->trigger('output', 'ajax', $context, $params);
360
	
361
			// Check the requester can accept JSON responses, if not fall back to
362
			// returning JSON in a plain-text response.  Some libraries request
363
			// JSON in an invisible iframe which they then read from the iframe,
364
			// however some browsers will not accept the JSON MIME type.
365
			$http_accept = _elgg_services()->request->server->get('HTTP_ACCEPT');
366
			if (stripos($http_accept, 'application/json') === false) {
367
				header("Content-type: text/plain;charset=utf-8");
368
			} else {
369
				header("Content-type: application/json;charset=utf-8");
370
			}
371
	
372
			echo json_encode($params);
373
			exit;
374
		}
375
	}
376
	
377
	/**
378
	 * @see ajax_action_hook
379
	 * @access private
380
	 */
381
	public function ajaxActionHook() {
382
		if (elgg_is_xhr()) {
383
			ob_start();
384
		}
385
	}
386
387
	/**
388
	 * Get all actions
389
	 * 
390
	 * @return array
391
	 */
392
	public function getAllActions() {
393
		return $this->actions;
394
	}
395
}
396
397