class RequestSanitizerTest

Same name in other branches
  1. 7.x modules/simpletest/tests/request_sanitizer.test \RequestSanitizerTest
  2. 9 core/tests/Drupal/Tests/Core/Security/RequestSanitizerTest.php \Drupal\Tests\Core\Security\RequestSanitizerTest
  3. 8.9.x core/tests/Drupal/Tests/Core/Security/RequestSanitizerTest.php \Drupal\Tests\Core\Security\RequestSanitizerTest
  4. 10 core/tests/Drupal/Tests/Core/Security/RequestSanitizerTest.php \Drupal\Tests\Core\Security\RequestSanitizerTest

Tests RequestSanitizer class.

@coversDefaultClass \Drupal\Core\Security\RequestSanitizer @runTestsInSeparateProcesses @preserveGlobalState disabled @group Security

Hierarchy

Expanded class hierarchy of RequestSanitizerTest

File

core/tests/Drupal/Tests/Core/Security/RequestSanitizerTest.php, line 19

Namespace

Drupal\Tests\Core\Security
View source
class RequestSanitizerTest extends UnitTestCase {
    
    /**
     * Log of errors triggered during sanitization.
     *
     * @var array
     */
    protected $errors;
    
    /**
     * {@inheritdoc}
     */
    protected function setUp() : void {
        parent::setUp();
        $this->errors = [];
        set_error_handler([
            $this,
            "errorHandler",
        ]);
    }
    
    /**
     * {@inheritdoc}
     */
    protected function tearDown() : void {
        restore_error_handler();
        parent::tearDown();
    }
    
    /**
     * Tests RequestSanitizer class.
     *
     * @param \Symfony\Component\HttpFoundation\Request $request
     *   The request to sanitize.
     * @param array $expected
     *   An array of expected request parameters after sanitization. The possible
     *   keys are 'cookies', 'query', 'request' which correspond to the parameter
     *   bags names on the request object. These values are also used to test the
     *   PHP globals post sanitization.
     * @param array|null $expected_errors
     *   An array of expected errors. If set to NULL then error logging is
     *   disabled.
     * @param array $allow_list
     *   An array of keys to allow and not sanitize.
     *
     * @dataProvider providerTestRequestSanitization
     */
    public function testRequestSanitization(Request $request, array $expected = [], ?array $expected_errors = NULL, array $allow_list = []) : void {
        // Set up globals.
        $_GET = $request->query
            ->all();
        $_POST = $request->request
            ->all();
        $_COOKIE = $request->cookies
            ->all();
        $_REQUEST = array_merge($request->query
            ->all(), $request->request
            ->all());
        $request->server
            ->set('QUERY_STRING', http_build_query($request->query
            ->all()));
        $_SERVER['QUERY_STRING'] = $request->server
            ->get('QUERY_STRING');
        $request = RequestSanitizer::sanitize($request, $allow_list, is_null($expected_errors) ? FALSE : TRUE);
        // Normalize the expected data.
        $expected += [
            'cookies' => [],
            'query' => [],
            'request' => [],
        ];
        $expected_query_string = http_build_query($expected['query']);
        // Test the request.
        $this->assertEquals($expected['cookies'], $request->cookies
            ->all());
        $this->assertEquals($expected['query'], $request->query
            ->all());
        $this->assertEquals($expected['request'], $request->request
            ->all());
        $this->assertTrue($request->attributes
            ->get(RequestSanitizer::SANITIZED));
        // The request object normalizes the request query string.
        $this->assertEquals(Request::normalizeQueryString($expected_query_string), $request->getQueryString());
        // Test PHP globals.
        $this->assertEquals($expected['cookies'], $_COOKIE);
        $this->assertEquals($expected['query'], $_GET);
        $this->assertEquals($expected['request'], $_POST);
        $expected_request = array_merge($expected['query'], $expected['request']);
        $this->assertEquals($expected_request, $_REQUEST);
        $this->assertEquals($expected_query_string, $_SERVER['QUERY_STRING']);
        // Ensure any expected errors have been triggered.
        if (!empty($expected_errors)) {
            foreach ($expected_errors as $expected_error) {
                $this->assertError($expected_error, E_USER_NOTICE);
            }
        }
        else {
            $this->assertEquals([], $this->errors);
        }
    }
    
