CSRF.php

<?php

namespace Lia\Test;

use \Lia\Utility\CSRF;


/** For testing CSRF validation
 * 
 */
class CSRFTester extends \Tlf\Tester {

    public function testCSRF(){

        $_SERVER['REQUEST_URI'] = '/';
        $_SERVER['HTTP_HOST'] = 'http://example.com';
        $_SERVER['HTTP_REFERER'] = 'http://example.com';

        echo "\n\n#### CUSTOM/ONE-OFF TESTS ####";

        $this->test("One token per form id during the same request");
        $token1 = CSRF::get_csrf_value('one_token', '', 10);
        $token2 = CSRF::get_csrf_value('one_token', '', 10);
        $token3 = CSRF::get_csrf_value('one_token', '', 10);
        $this->is_true($token1 == $token2 && $token2 == $token3);


        $this->test("Gets token, submits form, and generates new form for that same token. Tokens should be different");
        $token1 = ResettableCSRF::get_csrf_value('sameid', '', 10);
        ResettableCSRF::reset();
        $valid1 = ResettableCSRF::is_request_valid('sameid', ['csrf-sameid'=>$token1]);
        $token2 = ResettableCSRF::get_csrf_value('sameid', '', 10);
        ResettableCSRF::reset();
        $valid2 = ResettableCSRF::is_request_valid('sameid', ['csrf-sameid'=>$token2]);

        $this->is_true($valid1 && $valid2);
        $this->is_true($token1 != $token2);

        $this->test("Cannot validate same token on different requests");
        $token1 = ResettableCSRF::get_csrf_value('diffreqs', '', 10);
        ResettableCSRF::reset();
        $valid1 = ResettableCSRF::is_request_valid('diffreqs', ['csrf-diffreqs'=>$token1]);
        ResettableCSRF::reset();
        $invalid1 = ResettableCSRF::is_request_valid('diffreqs', ['csrf-diffreqs'=>$token1]);

        $this->is_true($valid1);
        $this->is_false($invalid1);

        $this->test("Multiple forms, different IDs");
        $token1 = CSRF::get_csrf_value('id1', '', 10);
        $token2 = CSRF::get_csrf_value('id2', '', 10);
        $token3 = CSRF::get_csrf_value('id3', '', 10);
        $valid1 = CSRF::is_request_valid('id1', ['csrf-id1'=>$token1]);
        $valid2 = CSRF::is_request_valid('id2', ['csrf-id2'=>$token2]);
        $valid3 = CSRF::is_request_valid('id3', ['csrf-id3'=>$token3]);
        $this->is_true($valid1 && $valid2 && $valid3);

        $this->test("Multiple forms, same IDs, different requests for setup");
        ResettableCSRF::reset();
        $token1 = ResettableCSRF::get_csrf_value('id1', '', 10);
        ResettableCSRF::reset();
        $token2 = ResettableCSRF::get_csrf_value('id1', '', 10);
        ResettableCSRF::reset();
        $token3 = ResettableCSRF::get_csrf_value('id1', '', 10);
        ResettableCSRF::reset();
        $this->is_true($token1!=$token2 && $token2 != $token3);
        $valid1 = ResettableCSRF::is_request_valid('id1', ['csrf-id1'=>$token1]);
        $valid2 = ResettableCSRF::is_request_valid('id1', ['csrf-id1'=>$token2]);
        $valid3 = ResettableCSRF::is_request_valid('id1', ['csrf-id1'=>$token3]);
        $this->is_true($valid1 && $valid2 && $valid3);

        $this->test("Multiple forms, same IDs, different requests for setup and validation");
        ResettableCSRF::reset();
        $token1 = ResettableCSRF::get_csrf_value('id1', '', 10);
        ResettableCSRF::reset();
        $token2 = ResettableCSRF::get_csrf_value('id1', '', 10);
        ResettableCSRF::reset();
        $token3 = ResettableCSRF::get_csrf_value('id1', '', 10);
        ResettableCSRF::reset();
        $this->is_true($token1!=$token2 && $token2 != $token3);
        $valid1 = ResettableCSRF::is_request_valid('id1', ['csrf-id1'=>$token1]);
        ResettableCSRF::reset();
        $valid2 = ResettableCSRF::is_request_valid('id1', ['csrf-id1'=>$token2]);
        ResettableCSRF::reset();
        $valid3 = ResettableCSRF::is_request_valid('id1', ['csrf-id1'=>$token3]);
        ResettableCSRF::reset();
        $this->is_true($valid1 && $valid2 && $valid3);



        $pass = [
            'path_specified'=>[
                'form_id'=>'pass1',
                'request_uri'=>'/test1/',
                'target_uri'=>'/test1/',
                'host'=>'http://test1.com',
                'referer'=>'http://test1.com',
            ],

            'form_id_already_used'=>[
                'form_id'=>'pass1',
                'request_uri'=>'/test1/',
                'target_uri'=>'/test1/',
                'host'=>'http://test1.com',
                'referer'=>'http://test1.com',
            ],
                
            'any_path'=>[
                'form_id'=>'pass2',
                'request_uri'=>'/some-path/',
                'target_uri'=>'',
                'host'=>'http://test-path.com',
                'referer'=>'http://test-path.com',
            ],
        ];

        echo "\n\n#### TESTS THAT HAVE VALID CSRF CHECKS ####";
        foreach ($pass as $test_name => $test){
            $this->test($test_name);
            $_SERVER['REQUEST_URI'] = $test['request_uri'];
            $_SERVER['HTTP_HOST'] = $test['host'];
            $_SERVER['HTTP_REFERER'] = $test['referer'];


            $key = 'csrf-'.$test['form_id'];
            $value = CSRF::get_csrf_value($test['form_id'], $test['target_uri'], 10);

            $is_valid1 = CSRF::is_request_valid($test['form_id'], [$key=>$value]);
            $is_valid2 = CSRF::is_request_valid($test['form_id'], [$key=>$value]);
            $is_valid3 = CSRF::is_request_valid($test['form_id'], [$key=>$value]);

            $this->is_true($is_valid1&&$is_valid2&&$is_valid3);
        }

        $fail = [
            'target_uri_wrong'=>[
                'form_id'=>'fail1',
                'request_uri'=>'/wrong-uri/',
                'target_uri'=>'/test1/',
                'host'=>'http://test1.com',
                'referer'=>'http://test1.com',
            ],
                
            'test_no_path'=>[
                'form_id'=>'fail2',
                'request_uri'=>'',
                'target_uri'=>'/some-path/',
                'host'=>'http://test-path.com',
                'referer'=>'http://test-path.com',
            ],

            'test_no_referer'=>[
                'form_id'=>'fail3',
                'request_uri'=>'/fail1/',
                'target_uri'=>'/fail1/',
                'host'=>'http://test1.com',
                'referer'=>'',
            ],
            'test_no_referer_or_host'=>[
                'form_id'=>'fail4',
                'request_uri'=>'/fail1/',
                'target_uri'=>'/fail1/',
                'host'=>'',
                'referer'=>'',
            ],
            'test_wrong_key'=>[
                'form_id'=>'fail6',
                'request_uri'=>'/fail1/',
                'target_uri'=>'/fail1/',
                'host'=>'http://test1.com',
                'referer'=>'http://test1.com',
                'key'=>'csrf-wrongformid',
            ],

            'test_wrong_value'=>[
                'form_id'=>'fail7',
                'request_uri'=>'/fail1/',
                'target_uri'=>'/fail1/',
                'host'=>'http://test1.com',
                'referer'=>'http://test1.com',
                'value'=>'not-a-real-token',
            ],
            'test_wrong_formid'=>[
                'form_id'=>'fail8',
                'request_uri'=>'/fail1/',
                'target_uri'=>'/fail1/',
                'host'=>'http://test1.com',
                'referer'=>'http://test1.com',
                'receipt_form_id'=>'wrong-form-id',
            ],

            'test_expired'=>[
                'form_id'=>'fail9',
                'request_uri'=>'/fail1/',
                'target_uri'=>'/fail1/',
                'host'=>'http://test1.com',
                'referer'=>'http://test1.com',
                'expiry'=>-10
            ],

        ];

        echo "\n\n#### TESTS THAT FAIL CSRF CHECKS ####";
        foreach ($fail as $test_name => $test){
            $this->test($test_name);
            $_SERVER['REQUEST_URI'] = $test['request_uri'];
            $_SERVER['HTTP_HOST'] = $test['host'];
            $_SERVER['HTTP_REFERER'] = $test['referer'];

            $key = 'csrf-'.($test['key'] ?? $test['form_id']);
            $value = $test['value'] ?? CSRF::get_csrf_value($test['form_id'], $test['target_uri'],
               $test['expiry'] ?? 10);

            $is_valid = CSRF::is_request_valid($test['receipt_form_id'] ?? $test['form_id'], [$key=>$value]);

            $this->is_false($is_valid);
        }

        $exceptions = [
            'test_no_host'=>[
                'form_id'=>'fail5',
                'request_uri'=>'/fail1/',
                'target_uri'=>'/fail1/',
                'host'=>'',
                'referer'=>'http://test1.com',
            ],
            // I cannot test CSRF_TOKEN_MISMATCH (*which should NEVER happen*) or CSRF_SESSION_NOT_STARTED
        ];

        echo "\n\n#### TESTS THAT THROW EXCEPTIONS ####";
        foreach ($exceptions as $test_name => $test){
            $this->test($test_name);
            $_SERVER['REQUEST_URI'] = $test['request_uri'];
            $_SERVER['HTTP_HOST'] = $test['host'];
            $_SERVER['HTTP_REFERER'] = $test['referer'];

            $key = 'csrf-'.($test['key'] ?? $test['form_id']);
            $value = $test['value'] ?? CSRF::get_csrf_value($test['form_id'], $test['target_uri'], 10);

            $did_throw = false;
            try {
                $is_valid = CSRF::is_request_valid($test['receipt_form_id'] ?? $test['form_id'], [$key=>$value]);
            } catch (\Exception $e){
                $did_throw = true;
            }

            $this->is_true($did_throw);
        }

    }

}


/** allows us to pretend that a new request has started by resetting CSRF's static class variables to default state */
class ResettableCSRF extends CSRF {

    static public function reset(){
        static::$valid_requests = [];
        static::$generated_csrf = [];
    }
}