Passed
Push — 1.11.x ( 36449a...d7ead2 )
by Angel Fernando Quiroz
14:38
created

Security   F

Complexity

Total Complexity 83

Size/Duplication

Total Lines 613
Duplicated Lines 0 %

Importance

Changes 2
Bugs 0 Features 0
Metric Value
wmc 83
eloc 265
c 2
b 0
f 0
dl 0
loc 613
rs 2

20 Methods

Rating   Name   Duplication   Size   Complexity  
A check_abs_path() 0 30 6
A get_ua() 0 5 1
A generateSecTokenVariable() 0 7 2
A get_existing_token() 0 8 2
A cleanPath() 0 5 1
A getPasswordRequirements() 0 19 2
A check_rel_path() 0 18 4
B getPasswordRequirementsToString() 0 47 7
A filter_filename() 0 3 1
A check_ua() 0 10 2
B filter_terms() 0 31 7
A clear_token() 0 5 1
D check_token() 0 38 20
A get() 0 7 2
A getTokenFromSession() 0 5 1
B filter_img_path() 0 35 8
A get_HTML_token() 0 8 1
F remove_XSS() 0 137 13
A sanitizeExecParam() 0 5 1
A get_token() 0 7 1

How to fix   Complexity   

Complex Class

Complex classes like Security often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

While breaking up the class, it is a good idea to analyze how other classes use Security, and based on these observations, apply Extract Interface, too.

1
<?php
2
3
/* For licensing terms, see /license.txt */
4
5
use Chamilo\CoreBundle\Component\HTMLPurifier\Filter\AllowIframes;
6
use ChamiloSession as Session;
7
8
/**
9
 * This is the security library for Chamilo.
10
 *
11
 * This library is based on recommendations found in the PHP5 Certification
12
 * Guide published at PHP|Architect, and other recommendations found on
13
 * http://www.phpsec.org/
14
 * The principles here are that all data is tainted (most scripts of Chamilo are
15
 * open to the public or at least to a certain public that could be malicious
16
 * under specific circumstances). We use the white list approach, whereas we
17
 * consider that data can only be used in the database or in a file if it has
18
 * been filtered.
19
 *
20
 * For session fixation, use ...
21
 * For session hijacking, use get_ua() and check_ua()
22
 * For Cross-Site Request Forgeries, use get_token() and check_token()
23
 * For basic filtering, use filter()
24
 * For files inclusions (using dynamic paths) use check_rel_path() and check_abs_path()
25
 *
26
 * @author Yannick Warnier <[email protected]>
27
 */
28
29
/**
30
 * Security class.
31
 *
32
 * Include/require it in your code and call Security::function()
33
 * to use its functionalities.
34
 *
35
 * This class can also be used as a container for filtered data, by creating
36
 * a new Security object and using $secure->filter($new_var,[more options])
37
 * and then using $secure->clean['var'] as a filtered equivalent, although
38
 * this is *not* mandatory at all.
39
 */
