Passed
Push — 1.11.x ( dbab0c...7ecc0c )
by Yannick
08:32
created

Security::sanitizeExecParam()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 3
Code Lines 1

Duplication

Lines 0
Ratio 0 %

Importance

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

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