ParameterConfirmationToken.php
6.7 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
<?php
/**
* Class ParameterConfirmationToken
*
* When you need to use a dangerous GET parameter that needs to be set before core/Core.php is
* established, this class takes care of allowing some other code of confirming the parameter,
* by generating a one-time-use token & redirecting with that token included in the redirected URL
*
* WARNING: This class is experimental and designed specifically for use pre-startup in main.php
* It will likely be heavily refactored before the release of 3.2
*/
class ParameterConfirmationToken {
/**
* The name of the parameter
*
* @var string
*/
protected $parameterName = null;
/**
* The parameter given
*
* @var string|null The string value, or null if not provided
*/
protected $parameter = null;
/**
* The validated and checked token for this parameter
*
* @var string|null A string value, or null if either not provided or invalid
*/
protected $token = null;
protected function pathForToken($token) {
return TEMP_FOLDER.'/token_'.preg_replace('/[^a-z0-9]+/', '', $token);
}
/**
* Generate a new random token and store it
*
* @return string Token name
*/
protected function genToken() {
// Generate a new random token (as random as possible)
require_once(dirname(dirname(dirname(__FILE__))).'/security/RandomGenerator.php');
$rg = new RandomGenerator();
$token = $rg->randomToken('md5');
// Store a file in the session save path (safer than /tmp, as open_basedir might limit that)
file_put_contents($this->pathForToken($token), $token);
return $token;
}
/**
* Validate a token
*
* @param string $token
* @return boolean True if the token is valid
*/
protected function checkToken($token) {
if(!$token) {
return false;
}
$file = $this->pathForToken($token);
$content = null;
if (file_exists($file)) {
$content = file_get_contents($file);
unlink($file);
}
return $content == $token;
}
/**
* Create a new ParameterConfirmationToken
*
* @param string $parameterName Name of the querystring parameter to check
*/
public function __construct($parameterName) {
// Store the parameter name
$this->parameterName = $parameterName;
// Store the parameter value
$this->parameter = isset($_GET[$parameterName]) ? $_GET[$parameterName] : null;
// If the token provided is valid, mark it as such
$token = isset($_GET[$parameterName.'token']) ? $_GET[$parameterName.'token'] : null;
if ($this->checkToken($token)) {
$this->token = $token;
}
}
/**
* Get the name of this token
*
* @return string
*/
public function getName() {
return $this->parameterName;
}
/**
* Is the parameter requested?
* ?parameter and ?parameter=1 are both considered requested
*
* @return bool
*/
public function parameterProvided() {
return $this->parameter !== null;
}
/**
* Is the necessary token provided for this parameter?
* A value must be provided for the token
*
* @return bool
*/
public function tokenProvided() {
return !empty($this->token);
}
/**
* Is this parameter requested without a valid token?
*
* @return bool True if the parameter is given without a valid token
*/
public function reloadRequired() {
return $this->parameterProvided() && !$this->tokenProvided();
}
/**
* Suppress the current parameter by unsetting it from $_GET
*/
public function suppress() {
unset($_GET[$this->parameterName]);
}
/**
* Determine the querystring parameters to include
*
* @return array List of querystring parameters with name and token parameters
*/
public function params() {
return array(
$this->parameterName => $this->parameter,
$this->parameterName.'token' => $this->genToken()
);
}
/** What to use instead of BASE_URL. Must not contain protocol or host. @var string */
static public $alternateBaseURL = null;
protected function currentAbsoluteURL() {
global $url;
// Are we http or https? Replicates Director::is_https() without its dependencies/
$proto = 'http';
if(
TRUSTED_PROXY
&& isset($_SERVER['HTTP_X_FORWARDED_PROTO'])
&& strtolower($_SERVER['HTTP_X_FORWARDED_PROTO']) == 'https'
) {
// Convention for (non-standard) proxy signaling a HTTPS forward,
// see https://en.wikipedia.org/wiki/List_of_HTTP_header_fields
$proto = 'https';
} else if(
TRUSTED_PROXY
&& isset($_SERVER['HTTP_X_FORWARDED_PROTOCOL'])
&& strtolower($_SERVER['HTTP_X_FORWARDED_PROTOCOL']) == 'https'
) {
// Less conventional proxy header
$proto = 'https';
} else if(
isset($_SERVER['HTTP_FRONT_END_HTTPS'])
&& strtolower($_SERVER['HTTP_FRONT_END_HTTPS']) == 'on'
) {
// Microsoft proxy convention: https://support.microsoft.com/?kbID=307347
$proto = 'https';
} else if((!empty($_SERVER['HTTPS']) && $_SERVER['HTTPS'] != 'off')) {
$proto = 'https';
} else if(isset($_SERVER['SSL'])) {
$proto = 'https';
}
if((!empty($_SERVER['HTTPS']) && $_SERVER['HTTPS'] != 'off')) $proto = 'https';
if(isset($_SERVER['SSL'])) $proto = 'https';
$parts = array_filter(array(
// What's our host
$_SERVER['HTTP_HOST'],
// SilverStripe base
self::$alternateBaseURL !== null ? self::$alternateBaseURL : BASE_URL,
// And URL
$url
));
// Join together with protocol into our current absolute URL, avoiding duplicated "/" characters
return "$proto://" . preg_replace('#/{2,}#', '/', implode('/', $parts));
}
/**
* Forces a reload of the request with the token included
* This method will terminate the script with `die`
*/
public function reloadWithToken() {
$location = $this->currentAbsoluteURL();
// What's our GET params (ensuring they include the original parameter + a new token)
$params = array_merge($_GET, $this->params());
unset($params['url']);
if ($params) $location .= '?'.http_build_query($params);
// And redirect
if (headers_sent()) {
echo "
<script>location.href='$location';</script>
<noscript><meta http-equiv='refresh' content='0; url=$location'></noscript>
You are being redirected. If you are not redirected soon, <a href='$location'>click here to continue the flush</a>
";
}
else header('location: '.$location, true, 302);
die;
}
/**
* Given a list of token names, suppress all tokens that have not been validated, and
* return the non-validated token with the highest priority
*
* @param array $keys List of token keys in ascending priority (low to high)
* @return ParameterConfirmationToken The token container for the unvalidated $key given with the highest priority
*/
public static function prepare_tokens($keys) {
$target = null;
foreach($keys as $key) {
$token = new ParameterConfirmationToken($key);
// Validate this token
if($token->reloadRequired()) {
$token->suppress();
$target = $token;
}
}
return $target;
}
}