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

Security::get()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 7
Code Lines 3

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 2
eloc 3
nc 2
nop 1
dl 0
loc 7
rs 10
c 0
b 0
f 0
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
    /**
142
     * This function checks that the token generated in get_token() has been kept (prevents
143
     * Cross-Site Request Forgeries attacks).
144
     *
145
     * @param    string    The array in which to get the token ('get' or 'post')
146
     *
147
     * @return bool True if it's the right token, false otherwise
148
     */
149
    public static function check_token($request_type = 'post', FormValidator $form = null, string $prefix = '')
150
    {
151
        $secTokenVariable = self::generateSecTokenVariable($prefix);
152
        $sessionToken = Session::read($secTokenVariable);
153
        switch ($request_type) {
154
            case 'request':
155
                if (!empty($sessionToken) && isset($_REQUEST[$secTokenVariable]) && $sessionToken === $_REQUEST[$secTokenVariable]) {
156
                    return true;
157
                }
158
159
                return false;
160
            case 'get':
161
                if (!empty($sessionToken) && isset($_GET[$secTokenVariable]) && $sessionToken === $_GET[$secTokenVariable]) {
162
                    return true;
163
                }
164
165
                return false;
166
            case 'post':
167
                if (!empty($sessionToken) && isset($_POST[$secTokenVariable]) && $sessionToken === $_POST[$secTokenVariable]) {
168
                    return true;
169
                }
170
171
                return false;
172
            case 'form':
173
                $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

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