Completed
Push — development ( 6a24df...5afdf5 )
by Nils
07:52
created

csrfProtector::isValidToken()   C

Complexity

Conditions 7
Paths 7

Size

Total Lines 23
Code Lines 13

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 7
eloc 13
nc 7
nop 1
dl 0
loc 23
rs 6.7272
c 0
b 0
f 0
1
<?php
2
3
if (!defined('__CSRF_PROTECTOR__')) {
4
    define('__CSRF_PROTECTOR__', true); // to avoid multiple declaration errors
5
6
    // name of HTTP POST variable for authentication
7
    define("CSRFP_TOKEN", "csrfp_token");
8
9
    // We insert token name and list of url patterns for which
10
    // GET requests are validated against CSRF as hidden input fields
11
    // these are the names of the input fields
12
    define("CSRFP_FIELD_TOKEN_NAME", "csrfp_hidden_data_token");
13
    define("CSRFP_FIELD_URLS", "csrfp_hidden_data_urls");
14
15
    /**
16
     * child exception classes
17
     */
18
    class configFileNotFoundException extends \exception {};
19
    class logDirectoryNotFoundException extends \exception {};
20
    class jsFileNotFoundException extends \exception {};
21
    class logFileWriteError extends \exception {};
22
    class baseJSFileNotFoundExceptio extends \exception {};
23
    class incompleteConfigurationException extends \exception {};
24
25
    class csrfProtector
26
    {
27
        /*
28
         * Variable: $cookieExpiryTime
29
         * expiry time for cookie
30
         * @var int
31
         */
32
        public static $cookieExpiryTime = 1800; //30 minutes
33
34
        /*
35
         * Variable: $isSameOrigin
36
         * flag for cross origin/same origin request
37
         * @var bool
38
         */
39
        private static $isSameOrigin = true;
0 ignored issues
show
Unused Code introduced by
The property $isSameOrigin is not used and could be removed.

This check marks private properties in classes that are never used. Those properties can be removed.

Loading history...
40
41
        /*
42
         * Variable: $isValidHTML
43
         * flag to check if output file is a valid HTML or not
44
         * @var bool
45
         */
46
        private static $isValidHTML = false;
47
48
        /*
49
         * Variable: $requestType
50
         * Varaible to store weather request type is post or get
51
         * @var string
52
         */
53
        protected static $requestType = "GET";
54
55
        /*
56
         * Variable: $config
57
         * config file for CSRFProtector
58
         * @var int Array, length = 6
59
         * Property: #1: failedAuthAction (int) => action to be taken in case autherisation fails
60
         * Property: #2: logDirectory (string) => directory in which log will be saved
61
         * Property: #3: customErrorMessage (string) => custom error message to be sent in case
62
         *                      of failed authentication
63
         * Property: #4: jsFile (string) => location of the CSRFProtector js file
64
         * Property: #5: tokenLength (int) => default length of hash
65
         * Property: #6: disabledJavascriptMessage (string) => error message if client's js is disabled
66
         */
67
        public static $config = array();
68
69
        /*
70
         * Variable: $requiredConfigurations
71
         * Contains list of those parameters that are required to be there
72
         *  in config file for csrfp to work
73
         */
74
        public static $requiredConfigurations = array('logDirectory', 'failedAuthAction', 'jsPath', 'jsUrl', 'tokenLength');
75
76
        /*
77
         *  Function: init
78
         *
79
         *  function to initialise the csrfProtector work flow
80
         *
81
         *  Parameters:
82
         *  $length - length of CSRF_AUTH_TOKEN to be generated
83
         *  $action - int array, for different actions to be taken in case of failed validation
84
         *
85
         *  Returns:
86
         *      void
87
         *
88
         *  Throws:
89
         *      configFileNotFoundException - when configuration file is not found
90
         *      incompleteConfigurationException - when all required fields in config
91
         *                                          file are not available
92
         *
93
         */
94
        public static function init($length = null, $action = null)
95
        {
96
            /*
97
             * if mod_csrfp already enabled, no verification, no filtering
98
             * Already done by mod_csrfp
99
             */
100
            if (getenv('mod_csrfp_enabled')) {
101
                            return;
102
            }
103
104
            //start session in case its not
105
            if (session_id() === '') {
106
                require_once __DIR__."/../../../../../sources/SecureHandler.php";
107
                session_start();
108
            }
109
110
            /*
111
             * load configuration file and properties
112
             * Check locally for a config.php then check for
113
             * a config/csrf_config.php file in the root folder
114
             * for composer installations
115
             */
116
            $standard_config_location = __DIR__."/../csrfp.config.php";
117
            $composer_config_location = __DIR__."/../../../../../config/csrf_config.php";
118
119
            if (file_exists($standard_config_location)) {
120
                self::$config = include($standard_config_location);
121
            } elseif (file_exists($composer_config_location)) {
122
                self::$config = include($composer_config_location);
123
            } else {
124
                throw new configFileNotFoundException("OWASP CSRFProtector: configuration file not found for CSRFProtector!");
125
            }
126
127
            //overriding length property if passed in parameters
128
            if ($length != null) {
129
                            self::$config['tokenLength'] = intval($length);
130
            }
131
132
            //action that is needed to be taken in case of failed authorisation
133
            if ($action != null) {
134
                            self::$config['failedAuthAction'] = $action;
135
            }
136
137
            if (self::$config['CSRFP_TOKEN'] == '') {
138
                            self::$config['CSRFP_TOKEN'] = CSRFP_TOKEN;
139
            }
140
141
            // Validate the config if everythings filled out
142
            foreach (self::$requiredConfigurations as $value) {
143
                if (!isset(self::$config[$value]) || self::$config[$value] == '') {
144
                    throw new incompleteConfigurationException("OWASP CSRFProtector: Incomplete configuration file!");
145
                    exit;
0 ignored issues
show
Unused Code introduced by
die; does not seem to be 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...
146
                }
147
            }
148
149
            // Authorise the incoming request
150
            self::authorizePost();
151
152
            // Initialize output buffering handler
153
            ob_start('csrfProtector::ob_handler');
154
155
            if (!isset($_COOKIE[self::$config['CSRFP_TOKEN']])
156
                || !isset($_SESSION[self::$config['CSRFP_TOKEN']])
157
                || !is_array($_SESSION[self::$config['CSRFP_TOKEN']])
158
                || !in_array($_COOKIE[self::$config['CSRFP_TOKEN']],
159
                    $_SESSION[self::$config['CSRFP_TOKEN']])) {
160
                            self::refreshToken();
161
            }
162
163
            // Set protected by CSRF Protector header
164
            header('X-CSRF-Protection: OWASP CSRFP 1.0.0');
165
        }
166
167
        /*
168
         * Function: authorizePost
169
         * function to authorise incoming post requests
170
         *
171
         * Parameters:
172
         * void
173
         *
174
         * Returns:
175
         * void
176
         *
177
         * Throws:
178
         * logDirectoryNotFoundException - if log directory is not found
179
         */
180
        public static function authorizePost()
181
        {
182
            //#todo this method is valid for same origin request only,
183
            //enable it for cross origin also sometime
184
            //for cross origin the functionality is different
185
            if (!static::isURLallowed()) {
186
187
                //currently for same origin only
188 View Code Duplication
                if (!(isset($_GET[self::$config['CSRFP_TOKEN']])
189
                    && isset($_SESSION[self::$config['CSRFP_TOKEN']])
190
                    && (self::isValidToken($_GET[self::$config['CSRFP_TOKEN']]))
191
                    )) {
192
193
                    //action in case of failed validation
194
                    self::failedValidationAction();
195
                } else {
196
                    self::refreshToken(); //refresh token for successfull validation
197
                }
198
            } else if ($_SERVER['REQUEST_METHOD'] === 'POST') {
199
200
                //set request type to POST
201
                self::$requestType = "POST";
202
203
                //currently for same origin only
204 View Code Duplication
                if (!(isset($_POST[self::$config['CSRFP_TOKEN']])
205
                    && isset($_SESSION[self::$config['CSRFP_TOKEN']])
206
                    && (self::isValidToken($_POST[self::$config['CSRFP_TOKEN']]))
207
                    )) {
208
209
                    //action in case of failed validation
210
                    self::failedValidationAction();
211
                } else {
212
                    self::refreshToken(); //refresh token for successfull validation
213
                }
214
            }
215
        }
216
217
        /*
218
         * Function: isValidToken
219
         * function to check the validity of token in session array
220
         * Function also clears all tokens older than latest one
221
         *
222
         * Parameters:
223
         * $token - the token sent with GET or POST payload
224
         *
225
         * Returns:
226
         * bool - true if its valid else false
227
         */
228
        private static function isValidToken($token) {
229
            if (!isset($_SESSION[self::$config['CSRFP_TOKEN']])) {
230
                return false;
231
            }
232
            if (!is_array($_SESSION[self::$config['CSRFP_TOKEN']])) {
233
                return false;
234
            }
235
            foreach ($_SESSION[self::$config['CSRFP_TOKEN']] as $key => $value) {
236
                if ($value == $token) {
237
238
                    // Clear all older tokens assuming they have been consumed
239
                    foreach ($_SESSION[self::$config['CSRFP_TOKEN']] as $_key => $_value) {
240
                        if ($_value == $token) {
241
                            break;
242
                        }
243
                        array_shift($_SESSION[self::$config['CSRFP_TOKEN']]);
244
                    }
245
                    return true;
246
                }
247
            }
248
249
            return false;
250
        }
251
252
        /*
253
         * Function: failedValidationAction
254
         * function to be called in case of failed validation
255
         * performs logging and take appropriate action
256
         *
257
         * Parameters:
258
         * void
259
         *
260
         * Returns:
261
         * void
262
         */
263
        private static function failedValidationAction()
264
        {
265
            if (!file_exists(__DIR__."/../".self::$config['logDirectory']))
266
                throw new logDirectoryNotFoundException("OWASP CSRFProtector: Log Directory Not Found!");
267
268
            //call the logging function
269
            static::logCSRFattack();
0 ignored issues
show
Bug introduced by
Since logCSRFattack() is declared private, calling it with static will lead to errors in possible sub-classes. You can either use self, or increase the visibility of logCSRFattack() to at least protected.

Let’s assume you have a class which uses late-static binding:

class YourClass
{
    private static function getTemperature() {
        return "3422 °C";
}

public static function getSomeVariable()
{
    return static::getTemperature();
}

}

The code above will run fine in your PHP runtime. However, if you now create a sub-class and call the getSomeVariable() on that sub-class, you will receive a runtime error:

class YourSubClass extends YourClass {
      private static function getTemperature() {
        return "-182 °C";
    }
}

print YourSubClass::getSomeVariable(); // Will cause an access error.

In the case above, it makes sense to update SomeClass to use self instead:

class YourClass
{
    private static function getTemperature() {
        return "3422 °C";
    }

    public static function getSomeVariable()
    {
        return self::getTemperature();
    }
}
Loading history...
270
271
            //#todo: ask mentors if $failedAuthAction is better as an int or string
272
            //default case is case 0
0 ignored issues
show
Unused Code Comprehensibility introduced by
45% of this comment could be valid code. Did you maybe forget this after debugging?

Sometimes obsolete code just ends up commented out instead of removed. In this case it is better to remove the code once you have checked you do not need it.

The code might also have been commented out for debugging purposes. In this case it is vital that someone uncomments it again or your project may behave in very unexpected ways in production.

This check looks for comments that seem to be mostly valid code and reports them.

Loading history...
273
            switch (self::$config['failedAuthAction'][self::$requestType]) {
274
                case 0:
275
                    //send 403 header
276
                    header('HTTP/1.0 403 Forbidden');
277
                    exit("<h2>403 Access Forbidden by CSRFProtector!</h2>");
278
                    break;
0 ignored issues
show
Unused Code introduced by
break; does not seem to be 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...
279
                case 1:
280
                    //unset the query parameters and forward
281
                    if (self::$requestType === 'GET') {
282
                        $_GET = array();
283
                    } else {
284
                        $_POST = array();
285
                    }
286
                    break;
287
                case 2:
288
                    //redirect to custom error page
289
                    $location = self::$config['errorRedirectionPage'];
290
                    header("location: $location");
291
                case 3:
292
                    //send custom error message
293
                    exit(self::$config['customErrorMessage']);
294
                    break;
0 ignored issues
show
Unused Code introduced by
break; does not seem to be 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...
295
                case 4:
296
                    //send 500 header -- internal server error
297
                    header($_SERVER['SERVER_PROTOCOL'].' 500 Internal Server Error', true, 500);
298
                    exit("<h2>500 Internal Server Error!</h2>");
299
                    break;
0 ignored issues
show
Unused Code introduced by
break; does not seem to be 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...
300
                default:
301
                    //unset the query parameters and forward
302
                    if (self::$requestType === 'GET') {
303
                        $_GET = array();
304
                    } else {
305
                        $_POST = array();
306
                    }
307
                    break;
308
            }
309
        }
310
311
        /*
312
         * Function: refreshToken
313
         * Function to set auth cookie
314
         *
315
         * Parameters:
316
         * void
317
         *
318
         * Returns:
319
         * void
320
         */
321
        public static function refreshToken()
322
        {
323
            $token = self::generateAuthToken();
324
325
            if (!isset($_SESSION[self::$config['CSRFP_TOKEN']])
326
                || !is_array($_SESSION[self::$config['CSRFP_TOKEN']])) {
327
                            $_SESSION[self::$config['CSRFP_TOKEN']] = array();
328
            }
329
330
            //set token to session for server side validation
331
            array_push($_SESSION[self::$config['CSRFP_TOKEN']], $token);
332
333
            //set token to cookie for client side processing
334
            setcookie(self::$config['CSRFP_TOKEN'],
335
                $token,
336
                time() + self::$cookieExpiryTime);
337
        }
338
339
        /*
340
         * Function: generateAuthToken
341
         * function to generate random hash of length as given in parameter
342
         * max length = 128
343
         *
344
         * Parameters:
345
         * length to hash required, int
346
         *
347
         * Returns:
348
         * string, token
349
         */
350
        public static function generateAuthToken()
351
        {
352
            //if config tokenLength value is 0 or some non int
353
            if (intval(self::$config['tokenLength']) == 0) {
354
                self::$config['tokenLength'] = 32; //set as default
355
            }
356
357
            //#todo - if $length > 128 throw exception
358
359
            if (function_exists("hash_algos") && in_array("sha512", hash_algos())) {
360
                $token = hash("sha512", mt_rand(0, mt_getrandmax()));
361
            } else {
362
                $token = '';
363
                for ($i = 0; $i < 128; ++$i) {
364
                    $r = mt_rand(0, 35);
365
                    if ($r < 26) {
366
                        $c = chr(ord('a') + $r);
367
                    } else {
368
                        $c = chr(ord('0') + $r - 26);
369
                    }
370
                    $token .= $c;
371
                }
372
            }
373
            return substr($token, 0, self::$config['tokenLength']);
374
        }
375
376
        /*
377
         * Function: ob_handler
378
         * Rewrites <form> on the fly to add CSRF tokens to them. This can also
379
         * inject our JavaScript library.
380
         *
381
         * Parameters:
382
         * $buffer - output buffer to which all output are stored
383
         * $flag - INT
384
         *
385
         * Return:
386
         * string, complete output buffer
387
         */
388
        public static function ob_handler($buffer, $flags)
0 ignored issues
show
Unused Code introduced by
The parameter $flags is not used and could be removed.

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

Loading history...
389
        {
390
            // Even though the user told us to rewrite, we should do a quick heuristic
391
            // to check if the page is *actually* HTML. We don't begin rewriting until
392
            // we hit the first <html tag.
393
            if (!self::$isValidHTML) {
394
                // not HTML until proven otherwise
395
                if (stripos($buffer, '<html') !== false) {
396
                    self::$isValidHTML = true;
397
                } else {
398
                    return $buffer;
399
                }
400
            }
401
402
            // TODO: statically rewrite all forms as well so that if a form is submitted
403
            // before the js has worked on, it will still have token to send
404
            // @priority: medium @labels: important @assign: mebjas
405
            // @deadline: 1 week
406
407
            //add a <noscript> message to outgoing HTML output,
408
            //informing the user to enable js for CSRFProtector to work
409
            //best section to add, after <body> tag
410
            $buffer = preg_replace("/<body[^>]*>/", "$0 <noscript>".self::$config['disabledJavascriptMessage'].
411
                "</noscript>", $buffer);
412
            $hiddenInput = '<fieldset style="display: none"><legend>CSRF Protection</legend>'.PHP_EOL;
413
            $hiddenInput .= '<input type="hidden" id="'.CSRFP_FIELD_TOKEN_NAME.'" value="'
414
                            .self::$config['CSRFP_TOKEN'].'" />'.PHP_EOL;
415
416
            $hiddenInput .= '<input type="hidden" id="'.CSRFP_FIELD_URLS.'" value=\''
417
                            .json_encode(str_replace("&", "%26", self::$config['verifyGetFor'])).'\' />'.PHP_EOL;
418
            $hiddenInput .= '</fieldset>';
419
420
            //implant hidden fields with check url information for reading in javascript
421
            $buffer = str_ireplace('</body>', $hiddenInput.'</body>', $buffer);
422
423
            //implant the CSRFGuard js file to outgoing script
424
            $script = '<script type="text/javascript" src="'.self::$config['jsUrl'].'"></script>'.PHP_EOL;
425
            $buffer = str_ireplace('</body>', $script.'</body>', $buffer, $count);
426
427
            if (!$count) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $count of type integer|null is loosely compared to false; this is ambiguous if the integer can be zero. You might want to explicitly use === null instead.

In PHP, under loose comparison (like ==, or !=, or switch conditions), values of different types might be equal.

For integer values, zero is a special case, in particular the following results might be unexpected:

0   == false // true
0   == null  // true
123 == false // false
123 == null  // false

// It is often better to use strict comparison
0 === false // false
0 === null  // false
Loading history...
428
                            $buffer .= $script;
429
            }
430
431
            return $buffer;
432
        }
433
434
        /*
435
         * Function: logCSRFattack
436
         * Functio to log CSRF Attack
437
         *
438
         * Parameters:
439
         * void
440
         *
441
         * Retruns:
442
         * void
443
         *
444
         * Throws:
445
         * logFileWriteError - if unable to log an attack
446
         */
447
        private static function logCSRFattack()
448
        {
449
            //if file doesnot exist for, create it
450
            $logFile = fopen(__DIR__."/../".self::$config['logDirectory']
451
            ."/".date("m-20y").".log", "a+");
452
453
            //throw exception if above fopen fails
454
            if (!$logFile) {
455
                            throw new logFileWriteError("OWASP CSRFProtector: Unable to write to the log file");
456
            }
457
458
            //miniature version of the log
459
            $log = array();
460
            $log['timestamp'] = time();
461
            $log['HOST'] = $_SERVER['HTTP_HOST'];
462
            $log['REQUEST_URI'] = $_SERVER['REQUEST_URI'];
463
            $log['requestType'] = self::$requestType;
464
465
            if (self::$requestType === "GET") {
466
                            $log['query'] = $_GET;
467
            } else {
468
                            $log['query'] = $_POST;
469
            }
470
471
            $log['cookie'] = $_COOKIE;
472
473
            //convert log array to JSON format to be logged
474
            $log = json_encode($log).PHP_EOL;
475
476
            //append log to the file
477
            fwrite($logFile, $log);
478
479
            //close the file handler
480
            fclose($logFile);
481
        }
482
483
        /*
484
         * Function: getCurrentUrl
485
         * Function to return current url of executing page
486
         *
487
         * Parameters:
488
         * void
489
         *
490
         * Returns:
491
         * string - current url
492
         */
493
        private static function getCurrentUrl()
494
        {
495
            $request_scheme = 'https';
0 ignored issues
show
Unused Code introduced by
$request_scheme is not used, you could remove the assignment.

This check looks for variable assignements that are either overwritten by other assignments or where the variable is not used subsequently.

$myVar = 'Value';
$higher = false;

if (rand(1, 6) > 3) {
    $higher = true;
} else {
    $higher = false;
}

Both the $myVar assignment in line 1 and the $higher assignment in line 2 are dead. The first because $myVar is never used and the second because $higher is always overwritten for every possible time line.

Loading history...
496
497
            if (isset($_SERVER['REQUEST_SCHEME'])) {
498
                $request_scheme = $_SERVER['REQUEST_SCHEME'];
499
            } else {
500
                if (isset($_SERVER['HTTPS']) && $_SERVER['HTTPS'] === 'on') {
501
                    $request_scheme = 'https';
502
                } else {
503
                    $request_scheme = 'http';
504
                }
505
            }
506
507
            return $request_scheme.'://'.$_SERVER['HTTP_HOST'].$_SERVER['REQUEST_URI'];
508
        }
509
510
        /*
511
         * Function: isURLallowed
512
         * Function to check if a url mataches for any urls
513
         * Listed in config file
514
         *
515
         * Parameters:
516
         * void
517
         *
518
         * Returns:
519
         * boolean - true is url need no validation, false if validation needed
520
         */
521
        public static function isURLallowed() {
522
            foreach (self::$config['verifyGetFor'] as $key => $value) {
523
                $value = str_replace(array('/', '*'), array('\/', '(.*)'), $value);
524
                preg_match('/'.$value.'/', self::getCurrentUrl(), $output);
525
                if (count($output) > 0)
526
                    return false;
527
            }
528
            return true;
529
        }
530
    };
531
}
532