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