Passed
Pull Request — 1.11.x (#3931)
by Angel Fernando Quiroz
13:37
created

Security::generateSecTokenVariable()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 7
Code Lines 3

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 0 Features 0
Metric Value
cc 2
eloc 3
c 1
b 0
f 0
nc 2
nop 1
dl 0
loc 7
rs 10
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, where as 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_tocken()
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 static $clean = [];
43
44
    /**
45
     * Checks if the absolute path (directory) given is really under the
46
     * checker path (directory).
47
     *
48
     * @param string    Absolute path to be checked (with trailing slash)
49
     * @param string    Checker path under which the path
50
     * should be (absolute path, with trailing slash, get it from api_get_path(SYS_COURSE_PATH))
51
     *
52
     * @return bool True if the path is under the checker, false otherwise
53
     */
54
    public static function check_abs_path($abs_path, $checker_path)
55
    {
56
        // The checker path must be set.
57
        if (empty($checker_path)) {
58
            return false;
59
        }
60
61
        // Clean $abs_path.
62
        $abs_path = str_replace(['//', '../'], ['/', ''], $abs_path);
63
        $true_path = str_replace("\\", '/', realpath($abs_path));
64
        $checker_path = str_replace("\\", '/', realpath($checker_path));
65
66
        if (empty($checker_path)) {
67
            return false;
68
        }
69
70
        $found = strpos($true_path.'/', $checker_path);
71
72
        if ($found === 0) {
73
            return true;
74
        } else {
75
            // Code specific to Windows and case-insensitive behaviour
76
            if (api_is_windows_os()) {
77
                $found = stripos($true_path.'/', $checker_path);
78
                if ($found === 0) {
79
                    return true;
80
                }
81
            }
82
        }
83
84
        return false;
85
    }
86
87
    /**
88
     * Checks if the relative path (directory) given is really under the
89
     * checker path (directory).
90
     *
91
     * @param string    Relative path to be checked (relative to the current directory) (with trailing slash)
92
     * @param string    Checker path under which the path
93
     * should be (absolute path, with trailing slash, get it from api_get_path(SYS_COURSE_PATH))
94
     *
95
     * @return bool True if the path is under the checker, false otherwise
96
     */
97
    public static function check_rel_path($rel_path, $checker_path)
98
    {
99
        // The checker path must be set.
100
        if (empty($checker_path)) {
101
            return false;
102
        }
103
        $current_path = getcwd(); // No trailing slash.
104
        if (substr($rel_path, -1, 1) != '/') {
105
            $rel_path = '/'.$rel_path;
106
        }
107
        $abs_path = $current_path.$rel_path;
108
        $true_path = str_replace("\\", '/', realpath($abs_path));
109
        $found = strpos($true_path.'/', $checker_path);
110
        if ($found === 0) {
111
            return true;
112
        }
113
114
        return false;
115
    }
116
117
    /**
118
     * Filters dangerous filenames (*.php[.]?* and .htaccess) and returns it in
119
     * a non-executable form (for PHP and htaccess, this is still vulnerable to
120
     * other languages' files extensions).
121
     *
122
     * @param string $filename Unfiltered filename
123
     *
124
     * @return string
125
     */
126
    public static function filter_filename($filename)
127
    {
128
        return disable_dangerous_file($filename);
129
    }
130
131
    /**
132
     * @return string
133
     */
134
    public static function getTokenFromSession(string $prefix = '')
135
    {
136
        $secTokenVariable = self::generateSecTokenVariable($prefix);
137
138
        return Session::read($secTokenVariable);
139
    }
140
141
    private static function generateSecTokenVariable(string $prefix = ''): string
142
    {
143
        if (empty($prefix)) {
144
            return 'sec_token';
145
        }
146
147
        return $prefix.'_sec_token';
148
    }
149
150
    /**
151
     * This function checks that the token generated in get_token() has been kept (prevents
152
     * Cross-Site Request Forgeries attacks).
153
     *
154
     * @param    string    The array in which to get the token ('get' or 'post')
155
     *
156
     * @return bool True if it's the right token, false otherwise
157
     */
158
    public static function check_token($request_type = 'post', FormValidator $form = null, string $prefix = '')
159
    {
160
        $secTokenVariable = self::generateSecTokenVariable($prefix);
161
        $sessionToken = Session::read($secTokenVariable);
162
        switch ($request_type) {
163
            case 'request':
164
                if (!empty($sessionToken) && isset($_REQUEST[$secTokenVariable]) && $sessionToken === $_REQUEST[$secTokenVariable]) {
165
                    return true;
166
                }
167
168
                return false;
169
            case 'get':
170
                if (!empty($sessionToken) && isset($_GET[$secTokenVariable]) && $sessionToken === $_GET[$secTokenVariable]) {
171
                    return true;
172
                }
173
174
                return false;
175
            case 'post':
176
                if (!empty($sessionToken) && isset($_POST[$secTokenVariable]) && $sessionToken === $_POST[$secTokenVariable]) {
177
                    return true;
178
                }
179
180
                return false;
181
            case 'form':
182
                $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

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