This project does not seem to handle request data directly as such no vulnerable execution paths were found.
include
, or for example
via PHP's auto-loading mechanism.
These results are based on our legacy PHP analysis, consider migrating to our new PHP analysis engine instead. Learn more
1 | <?php |
||
2 | /** |
||
3 | * @author Todd Burry <[email protected]> |
||
4 | * @copyright 2009-2014 Vanilla Forums Inc. |
||
5 | * @license MIT |
||
6 | */ |
||
7 | |||
8 | namespace Garden; |
||
9 | |||
10 | |||
11 | /** |
||
12 | * A class that provides functionality for encrypting and signing strings and then decoding those secure strings. |
||
13 | */ |
||
14 | class SecureString { |
||
15 | const SEP = '.'; |
||
16 | const EOS = '-'; |
||
17 | const STRICT = 'strict'; |
||
18 | |||
19 | protected $timestampExpiry; |
||
20 | |||
21 | /** |
||
22 | * Initialize an instance of the {@link SecureString} class. |
||
23 | */ |
||
24 | 440 | public function __construct() { |
|
25 | 440 | $this->timestampExpiry = strtotime('1 day', 0); |
|
26 | 440 | } |
|
27 | |||
28 | /** |
||
29 | * Get or set the timestamp expiry in seconds. |
||
30 | * |
||
31 | * @param int $value Set a new expiry value in seconds. |
||
32 | * @return int Returns the current timestamp expiry. |
||
33 | */ |
||
34 | 1 | public function timestampExpiry($value = 0) { |
|
35 | 1 | if ($value) { |
|
36 | 1 | $this->timestampExpiry = $value; |
|
37 | 1 | } |
|
38 | 1 | return $this->timestampExpiry; |
|
39 | } |
||
40 | |||
41 | /** |
||
42 | * Encode a string using a secure specification. |
||
43 | * |
||
44 | * @param mixed $data The data to encode. This must be data that supports json serialization. |
||
45 | * @param array $spec An array specifying how the string should be encoded. |
||
46 | * The array should be in the form ['spec1' => 'password', 'spec2' => 'password']. |
||
47 | * @param bool $throw Whether or not to throw an exception on error. |
||
48 | * @return string|null Returns the encoded string or null if there is an error. |
||
49 | */ |
||
50 | 438 | public function encode($data, array $spec, $throw = false) { |
|
51 | 438 | $str = json_encode($data, JSON_UNESCAPED_SLASHES); |
|
52 | |||
53 | 438 | $first = true; |
|
54 | 438 | foreach ($spec as $name => $password) { |
|
55 | 438 | if ($name === self::STRICT) { |
|
56 | // Strict is just an option so continue. |
||
57 | 1 | continue; |
|
58 | } |
||
59 | |||
60 | 438 | $supported = $this->supportedInfo($name, $throw); |
|
61 | 438 | if ($supported === null) { |
|
62 | 1 | return null; |
|
63 | } |
||
64 | |||
65 | 437 | list($encode,, $method, $encodeFirst) = $supported; |
|
66 | |||
67 | 437 | if ($first) { |
|
68 | 437 | if ($encodeFirst) { |
|
69 | 195 | $str = static::base64urlEncode($str); |
|
70 | 195 | } |
|
71 | 437 | $this->pushString($str, self::EOS); |
|
72 | 437 | } |
|
73 | |||
74 | View Code Duplication | switch ($encode) { |
|
75 | 437 | case 'encrypt': |
|
76 | 290 | $str = $this->encrypt($str, $method, $password, '', $throw); |
|
77 | 290 | break; |
|
78 | 292 | case 'hmac': |
|
79 | 292 | $str = $this->hmac($str, $method, $password, 0, $throw); |
|
80 | 292 | break; |
|
81 | default: |
||
82 | return $this->exception($throw, "Invalid method $encode.", 500); |
||
0 ignored issues
–
show
Bug
Compatibility
introduced
by
![]() |
|||
83 | } |
||
84 | // Return on error. |
||
85 | 437 | if (!$str) { |
|
86 | return null; |
||
87 | } |
||
88 | |||
89 | 437 | $this->pushString($str, $name); |
|
90 | |||
91 | 437 | $first = false; |
|
92 | 437 | } |
|
93 | |||
94 | 437 | return $str; |
|
95 | } |
||
96 | |||
97 | /** |
||
98 | * Decode a string that was encoded using {@link SecureString::encode()}. |
||
99 | * |
||
100 | * @param string $str The encoded string. |
||
101 | * @param array $spec An array specifying how the string should be encoded. |
||
102 | * @param bool $throw Whether or not to throw an exception on error. |
||
103 | * @return string|null Returns the decoded string or null of there was a problem. |
||
104 | */ |
||
105 | 439 | public function decode($str, array $spec, $throw = false) { |
|
106 | 439 | $decodeFirst = false; |
|
107 | 439 | $wstr = $str; // wstr means working string |
|
108 | 439 | $used = []; |
|
109 | |||
110 | 439 | $strict = true; |
|
111 | 439 | if (array_key_exists(self::STRICT, $spec)) { |
|
112 | 2 | $strict = (bool)$spec[self::STRICT]; |
|
113 | 2 | unset($spec[self::STRICT]); |
|
114 | 2 | } |
|
115 | |||
116 | 439 | while ($token = $this->popString($wstr)) { |
|
117 | 439 | if ($token === self::EOS) { |
|
118 | 147 | if ($decodeFirst) { |
|
119 | 65 | $wstr = static::base64urlDecode($wstr); |
|
120 | 65 | } |
|
121 | 147 | break; |
|
122 | } |
||
123 | |||
124 | try { |
||
125 | 438 | $supported = $this->supportedInfo($token, true); |
|
126 | 438 | } catch (\Exception $ex) { |
|
127 | 2 | return $this->exception($throw, $ex->getMessage(), 403); |
|
0 ignored issues
–
show
|
|||
128 | } |
||
129 | |||
130 | 437 | if (!isset($spec[$token])) { |
|
131 | 144 | return $this->exception($throw, "You did not provide a password for $token.", 403); |
|
0 ignored issues
–
show
|
|||
132 | } |
||
133 | 293 | $password = $spec[$token]; |
|
134 | 293 | array_unshift($used, $token); |
|
135 | |||
136 | 293 | list(, $decode, $method, $decodeFirst) = $supported; |
|
137 | |||
138 | View Code Duplication | switch ($decode) { |
|
139 | 293 | case 'decrypt': |
|
140 | 162 | $wstr = $this->decrypt($wstr, $method, $password, $throw); |
|
141 | 162 | break; |
|
142 | 180 | case 'verifyHmac': |
|
143 | 180 | $wstr = $this->verifyHmac($wstr, $method, $password, $throw); |
|
144 | 180 | break; |
|
145 | default: |
||
146 | return $this->exception($throw, "Invalid method $decode.", 500); |
||
0 ignored issues
–
show
|
|||
147 | } |
||
148 | |||
149 | 293 | if ($wstr === null) { |
|
150 | 146 | return null; |
|
151 | } |
||
152 | 147 | } |
|
153 | |||
154 | // Make sure that the string was secured at all. |
||
155 | 147 | if ($strict && array_keys($spec) != $used) { |
|
156 | 1 | return $this->exception($throw, 'The string is not fully secure.', 403); |
|
0 ignored issues
–
show
|
|||
157 | 147 | } elseif (empty($used)) { |
|
158 | 1 | return $this->exception($throw, 'The string is not secure.', 403); |
|
0 ignored issues
–
show
|
|||
159 | } |
||
160 | |||
161 | 146 | $data = json_decode($wstr, true); |
|
162 | 146 | if ($data === null) { |
|
163 | return $this->exception($throw, 'The final string is not valid json.', 403); |
||
0 ignored issues
–
show
|
|||
164 | } |
||
165 | |||
166 | 146 | return $data; |
|
167 | } |
||
168 | |||
169 | /** |
||
170 | * Generate a random string suitable for use as an encryption or signature key. |
||
171 | * |
||
172 | * @param int $len The number of characters in the key. |
||
173 | * @return string Returns a base64url encoded string representing the random key. |
||
174 | */ |
||
175 | 2 | public static function generateRandomKey($len = 32) { |
|
176 | 2 | $bytes = ceil($len * 3 / 4); |
|
177 | |||
178 | 2 | return substr(self::base64urlEncode(openssl_random_pseudo_bytes($bytes)), 0, $len); |
|
179 | } |
||
180 | |||
181 | /** |
||
182 | * Base64 Encode a string, but make it suitable to be passed in a url. |
||
183 | * |
||
184 | * @param string $str The string to encode. |
||
185 | * @return string The encoded string. |
||
186 | */ |
||
187 | 437 | protected static function base64urlEncode($str) { |
|
188 | 437 | return trim(strtr(base64_encode($str), '+/', '-_'), '='); |
|
189 | } |
||
190 | |||
191 | /** |
||
192 | * Decode a string that was encoded using base64UrlEncode(). |
||
193 | * |
||
194 | * @param string $str The encoded string. |
||
195 | * @return string The decoded string. |
||
196 | */ |
||
197 | 293 | protected static function base64urlDecode($str) { |
|
198 | 293 | return base64_decode(strtr($str, '-_', '+/')); |
|
199 | } |
||
200 | |||
201 | /** |
||
202 | * Encrypt a string with {@link openssl_encrypt()}. |
||
203 | * |
||
204 | * @param string $str The string to encrypt. |
||
205 | * @param string $method The encryption cipher. |
||
206 | * @param string $password The encryption password. |
||
207 | * @param string $iv The input vector. |
||
208 | * @param bool $throw Whether or not to throw an exception on error. |
||
209 | * @return string Returns the encrypted string. |
||
210 | */ |
||
211 | 290 | View Code Duplication | protected function encrypt($str, $method, $password, $iv = '', $throw = false) { |
212 | 290 | if ($iv === '') { |
|
213 | 290 | $iv = openssl_random_pseudo_bytes(openssl_cipher_iv_length($method)); |
|
214 | 290 | } |
|
215 | // Encrypt the string. |
||
216 | 290 | $encrypted = openssl_encrypt($str, $method, $password, OPENSSL_RAW_DATA, $iv); |
|
217 | |||
218 | 290 | if ($encrypted === false) { |
|
219 | return $this->exception($throw, "Error encrypting the string.", 400); |
||
0 ignored issues
–
show
|
|||
220 | } |
||
221 | |||
222 | 290 | $str = static::base64urlEncode($encrypted); |
|
223 | 290 | $this->pushString($str, static::base64urlEncode($iv)); |
|
224 | |||
225 | 290 | return $str; |
|
226 | } |
||
227 | |||
228 | /** |
||
229 | * Decrypt a string with {@link openssl_decrypt()}. |
||
230 | * |
||
231 | * @param string $str The base64 url encoded encrypted string. |
||
232 | * @param string $method The encryption cipher. |
||
233 | * @param string $password The password to decyrpt the string. |
||
234 | * @param bool $throw Whether or not to decode the string on exception. |
||
235 | * @return string|null Returns the decrypted string or null on error. |
||
236 | */ |
||
237 | 162 | protected function decrypt($str, $method, $password, $throw = false) { |
|
238 | 162 | $iv = static::base64urlDecode($this->popString($str)); |
|
239 | 162 | $encrypted = static::base64urlDecode($this->popString($str)); |
|
240 | |||
241 | try { |
||
242 | 162 | $decrypted = openssl_decrypt($encrypted, $method, $password, true, $iv); |
|
243 | 162 | } catch (\Exception $ex) { |
|
244 | return $this->exception($throw, "Error decrypting the string.", 403); |
||
245 | } |
||
246 | |||
247 | 162 | if ($decrypted === false) { |
|
248 | 64 | return $this->exception($throw, "Error decrypting the string.", 403); |
|
249 | } |
||
250 | |||
251 | 98 | return $decrypted; |
|
252 | } |
||
253 | |||
254 | /** |
||
255 | * Get information about a supported spec name. |
||
256 | * |
||
257 | * @param string $name The name of the spec. |
||
258 | * @param bool $throw Whether or not throw an exception on error. |
||
259 | * @return array|null Returns an array in the form [encode, decode, method, encodeFirst]. |
||
260 | * Returns null on error. |
||
261 | */ |
||
262 | 439 | protected function supportedInfo($name, $throw = false) { |
|
263 | switch ($name) { |
||
264 | 439 | case 'aes128': |
|
265 | 439 | case 'aes256': |
|
266 | 291 | $cipher = 'aes-'.substr($name, 3).'-cbc'; |
|
267 | 291 | return ['encrypt', 'decrypt', $cipher, false]; |
|
268 | 295 | case 'hsha1': |
|
269 | 295 | case 'hsha256': |
|
270 | 292 | $hash = substr($name, 1); |
|
271 | 292 | return ['hmac', 'verifyHmac', $hash, true]; |
|
272 | } |
||
273 | 3 | return $this->exception($throw, "Spec $name not supported.", 400); |
|
274 | } |
||
275 | |||
276 | /** |
||
277 | * Sign a string with hmac and a hash method. |
||
278 | * |
||
279 | * @param string $str The string to sign. |
||
280 | * @param string $method The hash method used to sign the string. |
||
281 | * @param string $password The password used to hmac hash the string with. |
||
282 | * @param int $timestamp The timestamp used to sign the string with or 0 to use the current time. |
||
283 | * @param bool $throw Whether or not the throw an exception on error. |
||
284 | * @return string Returns the string with signing information or null on error. |
||
285 | */ |
||
286 | 292 | View Code Duplication | protected function hmac($str, $method, $password, $timestamp = 0, $throw = false) { |
287 | 292 | if ($timestamp === 0) { |
|
288 | 292 | $timestamp = time(); |
|
289 | 292 | } |
|
290 | // Add the timestamp to the string. |
||
291 | 292 | static::pushString($str, $timestamp); |
|
292 | |||
293 | // Sign the string. |
||
294 | 292 | $signature = hash_hmac($method, $str, $password, true); |
|
295 | 292 | if ($signature === false) { |
|
296 | return $this->exception($throw, "Invalid hash method $method.", 400); |
||
0 ignored issues
–
show
|
|||
297 | } |
||
298 | |||
299 | // Add the signature to the string. |
||
300 | 292 | static::pushString($str, static::base64urlEncode($signature)); |
|
301 | |||
302 | 292 | return $str; |
|
303 | } |
||
304 | |||
305 | /** |
||
306 | * Verify the signature on a secure string. |
||
307 | * |
||
308 | * @param string $str The string to verify. |
||
309 | * @param string $method The hashing algorithm that the string was signed with. |
||
310 | * @param string $password The password used to sign the string. |
||
311 | * @param bool $throw Whether or not to throw an exeptio on error. |
||
312 | * @return bool Returns the string without the signing information or null on error. |
||
313 | */ |
||
314 | 180 | protected function verifyHmac($str, $method, $password, $throw = false) { |
|
315 | // Grab the signature from the string. |
||
316 | 180 | $signature = $this->popString($str); |
|
317 | 180 | if (!$signature) { |
|
0 ignored issues
–
show
The expression
$signature of type string|null is loosely compared to false ; this is ambiguous if the string can be empty. You might want to explicitly use === null instead.
In PHP, under loose comparison (like For '' == false // true
'' == null // true
'ab' == false // false
'ab' == null // false
// It is often better to use strict comparison
'' === false // false
'' === null // false
![]() |
|||
318 | return $this->exception($throw, "The signature is missing.", 403); |
||
319 | } |
||
320 | 180 | $signature = static::base64urlDecode($signature); |
|
321 | |||
322 | // Recalculate the signature to compare. |
||
323 | 180 | $calcSignature = hash_hmac($method, $str, $password, true); |
|
324 | |||
325 | 180 | if (strlen($signature) !== strlen($calcSignature)) { |
|
326 | return $this->exception($throw, "The signature is invalid.", 403); |
||
327 | } |
||
328 | |||
329 | // Do a double hmac comparison to prevent timing attacks. |
||
330 | // https://www.isecpartners.com/blog/2011/february/double-hmac-verification.aspx |
||
331 | 180 | $dblSignature = hash_hmac($method, $signature, $password, true); |
|
332 | 180 | $dblCalcSignature = hash_hmac($method, $calcSignature, $password, true); |
|
333 | |||
334 | 180 | if ($dblSignature !== $dblCalcSignature) { |
|
335 | 81 | return $this->exception($throw, "The signature is invalid.", 403); |
|
336 | } |
||
337 | |||
338 | // Grab the timestamp and verify it. |
||
339 | 99 | $timestamp = $this->popString($str); |
|
340 | 99 | if (!$this->verifyTimestamp($timestamp, $throw)) { |
|
341 | 1 | return null; |
|
342 | } |
||
343 | |||
344 | 98 | return $str; |
|
345 | } |
||
346 | |||
347 | /** |
||
348 | * Verify that a timestamp hasn't expired. |
||
349 | * |
||
350 | * @param string $timestamp The unix timestamp to verify. |
||
351 | * @param bool $throw Whether or not to throw an exception on error. |
||
352 | * @return bool Returns true if the timestamp is valid or false otherwise. |
||
353 | */ |
||
354 | 99 | protected function verifyTimestamp($timestamp, $throw = false) { |
|
355 | 99 | if (!is_numeric($timestamp)) { |
|
356 | return (bool)$this->exception($throw, "Invalid timestamp.", 403); |
||
357 | } |
||
358 | 99 | $intTimestamp = (int)$timestamp; |
|
359 | 99 | $now = time(); |
|
360 | 99 | if ($intTimestamp + $this->timestampExpiry <= $now) { |
|
361 | 1 | return (bool)$this->exception($throw, "The timestamp has expired.", 403); |
|
362 | } |
||
363 | |||
364 | 98 | return true; |
|
365 | } |
||
366 | |||
367 | /** |
||
368 | * Pop a string off of the end of an encoded secure string. |
||
369 | * |
||
370 | * @param string &$str The main string to pop. |
||
371 | * @return string|null Returns the popped string or null if {@link $str} is empty. |
||
372 | */ |
||
373 | 439 | protected function popString(&$str) { |
|
374 | 439 | if ($str === '') { |
|
375 | return null; |
||
376 | } |
||
377 | |||
378 | 439 | $pos = strrpos($str, '.'); |
|
379 | 439 | if ($pos !== false) { |
|
380 | 439 | $result = substr($str, $pos + 1); |
|
381 | 439 | $str = substr($str, 0, $pos); |
|
382 | 439 | } else { |
|
383 | 162 | $result = $str; |
|
384 | 162 | $str = ''; |
|
385 | } |
||
386 | 439 | return $result; |
|
387 | } |
||
388 | |||
389 | /** |
||
390 | * Pushes a string on to the end of an encoded secure string. |
||
391 | * |
||
392 | * @param string &$str The main string to push to. |
||
393 | * @param string|array $item The string or array of strings to push on to the end of {@link $str}. |
||
394 | */ |
||
395 | 437 | protected function pushString(&$str, $item) { |
|
396 | 437 | if ($str) { |
|
397 | 437 | $str .= static::SEP; |
|
398 | 437 | } |
|
399 | 437 | $str .= implode(static::SEP, (array)$item); |
|
400 | 437 | } |
|
401 | |||
402 | /** |
||
403 | * Throw an exception or return false. |
||
404 | * |
||
405 | * @param bool $throw Whether or not to throw an exception. |
||
406 | * @param string $message The exception message. |
||
407 | * @param int $code The exception code. |
||
408 | * @return bool Returns false if {@link $throw} is false. |
||
409 | * @throws \Exception Throws an exception with {@link $message} and {@link $code}. |
||
410 | */ |
||
411 | 295 | protected function exception($throw, $message, $code) { |
|
412 | 295 | if ($throw) { |
|
413 | 295 | throw new \Exception($message, $code); |
|
414 | } |
||
415 | 294 | return null; |
|
416 | } |
||
417 | |||
418 | /** |
||
419 | * Twiddle a value in an encoded secure string to another value. |
||
420 | * |
||
421 | * This method is mainly for testing so that an invalid string can be created. |
||
422 | * |
||
423 | * @param string $string A valid cookie to twiddle. |
||
424 | * @param int $index The index of the new value. |
||
425 | * @param string $value The new value. This will be base64url encoded. |
||
426 | * @param bool $encode Whether or not to base64 url encode the value. |
||
427 | * @return string Returns the new encoded cookie. |
||
428 | */ |
||
429 | 2 | public function twiddle($string, $index, $value, $encode = false) { |
|
430 | 2 | $parts = explode(static::SEP, $string); |
|
431 | |||
432 | 2 | if ($encode) { |
|
433 | $value = static::base64urlEncode($value); |
||
434 | } |
||
435 | 2 | $parts[$index] = $value; |
|
436 | |||
437 | 2 | return implode(static::SEP, $parts); |
|
438 | } |
||
439 | } |
||
440 |