40
class Security
41
{
42
    public const CHAR_UPPER = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ';
43
    public const CHAR_LOWER = 'abcdefghijklmnopqrstuvwxyz';
44
    public const CHAR_DIGITS = '0123456789';
45
    public const CHAR_SYMBOLS = '!"#$%&\'()*+,-./:;<=>?@[\]^_`{|}~';
46
47
    public static Array $clean = [];
48
49
    /**
50
     * Checks if the absolute path (directory) given is really under the
51
     * checker path (directory).
52
     *
53
     * @param string  $abs_path  Absolute path to be checked (with trailing slash)
54
     * @param string  $checker_path  Checker path under which the path
55
     * should be (absolute path, with trailing slash, get it from api_get_path(SYS_COURSE_PATH))
56
     *
57
     * @return bool True if the path is under the checker, false otherwise
58
     */
59
    public static function check_abs_path(string $abs_path, string $checker_path): bool
60
    {
61
        // The checker path must be set.
62
        if (empty($checker_path)) {
63
            return false;
64
        }
65
66
        // Clean $abs_path.
67
        $true_path = self::cleanPath($abs_path);
68
        $checker_path = str_replace("\\", '/', realpath($checker_path));
69
70
        if (empty($checker_path)) {
71
            return false;
72
        }
73
74
        $found = strpos($true_path.'/', $checker_path);
75
76
        if ($found === 0) {
77
            return true;
78
        } else {
79
            // Code specific to Windows and case-insensitive behaviour
80
            if (api_is_windows_os()) {
81
                $found = stripos($true_path.'/', $checker_path);
82
                if ($found === 0) {
83
                    return true;
84
                }
85
            }
86
        }
87
88
        return false;
89
    }
90
91
    public static function cleanPath(string $absPath): string
92
    {
93
        $absPath = str_replace(['//', '../'], ['/', ''], $absPath);
94
95
        return str_replace("\\", '/', realpath($absPath));
96
    }
97
98
    /**
99
     * Checks if the relative path (directory) given is really under the
100
     * checker path (directory).
101
     *
102
     * @param string  $rel_path  Relative path to be checked (relative to the current directory) (with trailing slash)
103
     * @param string  $checker_path  Checker path under which the path
104
     * should be (absolute path, with trailing slash, get it from api_get_path(SYS_COURSE_PATH))
105
     *
106
     * @return bool True if the path is under the checker, false otherwise
107
     */
108
    public static function check_rel_path(string $rel_path, string $checker_path): bool
109
    {
110
        // The checker path must be set.
111
        if (empty($checker_path)) {
112
            return false;
113
        }
114
        $current_path = getcwd(); // No trailing slash.
115
        if (substr($rel_path, -1, 1) != '/') {
116
            $rel_path = '/'.$rel_path;
117
        }
118
        $abs_path = $current_path.$rel_path;
119
        $true_path = str_replace("\\", '/', realpath($abs_path));
120
        $found = strpos($true_path.'/', $checker_path);
121
        if ($found === 0) {
122
            return true;
123
        }
124
125
        return false;
126
    }
127
128
    /**
129
     * Filters dangerous filenames (*.php[.]?* and .htaccess) and returns it in
130
     * a non-executable form (for PHP and htaccess, this is still vulnerable to
131
     * other languages' files extensions).
132
     *
133
     * @param string $filename Unfiltered filename
134
     *
135
     * @return string
136
     */
137
    public static function filter_filename(string $filename): string
138
    {
139
        return disable_dangerous_file($filename);
140
    }
141
142
    /**
143
     * @return string
144
     */
145
    public static function getTokenFromSession(string $prefix = ''): string
146
    {
147
        $secTokenVariable = self::generateSecTokenVariable($prefix);
148
149
        return Session::read($secTokenVariable);
0 ignored issues
show
Bug Best Practice introduced by
The expression return ChamiloSession::read($secTokenVariable) could return the type null which is incompatible with the type-hinted return string. Consider adding an additional type-check to rule them out.
Loading history...
150
    }
151
152
    /**
153
     * This function checks that the token generated in get_token() has been kept (prevents
154
     * Cross-Site Request Forgeries attacks).
155
     *
156
     * @param string $requestType   The array in which to get the token ('get' or 'post')
157
     * @param ?FormValidator $form
158
     * @param string $prefix
159
     *
160
     * @return bool True if it's the right token, false otherwise
161
     */
162
    public static function check_token(string $requestType = 'post', FormValidator $form = null, string $prefix = ''): bool
163
    {
164
        $secTokenVariable = self::generateSecTokenVariable($prefix);
165
        $sessionToken = Session::read($secTokenVariable);
166
        switch ($requestType) {
167
            case 'request':
168
                if (!empty($sessionToken) && isset($_REQUEST[$secTokenVariable]) && $sessionToken === $_REQUEST[$secTokenVariable]) {
169
                    return true;
170
                }
171
172
                return false;
173
            case 'get':
174
                if (!empty($sessionToken) && isset($_GET[$secTokenVariable]) && $sessionToken === $_GET[$secTokenVariable]) {
175
                    return true;
176
                }
177
178
                return false;
179
            case 'post':
180
                if (!empty($sessionToken) && isset($_POST[$secTokenVariable]) && $sessionToken === $_POST[$secTokenVariable]) {
181
                    return true;
182
                }
183
184
                return false;
185
            case 'form':
186
                $token = $form->getSubmitValue('protect_token');
0 ignored issues
show
Bug introduced by
The method getSubmitValue() does not exist on null. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

186
                /** @scrutinizer ignore-call */ 
187
                $token = $form->getSubmitValue('protect_token');

This check looks for calls to methods that do not seem to exist on a given type. It looks for the method on the type itself as well as in inherited classes or implemented interfaces.

This is most likely a typographical error or the method has been renamed.

Loading history...
187
188
                if (!empty($sessionToken) && !empty($token) && $sessionToken === $token) {
189
                    return true;
190
                }
191
192
                return false;
193
            default:
194
                if (!empty($sessionToken) && isset($requestType) && $sessionToken === $requestType) {
195
                    return true;
196
                }
197
        }
198
199
        return false; // Just in case, don't let anything slip.
200
    }
201
202
    /**
203
     * Checks the user agent of the client as recorder by get_ua() to prevent
204
     * most session hijacking attacks.
205
     *
206
     * @return bool True if the user agent is the same, false otherwise
207
     */
208
    public static function check_ua(): bool
209
    {
210
        $security = Session::read('sec_ua');
211
        $securitySeed = Session::read('sec_ua_seed');
212
213
        if ($security === $_SERVER['HTTP_USER_AGENT'].$securitySeed) {
214
            return true;
215
        }
216
217
        return false;
218
    }
219
220
    /**
221
     * Clear the security token from the session.
222
     */
223
    public static function clear_token(string $prefix = '')
224
    {
225
        $secTokenVariable = self::generateSecTokenVariable($prefix);
226
227
        Session::erase($secTokenVariable);
228
    }
229
230
    /**
231
     * This function sets a random token to be included in a form as a hidden field
232
     * and saves it into the user's session. Returns an HTML form element
233
     * This later prevents Cross-Site Request Forgeries by checking that the user is really
234
     * the one that sent this form in knowingly (this form hasn't been generated from
235
     * another website visited by the user at the same time).
236
     * Check the token with check_token().
237
     *
238
     * @return string Hidden-type input ready to insert into a form
239
     */
240
    public static function get_HTML_token(string $prefix = ''): string
241
    {
242
        $secTokenVariable = self::generateSecTokenVariable($prefix);
243
        $token = md5(uniqid(rand(), true));
244
        $string = '<input type="hidden" name="'.$secTokenVariable.'" value="'.$token.'" />';
245
        Session::write($secTokenVariable, $token);
246
247
        return $string;
248
    }
249
250
    /**
251
     * This function sets a random token to be included in a form as a hidden field
252
     * and saves it into the user's session.
253
     * This later prevents Cross-Site Request Forgeries by checking that the user is really
254
     * the one that sent this form in knowingly (this form hasn't been generated from
255
     * another website visited by the user at the same time).
256
     * Check the token with check_token().
257
     *
258
     * @return string Token
259
     */
260
    public static function get_token($prefix = ''): string
261
    {
262
        $secTokenVariable = self::generateSecTokenVariable($prefix);
263
        $token = md5(uniqid(rand(), true));
264
        Session::write($secTokenVariable, $token);
265
266
        return $token;
267
    }
268
269
    /**
270
     * @return string
271
     */
272
    public static function get_existing_token(string $prefix = ''): string
273
    {
274
        $secTokenVariable = self::generateSecTokenVariable($prefix);
275
        $token = Session::read($secTokenVariable);
276
        if (!empty($token)) {
277
            return $token;
278
        } else {
279
            return self::get_token($prefix);
280
        }
281
    }
282
283
    /**
284
     * Gets the user agent in the session to later check it with check_ua() to prevent
285
     * most cases of session hijacking.
286
     */
287
    public static function get_ua()
288
    {
289
        $seed = uniqid(rand(), true);
290
        Session::write('sec_ua_seed', $seed);
291
        Session::write('sec_ua', $_SERVER['HTTP_USER_AGENT'].$seed);
292
    }
293
294
    /**
295
     * This function returns a variable from the clean array. If the variable doesn't exist,
296
     * it returns null.
297
     *
298
     * @param string  $varname  Variable name
299
     *
300
     * @return mixed Variable or NULL on error
301
     */
302
    public static function get(string $varname)
303
    {
304
        if (isset(self::$clean[$varname])) {
305
            return self::$clean[$varname];
306
        }
307
308
        return null;
309
    }
310
311
    /**
312
     * This function tackles the XSS injections.
313
     * Filtering for XSS is very easily done by using the htmlentities() function.
314
     * This kind of filtering prevents JavaScript snippets to be understood as such.
315
     *
316
     * @param string|array $var The variable to filter for XSS, this params can be a string or an array (example : array(x,y))
317
     * @param ?int $user_status The user status,constant allowed (STUDENT, COURSEMANAGER, ANONYMOUS, COURSEMANAGERLOWSECURITY)
318
     * @param bool $filter_terms
319
     *
320
     * @return mixed Filtered string or array
321
     */
322
    public static function remove_XSS($var, int $user_status = null, bool $filter_terms = false)
323
    {
324
        if ($filter_terms) {
325
            $var = self::filter_terms($var);
326
        }
327
328
        if (empty($user_status)) {
329
            if (api_is_anonymous()) {
330
                $user_status = ANONYMOUS;
331
            } else {
332
                if (api_is_allowed_to_edit()) {
333
                    $user_status = COURSEMANAGER;
334
                } else {
335
                    $user_status = STUDENT;
336
                }
337
            }
338
        }
339
340
        if ($user_status == COURSEMANAGERLOWSECURITY) {
341
            return $var; // No filtering.
342
        }
343
344
        static $purifier = [];
345
        if (!isset($purifier[$user_status])) {
346
            $cache_dir = api_get_path(SYS_ARCHIVE_PATH).'Serializer';
347
            if (!file_exists($cache_dir)) {
348
                $mode = api_get_permissions_for_new_directories();
349
                mkdir($cache_dir, $mode);
350
            }
351
            $config = HTMLPurifier_Config::createDefault();
352
            $config->set('Cache.SerializerPath', $cache_dir);
353
            $config->set('Core.Encoding', api_get_system_encoding());
354
            $config->set('HTML.Doctype', 'XHTML 1.0 Transitional');
355
            $config->set('HTML.MaxImgLength', '2560');
356
            $config->set('HTML.TidyLevel', 'light');
357
            $config->set('Core.ConvertDocumentToFragment', false);
358
            $config->set('Core.RemoveProcessingInstructions', true);
359
360
            if (api_get_setting('enable_iframe_inclusion') == 'true') {
361
                $config->set('Filter.Custom', [new AllowIframes()]);
362
            }
363
364
            // Shows _target attribute in anchors
365
            $config->set('Attr.AllowedFrameTargets', ['_blank', '_top', '_self', '_parent']);
366
367
            if ($user_status == STUDENT) {
368
                global $allowed_html_student;
369
                $config->set('HTML.SafeEmbed', true);
370
                $config->set('HTML.SafeObject', true);
371
                $config->set('Filter.YouTube', true);
372
                $config->set('HTML.FlashAllowFullScreen', true);
373
                $config->set('HTML.Allowed', $allowed_html_student);
374
            } elseif ($user_status == COURSEMANAGER) {
375
                global $allowed_html_teacher;
376
                $config->set('HTML.SafeEmbed', true);
377
                $config->set('HTML.SafeObject', true);
378
                $config->set('Filter.YouTube', true);
379
                $config->set('HTML.FlashAllowFullScreen', true);
380
                $config->set('HTML.Allowed', $allowed_html_teacher);
381
            } else {
382
                global $allowed_html_anonymous;
383
                $config->set('HTML.Allowed', $allowed_html_anonymous);
384
            }
385
386
            // We need it for example for the flv player (ids of surrounding div-tags have to be preserved).
387
            $config->set('Attr.EnableID', true);
388
            $config->set('CSS.AllowImportant', true);
389
            // We need for the flv player the css definition display: none;
390
            $config->set('CSS.AllowTricky', true);
391
            $config->set('CSS.Proprietary', true);
392
393
            // Allow uri scheme.
394
            $config->set('URI.AllowedSchemes', [
395
                'http' => true,
396
                'https' => true,
397
                'mailto' => true,
398
                'ftp' => true,
399
                'nntp' => true,
400
                'news' => true,
401
                'data' => true,
402
            ]);
403
404
            // Allow <video> tag
405
            //$config->set('HTML.Doctype', 'HTML 4.01 Transitional');
406
            $config->set('HTML.SafeIframe', true);
407
408
            // Set some HTML5 properties
409
            $config->set('HTML.DefinitionID', 'html5-definitions'); // unqiue id
410
            $config->set('HTML.DefinitionRev', 1);
411
            if ($def = $config->maybeGetRawHTMLDefinition()) {
412
                // https://html.spec.whatwg.org/dev/media.html#the-video-element
413
                $def->addElement(
414
                    'video',
415
                    'Block',
416
                    'Optional: (source, Flow) | (Flow, source) | Flow',
417
                    'Common',
418
                    [
419
                        'src' => 'URI',
420
                        'type' => 'Text',
421
                        'width' => 'Length',
422
                        'height' => 'Length',
423
                        'poster' => 'URI',
424
                        'preload' => 'Enum#auto,metadata,none',
425
                        'controls' => 'Bool',
426
                    ]
427
                );
428
                // https://html.spec.whatwg.org/dev/media.html#the-audio-element
429
                $def->addElement(
430
                    'audio',
431
                    'Block',
432
                    'Optional: (source, Flow) | (Flow, source) | Flow',
433
                    'Common',
434
                    [
435
                        'autoplay' => 'Bool',
436
                        'src' => 'URI',
437
                        'loop' => 'Bool',
438
                        'preload' => 'Enum#auto,metadata,none',
439
                        'controls' => 'Bool',
440
                        'muted' => 'Bool',
441
                    ]
442
                );
443
                $def->addElement(
444
                    'source',
445
                    'Block',
446
                    'Flow',
447
                    'Common',
448
                    ['src' => 'URI', 'type' => 'Text']
449
                );
450
            }
451
452
            $purifier[$user_status] = new HTMLPurifier($config);
453
        }
454
455
        if (is_array($var)) {
456
            return $purifier[$user_status]->purifyArray($var);
457
        } else {
458
            return $purifier[$user_status]->purify($var);
459
        }
460
    }
461
462
    /**
463
     * Filter content.
464
     *
465
     * @param string $text to be filtered
466
     *
467
     * @return string
468
     */
469
    public static function filter_terms(string $text): string
470
    {
471
        static $bad_terms = [];
472
473
        if (empty($bad_terms)) {
474
            $list = api_get_setting('filter_terms');
475
            if (!empty($list)) {
476
                $list = explode("\n", $list);
477
                $list = array_filter($list);
478
                if (!empty($list)) {
479
                    foreach ($list as $term) {
480
                        $term = str_replace(["\r\n", "\r", "\n", "\t"], '', $term);
481
                        $html_entities_value = api_htmlentities($term, ENT_QUOTES, api_get_system_encoding());
482
                        $bad_terms[] = $term;
483
                        if ($term != $html_entities_value) {
484
                            $bad_terms[] = $html_entities_value;
485
                        }
486
                    }
487
                }
488
                $bad_terms = array_filter($bad_terms);
489
            }
490
        }
491
492
        $replace = '***';
493
        if (!empty($bad_terms)) {
494
            // Fast way
495
            $new_text = str_ireplace($bad_terms, $replace, $text);
496
            $text = $new_text;
497
        }
498
499
        return $text;
0 ignored issues
show
Bug Best Practice introduced by
The expression return $text could return the type array which is incompatible with the type-hinted return string. Consider adding an additional type-check to rule them out.
Loading history...
500
    }
501
502
    /**
503
     * This method provides specific protection (against XSS and other kinds of attacks)
504
     * for static images (icons) used by the system.
505
     * Image paths are supposed to be given by programmers - people who know what they do, anyway,
506
     * this method encourages a safe practice for generating icon paths, without using heavy solutions
507
     * based on HTMLPurifier for example.
508
     *
509
     * @param string $image_path the input path of the image, it could be relative or absolute URL
510
     *
511
     * @return string returns sanitized image path or an empty string when the image path is not secure
512
     *
513
     * @author Ivan Tcholakov, March 2011
514
     */
515
    public static function filter_img_path(string $image_path): string
516
    {
517
        static $allowed_extensions = ['png', 'gif', 'jpg', 'jpeg', 'svg', 'webp'];
518
        $image_path = htmlspecialchars(trim($image_path)); // No html code is allowed.
519
        // We allow static images only, query strings are forbidden.
520
        if (strpos($image_path, '?') !== false) {
521
            return '';
522
        }
523
        if (($pos = strpos($image_path, ':')) !== false) {
524
            // Protocol has been specified, let's check it.
525
            if (stripos($image_path, 'javascript:') !== false) {
526
                // Javascript everywhere in the path is not allowed.
527
                return '';
528
            }
529
            // We allow only http: and https: protocols for now.
530
            //if (!preg_match('/^https?:\/\//i', $image_path)) {
531
            //    return '';
532
            //}
533
            if (stripos($image_path, 'http://') !== 0 && stripos($image_path, 'https://') !== 0) {
534
                return '';
535
            }
536
        }
537
        // We allow file extensions for images only.
538
        //if (!preg_match('/.+\.(png|gif|jpg|jpeg)$/i', $image_path)) {
539
        //    return '';
540
        //}
541
        if (($pos = strrpos($image_path, '.')) !== false) {
542
            if (!in_array(strtolower(substr($image_path, $pos + 1)), $allowed_extensions)) {
543
                return '';
544
            }
545
        } else {
546
            return '';
547
        }
548
549
        return $image_path;
550
    }
551
552
    /**
553
     * Get password requirements
554
     * It checks config value 'password_requirements' or uses the "classic"
555
     * Chamilo password requirements.
556
     *
557
     * @return array
558
     */
559
    public static function getPasswordRequirements(): array
560
    {
561
        // Default
562
        $requirements = [
563
            'min' => [
564
                'lowercase' => 0,
565
                'uppercase' => 0,
566
                'numeric' => 2,
567
                'length' => 5,
568
                'specials' => 1,
569
            ],
570
        ];
571
572
        $passwordRequirements = api_get_configuration_value('password_requirements');
573
        if (!empty($passwordRequirements)) {
574
            $requirements = $passwordRequirements;
575
        }
576
577
        return ['min' => $requirements['min']];
578
    }
579
580
    /**
581
     * Gets password requirements in the platform language using get_lang
582
     * based in platform settings. See function 'self::getPasswordRequirements'.
583
     */
584
    public static function getPasswordRequirementsToString(array $evaluatedConditions = []): string
585
    {
586
        $output = '';
587
        $setting = self::getPasswordRequirements();
588
589
        $passedIcon = Display::returnFontAwesomeIcon(
590
            'check',
591
            '',
592
            true,
593
            'text-success',
594
            get_lang('PasswordRequirementPassed')
595
        );
596
        $pendingIcon = Display::returnFontAwesomeIcon(
597
            'times',
598
            '',
599
            true,
600
            'text-danger',
601
            get_lang('PasswordRequirementPending')
602
        );
603
604
        foreach ($setting as $type => $rules) {
605
            foreach ($rules as $rule => $parameter) {
606
                if (empty($parameter)) {
607
                    continue;
608
                }
609
610
                $evaluatedCondition = $type.'_'.$rule;
611
                $icon = $passedIcon;
612
613
                if (array_key_exists($evaluatedCondition, $evaluatedConditions)
614
                    && false === $evaluatedConditions[$evaluatedCondition]
615
                ) {
616
                    $icon = $pendingIcon;
617
                }
618
619
                $output .= empty($evaluatedConditions) ? '' : $icon;
620
                $output .= sprintf(
621
                    get_lang(
622
                        'NewPasswordRequirement'.ucfirst($type).'X'.ucfirst($rule)
623
                    ),
624
                    $parameter
625
                );
626
                $output .= '<br />';
627
            }
628
        }
629
630
        return $output;
631
    }
632
633
    /**
634
     * Sanitize a string, so it can be used in the exec() command without
635
     * "jail-breaking" to execute other commands.
636
     *
637
     * @param string $param The string to filter
638
     */
639
    public static function sanitizeExecParam(string $param): string
640
    {
641
        $param = preg_replace('/[`;&|]/', '', $param);
642
643
        return escapeshellarg($param);
644
    }
645
646
    private static function generateSecTokenVariable(string $prefix = ''): string
647
    {
648
        if (empty($prefix)) {
649
            return 'sec_token';
650
        }
651
652
        return $prefix.'_sec_token';
653
    }
654
}
655