    /**
     * Data provider for testRequestSanitization.
     *
     * @return array
     *   An array of test data for testRequestSanitization.
     */
    public static function providerTestRequestSanitization() {
        $tests = [];
        $request = new Request([
            'q' => 'index.php',
        ]);
        $tests['no sanitization GET'] = [
            $request,
            [
                'query' => [
                    'q' => 'index.php',
                ],
            ],
        ];
        $request = new Request([], [
            'field' => 'value',
        ]);
        $tests['no sanitization POST'] = [
            $request,
            [
                'request' => [
                    'field' => 'value',
                ],
            ],
        ];
        $request = new Request([], [], [], [
            'key' => 'value',
        ]);
        $tests['no sanitization COOKIE'] = [
            $request,
            [
                'cookies' => [
                    'key' => 'value',
                ],
            ],
        ];
        $request = new Request([
            'q' => 'index.php',
        ], [
            'field' => 'value',
        ], [], [
            'key' => 'value',
        ]);
        $tests['no sanitization GET, POST, COOKIE'] = [
            $request,
            [
                'query' => [
                    'q' => 'index.php',
                ],
                'request' => [
                    'field' => 'value',
                ],
                'cookies' => [
                    'key' => 'value',
                ],
            ],
        ];
        $request = new Request([
            'q' => 'index.php',
        ]);
        $tests['no sanitization GET log'] = [
            $request,
            [
                'query' => [
                    'q' => 'index.php',
                ],
            ],
            [],
        ];
        $request = new Request([], [
            'field' => 'value',
        ]);
        $tests['no sanitization POST log'] = [
            $request,
            [
                'request' => [
                    'field' => 'value',
                ],
            ],
            [],
        ];
        $request = new Request([], [], [], [
            'key' => 'value',
        ]);
        $tests['no sanitization COOKIE log'] = [
            $request,
            [
                'cookies' => [
                    'key' => 'value',
                ],
            ],
            [],
        ];
        $request = new Request([
            '#q' => 'index.php',
        ]);
        $tests['sanitization GET'] = [
            $request,
        ];
        $request = new Request([], [
            '#field' => 'value',
        ]);
        $tests['sanitization POST'] = [
            $request,
        ];
        $request = new Request([], [], [], [
            '#key' => 'value',
        ]);
        $tests['sanitization COOKIE'] = [
            $request,
        ];
        $request = new Request([
            '#q' => 'index.php',
        ], [
            '#field' => 'value',
        ], [], [
            '#key' => 'value',
        ]);
        $tests['sanitization GET, POST, COOKIE'] = [
            $request,
        ];
        $request = new Request([
            '#q' => 'index.php',
        ]);
        $tests['sanitization GET log'] = [
            $request,
            [],
            [
                'Potentially unsafe keys removed from query string parameters (GET): #q',
            ],
        ];
        $request = new Request([], [
            '#field' => 'value',
        ]);
        $tests['sanitization POST log'] = [
            $request,
            [],
            [
                'Potentially unsafe keys removed from request body parameters (POST): #field',
            ],
        ];
        $request = new Request([], [], [], [
            '#key' => 'value',
        ]);
        $tests['sanitization COOKIE log'] = [
            $request,
            [],
            [
                'Potentially unsafe keys removed from cookie parameters: #key',
            ],
        ];
        $request = new Request([
            '#q' => 'index.php',
        ], [
            '#field' => 'value',
        ], [], [
            '#key' => 'value',
        ]);
        $tests['sanitization GET, POST, COOKIE log'] = [
            $request,
            [],
            [
                'Potentially unsafe keys removed from query string parameters (GET): #q',
                'Potentially unsafe keys removed from request body parameters (POST): #field',
                'Potentially unsafe keys removed from cookie parameters: #key',
            ],
        ];
        $request = new Request([
            'q' => 'index.php',
            'foo' => [
                '#bar' => 'foo',
            ],
        ]);
        $tests['recursive sanitization log'] = [
            $request,
            [
                'query' => [
                    'q' => 'index.php',
                    'foo' => [],
                ],
            ],
            [
                'Potentially unsafe keys removed from query string parameters (GET): #bar',
            ],
        ];
        $request = new Request([
            'q' => 'index.php',
            'foo' => [
                '#bar' => 'foo',
            ],
        ]);
        $tests['recursive no sanitization allowed list'] = [
            $request,
            [
                'query' => [
                    'q' => 'index.php',
                    'foo' => [
                        '#bar' => 'foo',
                    ],
                ],
            ],
            [],
            [
                '#bar',
            ],
        ];
        $request = new Request([], [
            '#field' => 'value',
        ]);
        $tests['no sanitization POST allowed list'] = [
            $request,
            [
                'request' => [
                    '#field' => 'value',
                ],
            ],
            [],
            [
                '#field',
            ],
        ];
        $request = new Request([
            'q' => 'index.php',
            'foo' => [
                '#bar' => 'foo',
                '#foo' => 'bar',
            ],
        ]);
        $tests['recursive multiple sanitization log'] = [
            $request,
            [
                'query' => [
                    'q' => 'index.php',
                    'foo' => [],
                ],
            ],
            [
                'Potentially unsafe keys removed from query string parameters (GET): #bar, #foo',
            ],
        ];
        $request = new Request([
            '#q' => 'index.php',
        ]);
        $request->attributes
            ->set(RequestSanitizer::SANITIZED, TRUE);
        $tests['already sanitized request'] = [
            $request,
            [
                'query' => [
                    '#q' => 'index.php',
                ],
            ],
        ];
        $request = new Request([
            'destination' => 'whatever?%23test=value',
        ]);
        $tests['destination removal GET'] = [
            $request,
        ];
        $request = new Request([], [
            'destination' => 'whatever?%23test=value',
        ]);
        $tests['destination removal POST'] = [
            $request,
        ];
        $request = new Request([], [], [], [
            'destination' => 'whatever?%23test=value',
        ]);
        $tests['destination removal COOKIE'] = [
            $request,
        ];
        $request = new Request([
            'destination' => 'whatever?%23test=value',
        ]);
        $tests['destination removal GET log'] = [
            $request,
            [],
            [
                'Potentially unsafe destination removed from query parameter bag because it contained the following keys: #test',
            ],
        ];
        $request = new Request([], [
            'destination' => 'whatever?%23test=value',
        ]);
        $tests['destination removal POST log'] = [
            $request,
            [],
            [
                'Potentially unsafe destination removed from request parameter bag because it contained the following keys: #test',
            ],
        ];
        $request = new Request([], [], [], [
            'destination' => 'whatever?%23test=value',
        ]);
        $tests['destination removal COOKIE log'] = [
            $request,
            [],
            [
                'Potentially unsafe destination removed from cookies parameter bag because it contained the following keys: #test',
            ],
        ];
        $request = new Request([
            'destination' => 'whatever?q[%23test]=value',
        ]);
        $tests['destination removal subkey'] = [
            $request,
        ];
        $request = new Request([
            'destination' => 'whatever?q[%23test]=value',
        ]);
        $tests['destination allowed list'] = [
            $request,
            [
                'query' => [
                    'destination' => 'whatever?q[%23test]=value',
                ],
            ],
            [],
            [
                '#test',
            ],
        ];
        $request = new Request([
            'destination' => "whatever?\x00bar=base&%23test=value",
        ]);
        $tests['destination removal zero byte'] = [
            $request,
        ];
        $request = new Request([
            'destination' => 'whatever?q=value',
        ]);
        $tests['destination kept'] = [
            $request,
            [
                'query' => [
                    'destination' => 'whatever?q=value',
                ],
            ],
        ];
        $request = new Request([
            'destination' => 'whatever',
        ]);
        $tests['destination no query'] = [
            $request,
            [
                'query' => [
                    'destination' => 'whatever',
                ],
            ],
        ];
        return $tests;
    }
    
