RequestSanitizerTest.php
Same filename in other branches
Namespace
Drupal\Tests\Core\SecurityFile
-
core/
tests/ Drupal/ Tests/ Core/ Security/ RequestSanitizerTest.php
View source
<?php
declare (strict_types=1);
namespace Drupal\Tests\Core\Security;
use Drupal\Core\Security\RequestSanitizer;
use Drupal\Tests\UnitTestCase;
use Symfony\Component\HttpFoundation\Request;
/**
* Tests RequestSanitizer class.
*
* @coversDefaultClass \Drupal\Core\Security\RequestSanitizer
* @runTestsInSeparateProcesses
* @preserveGlobalState disabled
* @group Security
*/
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
*/
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));
}
}
Classes
Title | Deprecated | Summary |
---|---|---|
RequestSanitizerTest | Tests RequestSanitizer class. |
Buggy or inaccurate documentation? Please file an issue. Need support? Need help programming? Connect with the Drupal community.