Passed
Push — ofaj ( d9b422...0f9380 )
by
unknown
11:25 queued 11s
created

Security::check_token()   D

Complexity

Conditions 20
Paths 10

Size

Total Lines 39
Code Lines 24

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 20
eloc 24
nc 10
nop 2
dl 0
loc 39
rs 4.1666
c 0
b 0
f 0

How to fix   Complexity   

Long Method

Small methods make your code easier to understand, in particular if combined with a good name. Besides, if your method is small, finding a good name is usually much easier.

For example, if you find yourself adding comments to a method's body, this is usually a good sign to extract the commented part to a new method, and use the comment as a starting point when coming up with a good name for this new method.

Commonly applied refactorings include:

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

169
                /** @scrutinizer ignore-call */ 
170
                $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...
170
171
                if (!empty($sessionToken) && !empty($token) && $sessionToken === $token) {
172
                    return true;
173
                }
174
175
                return false;
176
            default:
177
                if (!empty($sessionToken) && isset($request_type) && $sessionToken === $request_type) {
178
                    return true;
179
                }
180
181
                return false;
182
        }
183
184
        return false; // Just in case, don't let anything slip.
0 ignored issues
show
Unused Code introduced by
return false is not reachable.

This check looks for unreachable code. It uses sophisticated control flow analysis techniques to find statements which will never be executed.

Unreachable code is most often the result of return, die or exit statements that have been added for debug purposes.

function fx() {
    try {
        doSomething();
        return true;
    }
    catch (\Exception $e) {
        return false;
    }

    return false;
}

In the above example, the last return false will never be executed, because a return statement has already been met in every possible execution path.

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