    /**
     * Tests acceptable destinations are not removed from GET requests.
     *
     * @param string $destination
     *   The destination string to test.
     *
     * @dataProvider providerTestAcceptableDestinations
     */
    public function testAcceptableDestinationGet($destination) : void {
        // Set up a GET request.
        $request = $this->createRequestForTesting([
            'destination' => $destination,
        ]);
        $request = RequestSanitizer::sanitize($request, [], TRUE);
        $this->assertSame($destination, $request->query
            ->get('destination', NULL));
        $this->assertNull($request->request
            ->get('destination', NULL));
        $this->assertSame($destination, $_GET['destination']);
        $this->assertSame($destination, $_REQUEST['destination']);
        $this->assertArrayNotHasKey('destination', $_POST);
        $this->assertEquals([], $this->errors);
    }
    
    /**
     * Tests unacceptable destinations are removed from GET requests.
     *
     * @param string $destination
     *   The destination string to test.
     *
     * @dataProvider providerTestSanitizedDestinations
     */
    public function testSanitizedDestinationGet($destination) : void {
        // Set up a GET request.
        $request = $this->createRequestForTesting([
            'destination' => $destination,
        ]);
        $request = RequestSanitizer::sanitize($request, [], TRUE);
        $this->assertNull($request->request
            ->get('destination', NULL));
        $this->assertNull($request->query
            ->get('destination', NULL));
        $this->assertArrayNotHasKey('destination', $_POST);
        $this->assertArrayNotHasKey('destination', $_REQUEST);
        $this->assertArrayNotHasKey('destination', $_GET);
        $this->assertError('Potentially unsafe destination removed from query parameter bag because it points to an external URL.', E_USER_NOTICE);
    }
    
