Completed
Push — master ( 966b12...7d9ab3 )
by Julito
08:22
created

Security::get_HTML_token()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 7
Code Lines 4

Duplication

Lines 0
Ratio 0 %

Importance

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

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