Passed
Push — 1.11.x ( 42d1a5...bf5054 )
by Julito
12:38
created

Security::get_ua()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 5
Code Lines 3

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 1
eloc 3
nc 1
nop 0
dl 0
loc 5
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
 * @package chamilo.library
27
 *
28
 * @author Yannick Warnier <[email protected]>
29
 */
30
31
/**
32
 * Security class.
33
 *
34
 * Include/require it in your code and call Security::function()
35
 * to use its functionalities.
36
 *
37
 * This class can also be used as a container for filtered data, by creating
38
 * a new Security object and using $secure->filter($new_var,[more options])
39
 * and then using $secure->clean['var'] as a filtered equivalent, although
40
 * this is *not* mandatory at all.
41
 */
42
class Security
43
{
44
    public static $clean = [];
45
46
    /**
47
     * Checks if the absolute path (directory) given is really under the
48
     * checker path (directory).
49
     *
50
     * @param string    Absolute path to be checked (with trailing slash)
51
     * @param string    Checker path under which the path
52
     * should be (absolute path, with trailing slash, get it from api_get_path(SYS_COURSE_PATH))
53
     *
54
     * @return bool True if the path is under the checker, false otherwise
55
     */
56
    public static function check_abs_path($abs_path, $checker_path)
57
    {
58
        // The checker path must be set.
59
        if (empty($checker_path)) {
60
            return false;
61
        }
62
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()
135
    {
136
        return Session::read('sec_token');
137
    }
138
139
    /**
140
     * This function checks that the token generated in get_token() has been kept (prevents
141
     * Cross-Site Request Forgeries attacks).
142
     *
143
     * @param    string    The array in which to get the token ('get' or 'post')
144
     *
145
     * @return bool True if it's the right token, false otherwise
146
     */
147
    public static function check_token($request_type = 'post', FormValidator $form = null)
148
    {
149
        $sessionToken = Session::read('sec_token');
150
        switch ($request_type) {
151
            case 'request':
152
                if (!empty($sessionToken) && isset($_REQUEST['sec_token']) && $sessionToken === $_REQUEST['sec_token']) {
153
                    return true;
154
                }
155
156
                return false;
157
            case 'get':
158
                if (!empty($sessionToken) && isset($_GET['sec_token']) && $sessionToken === $_GET['sec_token']) {
159
                    return true;
160
                }
161
162
                return false;
163
            case 'post':
164
                if (!empty($sessionToken) && isset($_POST['sec_token']) && $sessionToken === $_POST['sec_token']) {
165
                    return true;
166
                }
167
168
                return false;
169
            case 'form':
170
                $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

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

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

566
    public static function getPasswordRequirementsToString(/** @scrutinizer ignore-unused */ $passedConditions = [])

This check looks for parameters that have been defined for a function or method, but which are not used in the method body.

Loading history...
567
    {
568
        $output = '';
569
        $setting = self::getPasswordRequirements();
570
        foreach ($setting as $type => $rules) {
571
            foreach ($rules as $rule => $parameter) {
572
                if (empty($parameter)) {
573
                    continue;
574
                }
575
                $output .= sprintf(
576
                    get_lang(
577
                        'NewPasswordRequirement'.ucfirst($type).'X'.ucfirst($rule)
578
                    ),
579
                    $parameter
580
                );
581
                $output .= '<br />';
582
            }
583
        }
584
585
        return $output;
586
    }
587
}
588