    /**
     * Tests acceptable destinations are not removed from POST requests.
     *
     * @param string $destination
     *   The destination string to test.
     *
     * @dataProvider providerTestAcceptableDestinations
     */
    public function testAcceptableDestinationPost($destination) : void {
        // Set up a POST request.
        $request = $this->createRequestForTesting([], [
            'destination' => $destination,
        ]);
        $request = RequestSanitizer::sanitize($request, [], TRUE);
        $this->assertSame($destination, $request->request
            ->get('destination', NULL));
        $this->assertNull($request->query
            ->get('destination', NULL));
        $this->assertSame($destination, $_POST['destination']);
        $this->assertSame($destination, $_REQUEST['destination']);
        $this->assertArrayNotHasKey('destination', $_GET);
        $this->assertEquals([], $this->errors);
    }
    
    /**
     * Tests unacceptable destinations are removed from GET requests.
     *
     * @param string $destination
     *   The destination string to test.
     *
     * @dataProvider providerTestSanitizedDestinations
     */
    public function testSanitizedDestinationPost($destination) : void {
        // Set up a POST request.
        $request = $this->createRequestForTesting([], [
            'destination' => $destination,
        ]);
        $request = RequestSanitizer::sanitize($request, [], TRUE);
        $this->assertNull($request->request
            ->get('destination', NULL));
        $this->assertNull($request->query
            ->get('destination', NULL));
        $this->assertArrayNotHasKey('destination', $_POST);
        $this->assertArrayNotHasKey('destination', $_REQUEST);
        $this->assertArrayNotHasKey('destination', $_GET);
        $this->assertError('Potentially unsafe destination removed from request parameter bag because it points to an external URL.', E_USER_NOTICE);
    }
    
    /**
     * Creates a request and sets PHP globals for testing.
     *
     * @param array $query
     *   (optional) The GET parameters.
     * @param array $request
     *   (optional) The POST parameters.
     *
     * @return \Symfony\Component\HttpFoundation\Request
     *   The request object.
     */
    protected function createRequestForTesting(array $query = [], array $request = []) {
        $request = new Request($query, $request);
        // Set up globals.
        $_GET = $request->query
            ->all();
        $_POST = $request->request
            ->all();
        $_COOKIE = $request->cookies
            ->all();
        $_REQUEST = array_merge($request->query
            ->all(), $request->request
            ->all());
        $request->server
            ->set('QUERY_STRING', http_build_query($request->query
            ->all()));
        $_SERVER['QUERY_STRING'] = $request->server
            ->get('QUERY_STRING');
        return $request;
    }
    
