Issues (48)

Security Analysis    not enabled

This project does not seem to handle request data directly as such no vulnerable execution paths were found.

  Cross-Site Scripting
Cross-Site Scripting enables an attacker to inject code into the response of a web-request that is viewed by other users. It can for example be used to bypass access controls, or even to take over other users' accounts.
  File Exposure
File Exposure allows an attacker to gain access to local files that he should not be able to access. These files can for example include database credentials, or other configuration files.
  File Manipulation
File Manipulation enables an attacker to write custom data to files. This potentially leads to injection of arbitrary code on the server.
  Object Injection
Object Injection enables an attacker to inject an object into PHP code, and can lead to arbitrary code execution, file exposure, or file manipulation attacks.
  Code Injection
Code Injection enables an attacker to execute arbitrary code on the server.
  Response Splitting
Response Splitting can be used to send arbitrary responses.
  File Inclusion
File Inclusion enables an attacker to inject custom files into PHP's file loading mechanism, either explicitly passed to include, or for example via PHP's auto-loading mechanism.
  Command Injection
Command Injection enables an attacker to inject a shell command that is execute with the privileges of the web-server. This can be used to expose sensitive data, or gain access of your server.
  SQL Injection
SQL Injection enables an attacker to execute arbitrary SQL code on your database server gaining access to user data, or manipulating user data.
  XPath Injection
XPath Injection enables an attacker to modify the parts of XML document that are read. If that XML document is for example used for authentication, this can lead to further vulnerabilities similar to SQL Injection.
  LDAP Injection
LDAP Injection enables an attacker to inject LDAP statements potentially granting permission to run unauthorized queries, or modify content inside the LDAP tree.
  Header Injection
  Other Vulnerability
This category comprises other attack vectors such as manipulating the PHP runtime, loading custom extensions, freezing the runtime, or similar.
  Regex Injection
Regex Injection enables an attacker to execute arbitrary code in your PHP process.
  XML Injection
XML Injection enables an attacker to read files on your local filesystem including configuration files, or can be abused to freeze your web-server process.
  Variable Injection
Variable Injection enables an attacker to overwrite program variables with custom data, and can lead to further vulnerabilities.
Unfortunately, the security analysis is currently not available for your project. If you are a non-commercial open-source project, please contact support to gain access.

src/SecureString.php (10 issues)

Upgrade to new PHP Analysis Engine

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
The expression $this->exception($throw,...thod {$encode}.", 500); of type boolean|null adds the type boolean to the return on line 82 which is incompatible with the return type documented by Garden\SecureString::encode of type string|null.
Loading history...
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
Bug Compatibility introduced by
The expression $this->exception($throw, $ex->getMessage(), 403); of type boolean|null adds the type boolean to the return on line 127 which is incompatible with the return type documented by Garden\SecureString::decode of type string|null.
Loading history...
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
Bug Compatibility introduced by
The expression $this->exception($throw,...d for {$token}.", 403); of type boolean|null adds the type boolean to the return on line 131 which is incompatible with the return type documented by Garden\SecureString::decode of type string|null.
Loading history...
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
Bug Compatibility introduced by
The expression $this->exception($throw,...thod {$decode}.", 500); of type boolean|null adds the type boolean to the return on line 146 which is incompatible with the return type documented by Garden\SecureString::decode of type string|null.
Loading history...
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
Bug Compatibility introduced by
The expression $this->exception($throw,...t fully secure.', 403); of type boolean|null adds the type boolean to the return on line 156 which is incompatible with the return type documented by Garden\SecureString::decode of type string|null.
Loading history...
157 147
        } elseif (empty($used)) {
158 1
            return $this->exception($throw, 'The string is not secure.', 403);
0 ignored issues
show
Bug Compatibility introduced by
The expression $this->exception($throw,... is not secure.', 403); of type boolean|null adds the type boolean to the return on line 158 which is incompatible with the return type documented by Garden\SecureString::decode of type string|null.
Loading history...
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
Bug Compatibility introduced by
The expression $this->exception($throw,...not valid json.', 403); of type boolean|null adds the type boolean to the return on line 163 which is incompatible with the return type documented by Garden\SecureString::decode of type string|null.
Loading history...
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
Bug Compatibility introduced by
The expression $this->exception($throw,...ing the string.', 400); of type boolean|null adds the type boolean to the return on line 219 which is incompatible with the return type documented by Garden\SecureString::encrypt of type string.
Loading history...
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
Bug Compatibility introduced by
The expression $this->exception($throw,...thod {$method}.", 400); of type boolean|null adds the type boolean to the return on line 296 which is incompatible with the return type documented by Garden\SecureString::hmac of type string.
Loading history...
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
Bug Best Practice introduced by
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 ==, or !=, or switch conditions), values of different types might be equal.

For string values, the empty string '' is a special case, in particular the following results might be unexpected:

''   == false // true
''   == null  // true
'ab' == false // false
'ab' == null  // false

// It is often better to use strict comparison
'' === false // false
'' === null  // false
Loading history...
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