    /**
     * Data provider for testing acceptable destinations.
     */
    public static function providerTestAcceptableDestinations() {
        $data = [];
        // Standard internal example node path is present in the 'destination'
        // parameter.
        $data[] = [
            'node',
        ];
        // Internal path with one leading slash is allowed.
        $data[] = [
            '/example.com',
        ];
        // Internal URL using a colon is allowed.
        $data[] = [
            'example:test',
        ];
        // JavaScript URL is allowed because it is treated as an internal URL.
        $data[] = [
            'javascript:alert(0)',
        ];
        return $data;
    }
    
    /**
     * Data provider for testing sanitized destinations.
     */
    public static function providerTestSanitizedDestinations() {
        $data = [];
        // External URL without scheme is not allowed.
        $data[] = [
            '//example.com/test',
        ];
        // External URL is not allowed.
        $data[] = [
            'http://example.com',
        ];
        return $data;
    }
    
    /**
     * Catches and logs errors to $this->errors.
     *
     * @param int $errno
     *   The severity level of the error.
     * @param string $errstr
     *   The error message.
     */
    public function errorHandler($errno, $errstr) : void {
        $this->errors[] = compact('errno', 'errstr');
    }
    
    /**
     * Asserts that the expected error has been logged.
     *
     * @param string $errstr
     *   The error message.
     * @param int $errno
     *   The severity level of the error.
     *
     * @internal
     */
    protected function assertError(string $errstr, int $errno) : void {
        foreach ($this->errors as $error) {
            if ($error['errstr'] === $errstr && $error['errno'] === $errno) {
                return;
            }
        }
        $this->fail("Error with level {$errno} and message '{$errstr}' not found in " . var_export($this->errors, TRUE));
    }

}

Members

Title Sort descending Modifiers Object type Summary Overriden Title
ExpectDeprecationTrait::expectDeprecation public function Adds an expected deprecation.
ExpectDeprecationTrait::setUpErrorHandler public function Sets up the test error handler.
ExpectDeprecationTrait::tearDownErrorHandler public function Tears down the test error handler.
RandomGeneratorTrait::getRandomGenerator protected function Gets the random generator for the utility methods.
RandomGeneratorTrait::randomMachineName protected function Generates a unique random string containing letters and numbers.
RandomGeneratorTrait::randomObject public function Generates a random PHP object.
RandomGeneratorTrait::randomString public function Generates a pseudo-random string of ASCII characters of codes 32 to 126.
RequestSanitizerTest::$errors protected property Log of errors triggered during sanitization.
RequestSanitizerTest::assertError protected function Asserts that the expected error has been logged.
RequestSanitizerTest::createRequestForTesting protected function Creates a request and sets PHP globals for testing.
RequestSanitizerTest::errorHandler public function Catches and logs errors to $this->errors.
RequestSanitizerTest::providerTestAcceptableDestinations public static function Data provider for testing acceptable destinations.
RequestSanitizerTest::providerTestRequestSanitization public static function Data provider for testRequestSanitization.
RequestSanitizerTest::providerTestSanitizedDestinations public static function Data provider for testing sanitized destinations.
RequestSanitizerTest::setUp protected function Overrides UnitTestCase::setUp
RequestSanitizerTest::tearDown protected function
RequestSanitizerTest::testAcceptableDestinationGet public function Tests acceptable destinations are not removed from GET requests.
RequestSanitizerTest::testAcceptableDestinationPost public function Tests acceptable destinations are not removed from POST requests.
RequestSanitizerTest::testRequestSanitization public function Tests RequestSanitizer class.
RequestSanitizerTest::testSanitizedDestinationGet public function Tests unacceptable destinations are removed from GET requests.
RequestSanitizerTest::testSanitizedDestinationPost public function Tests unacceptable destinations are removed from GET requests.
UnitTestCase::$root protected property The app root.
UnitTestCase::getClassResolverStub protected function Returns a stub class resolver.
UnitTestCase::getConfigFactoryStub public function Returns a stub config factory that behaves according to the passed array.
UnitTestCase::getContainerWithCacheTagsInvalidator protected function Sets up a container with a cache tags invalidator.
UnitTestCase::getStringTranslationStub public function Returns a stub translation manager that just returns the passed string.
UnitTestCase::setDebugDumpHandler public static function Registers the dumper CLI handler when the DebugDump extension is enabled.

Buggy or inaccurate documentation? Please file an issue. Need support? Need help programming? Connect with the Drupal community.