3 namespace Drupal\Tests\Core\Security;
5 use Drupal\Core\Security\RequestSanitizer;
6 use Drupal\Tests\UnitTestCase;
7 use Symfony\Component\HttpFoundation\Request;
10 * Tests RequestSanitizer class.
12 * @coversDefaultClass \Drupal\Core\Security\RequestSanitizer
13 * @runTestsInSeparateProcesses
14 * @preserveGlobalState disabled
17 class RequestSanitizerTest extends UnitTestCase {
20 * Log of errors triggered during sanitization.
29 protected function setUp() {
32 set_error_handler([$this, "errorHandler"]);
36 * Tests RequestSanitizer class.
38 * @param \Symfony\Component\HttpFoundation\Request $request
39 * The request to sanitize.
40 * @param array $expected
41 * An array of expected request parameters after sanitization. The possible
42 * keys are 'cookies', 'query', 'request' which correspond to the parameter
43 * bags names on the request object. These values are also used to test the
44 * PHP globals post sanitization.
45 * @param array|null $expected_errors
46 * An array of expected errors. If set to NULL then error logging is
48 * @param array $whitelist
49 * An array of keys to whitelist and not sanitize.
51 * @dataProvider providerTestRequestSanitization
53 public function testRequestSanitization(Request $request, array $expected = [], array $expected_errors = NULL, array $whitelist = []) {
55 $_GET = $request->query->all();
56 $_POST = $request->request->all();
57 $_COOKIE = $request->cookies->all();
58 $_REQUEST = array_merge($request->query->all(), $request->request->all());
59 $request->server->set('QUERY_STRING', http_build_query($request->query->all()));
60 $_SERVER['QUERY_STRING'] = $request->server->get('QUERY_STRING');
62 $request = RequestSanitizer::sanitize($request, $whitelist, is_null($expected_errors) ? FALSE : TRUE);
64 // Normalise the expected data.
65 $expected += ['cookies' => [], 'query' => [], 'request' => []];
66 $expected_query_string = http_build_query($expected['query']);
69 $this->assertEquals($expected['cookies'], $request->cookies->all());
70 $this->assertEquals($expected['query'], $request->query->all());
71 $this->assertEquals($expected['request'], $request->request->all());
72 $this->assertTrue($request->attributes->get(RequestSanitizer::SANITIZED));
73 // The request object normalizes the request query string.
74 $this->assertEquals(Request::normalizeQueryString($expected_query_string), $request->getQueryString());
77 $this->assertEquals($expected['cookies'], $_COOKIE);
78 $this->assertEquals($expected['query'], $_GET);
79 $this->assertEquals($expected['request'], $_POST);
80 $expected_request = array_merge($expected['query'], $expected['request']);
81 $this->assertEquals($expected_request, $_REQUEST);
82 $this->assertEquals($expected_query_string, $_SERVER['QUERY_STRING']);
84 // Ensure any expected errors have been triggered.
85 if (!empty($expected_errors)) {
86 foreach ($expected_errors as $expected_error) {
87 $this->assertError($expected_error, E_USER_NOTICE);
91 $this->assertEquals([], $this->errors);
96 * Data provider for testRequestSanitization.
100 public function providerTestRequestSanitization() {
103 $request = new Request(['q' => 'index.php']);
104 $tests['no sanitization GET'] = [$request, ['query' => ['q' => 'index.php']]];
106 $request = new Request([], ['field' => 'value']);
107 $tests['no sanitization POST'] = [$request, ['request' => ['field' => 'value']]];
109 $request = new Request([], [], [], ['key' => 'value']);
110 $tests['no sanitization COOKIE'] = [$request, ['cookies' => ['key' => 'value']]];
112 $request = new Request(['q' => 'index.php'], ['field' => 'value'], [], ['key' => 'value']);
113 $tests['no sanitization GET, POST, COOKIE'] = [$request, ['query' => ['q' => 'index.php'], 'request' => ['field' => 'value'], 'cookies' => ['key' => 'value']]];
115 $request = new Request(['q' => 'index.php']);
116 $tests['no sanitization GET log'] = [$request, ['query' => ['q' => 'index.php']], []];
118 $request = new Request([], ['field' => 'value']);
119 $tests['no sanitization POST log'] = [$request, ['request' => ['field' => 'value']], []];
121 $request = new Request([], [], [], ['key' => 'value']);
122 $tests['no sanitization COOKIE log'] = [$request, ['cookies' => ['key' => 'value']], []];
124 $request = new Request(['#q' => 'index.php']);
125 $tests['sanitization GET'] = [$request];
127 $request = new Request([], ['#field' => 'value']);
128 $tests['sanitization POST'] = [$request];
130 $request = new Request([], [], [], ['#key' => 'value']);
131 $tests['sanitization COOKIE'] = [$request];
133 $request = new Request(['#q' => 'index.php'], ['#field' => 'value'], [], ['#key' => 'value']);
134 $tests['sanitization GET, POST, COOKIE'] = [$request];
136 $request = new Request(['#q' => 'index.php']);
137 $tests['sanitization GET log'] = [$request, [], ['Potentially unsafe keys removed from query string parameters (GET): #q']];
139 $request = new Request([], ['#field' => 'value']);
140 $tests['sanitization POST log'] = [$request, [], ['Potentially unsafe keys removed from request body parameters (POST): #field']];
142 $request = new Request([], [], [], ['#key' => 'value']);
143 $tests['sanitization COOKIE log'] = [$request, [], ['Potentially unsafe keys removed from cookie parameters: #key']];
145 $request = new Request(['#q' => 'index.php'], ['#field' => 'value'], [], ['#key' => 'value']);
146 $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']];
148 $request = new Request(['q' => 'index.php', 'foo' => ['#bar' => 'foo']]);
149 $tests['recursive sanitization log'] = [$request, ['query' => ['q' => 'index.php', 'foo' => []]], ['Potentially unsafe keys removed from query string parameters (GET): #bar']];
151 $request = new Request(['q' => 'index.php', 'foo' => ['#bar' => 'foo']]);
152 $tests['recursive no sanitization whitelist'] = [$request, ['query' => ['q' => 'index.php', 'foo' => ['#bar' => 'foo']]], [], ['#bar']];
154 $request = new Request([], ['#field' => 'value']);
155 $tests['no sanitization POST whitelist'] = [$request, ['request' => ['#field' => 'value']], [], ['#field']];
157 $request = new Request(['q' => 'index.php', 'foo' => ['#bar' => 'foo', '#foo' => 'bar']]);
158 $tests['recursive multiple sanitization log'] = [$request, ['query' => ['q' => 'index.php', 'foo' => []]], ['Potentially unsafe keys removed from query string parameters (GET): #bar, #foo']];
160 $request = new Request(['#q' => 'index.php']);
161 $request->attributes->set(RequestSanitizer::SANITIZED, TRUE);
162 $tests['already sanitized request'] = [$request, ['query' => ['#q' => 'index.php']]];
164 $request = new Request(['destination' => 'whatever?%23test=value']);
165 $tests['destination removal GET'] = [$request];
167 $request = new Request([], ['destination' => 'whatever?%23test=value']);
168 $tests['destination removal POST'] = [$request];
170 $request = new Request([], [], [], ['destination' => 'whatever?%23test=value']);
171 $tests['destination removal COOKIE'] = [$request];
173 $request = new Request(['destination' => 'whatever?%23test=value']);
174 $tests['destination removal GET log'] = [$request, [], ['Potentially unsafe destination removed from query parameter bag because it contained the following keys: #test']];
176 $request = new Request([], ['destination' => 'whatever?%23test=value']);
177 $tests['destination removal POST log'] = [$request, [], ['Potentially unsafe destination removed from request parameter bag because it contained the following keys: #test']];
179 $request = new Request([], [], [], ['destination' => 'whatever?%23test=value']);
180 $tests['destination removal COOKIE log'] = [$request, [], ['Potentially unsafe destination removed from cookies parameter bag because it contained the following keys: #test']];
182 $request = new Request(['destination' => 'whatever?q[%23test]=value']);
183 $tests['destination removal subkey'] = [$request];
185 $request = new Request(['destination' => 'whatever?q[%23test]=value']);
186 $tests['destination whitelist'] = [$request, ['query' => ['destination' => 'whatever?q[%23test]=value']], [], ['#test']];
188 $request = new Request(['destination' => "whatever?\x00bar=base&%23test=value"]);
189 $tests['destination removal zero byte'] = [$request];
191 $request = new Request(['destination' => 'whatever?q=value']);
192 $tests['destination kept'] = [$request, ['query' => ['destination' => 'whatever?q=value']]];
194 $request = new Request(['destination' => 'whatever']);
195 $tests['destination no query'] = [$request, ['query' => ['destination' => 'whatever']]];
201 * Tests acceptable destinations are not removed from GET requests.
203 * @param string $destination
204 * The destination string to test.
206 * @dataProvider providerTestAcceptableDestinations
208 public function testAcceptableDestinationGet($destination) {
209 // Set up a GET request.
210 $request = $this->createRequestForTesting(['destination' => $destination]);
212 $request = RequestSanitizer::sanitize($request, [], TRUE);
214 $this->assertSame($destination, $request->query->get('destination', NULL));
215 $this->assertNull($request->request->get('destination', NULL));
216 $this->assertSame($destination, $_GET['destination']);
217 $this->assertSame($destination, $_REQUEST['destination']);
218 $this->assertArrayNotHasKey('destination', $_POST);
219 $this->assertEquals([], $this->errors);
223 * Tests unacceptable destinations are removed from GET requests.
225 * @param string $destination
226 * The destination string to test.
228 * @dataProvider providerTestSanitizedDestinations
230 public function testSanitizedDestinationGet($destination) {
231 // Set up a GET request.
232 $request = $this->createRequestForTesting(['destination' => $destination]);
234 $request = RequestSanitizer::sanitize($request, [], TRUE);
236 $this->assertNull($request->request->get('destination', NULL));
237 $this->assertNull($request->query->get('destination', NULL));
238 $this->assertArrayNotHasKey('destination', $_POST);
239 $this->assertArrayNotHasKey('destination', $_REQUEST);
240 $this->assertArrayNotHasKey('destination', $_GET);
241 $this->assertError('Potentially unsafe destination removed from query parameter bag because it points to an external URL.', E_USER_NOTICE);
245 * Tests acceptable destinations are not removed from POST requests.
247 * @param string $destination
248 * The destination string to test.
250 * @dataProvider providerTestAcceptableDestinations
252 public function testAcceptableDestinationPost($destination) {
253 // Set up a POST request.
254 $request = $this->createRequestForTesting([], ['destination' => $destination]);
256 $request = RequestSanitizer::sanitize($request, [], TRUE);
258 $this->assertSame($destination, $request->request->get('destination', NULL));
259 $this->assertNull($request->query->get('destination', NULL));
260 $this->assertSame($destination, $_POST['destination']);
261 $this->assertSame($destination, $_REQUEST['destination']);
262 $this->assertArrayNotHasKey('destination', $_GET);
263 $this->assertEquals([], $this->errors);
267 * Tests unacceptable destinations are removed from GET requests.
269 * @param string $destination
270 * The destination string to test.
272 * @dataProvider providerTestSanitizedDestinations
274 public function testSanitizedDestinationPost($destination) {
275 // Set up a POST request.
276 $request = $this->createRequestForTesting([], ['destination' => $destination]);
278 $request = RequestSanitizer::sanitize($request, [], TRUE);
280 $this->assertNull($request->request->get('destination', NULL));
281 $this->assertNull($request->query->get('destination', NULL));
282 $this->assertArrayNotHasKey('destination', $_POST);
283 $this->assertArrayNotHasKey('destination', $_REQUEST);
284 $this->assertArrayNotHasKey('destination', $_GET);
285 $this->assertError('Potentially unsafe destination removed from request parameter bag because it points to an external URL.', E_USER_NOTICE);
289 * Creates a request and sets PHP globals for testing.
291 * @param array $query
292 * (optional) The GET parameters.
293 * @param array $request
294 * (optional) The POST parameters.
296 * @return \Symfony\Component\HttpFoundation\Request
297 * The request object.
299 protected function createRequestForTesting(array $query = [], array $request = []) {
300 $request = new Request($query, $request);
303 $_GET = $request->query->all();
304 $_POST = $request->request->all();
305 $_COOKIE = $request->cookies->all();
306 $_REQUEST = array_merge($request->query->all(), $request->request->all());
307 $request->server->set('QUERY_STRING', http_build_query($request->query->all()));
308 $_SERVER['QUERY_STRING'] = $request->server->get('QUERY_STRING');
313 * Data provider for testing acceptable destinations.
315 public function providerTestAcceptableDestinations() {
317 // Standard internal example node path is present in the 'destination'
320 // Internal path with one leading slash is allowed.
321 $data[] = ['/example.com'];
322 // Internal URL using a colon is allowed.
323 $data[] = ['example:test'];
324 // Javascript URL is allowed because it is treated as an internal URL.
325 $data[] = ['javascript:alert(0)'];
330 * Data provider for testing sanitized destinations.
332 public function providerTestSanitizedDestinations() {
334 // External URL without scheme is not allowed.
335 $data[] = ['//example.com/test'];
336 // External URL is not allowed.
337 $data[] = ['http://example.com'];
342 * Catches and logs errors to $this->errors.
345 * The severity level of the error.
346 * @param string $errstr
349 public function errorHandler($errno, $errstr) {
350 $this->errors[] = compact('errno', 'errstr');
354 * Asserts that the expected error has been logged.
356 * @param string $errstr
359 * The severity level of the error.
361 protected function assertError($errstr, $errno) {
362 foreach ($this->errors as $error) {
363 if ($error['errstr'] === $errstr && $error['errno'] === $errno) {
367 $this->fail("Error with level $errno and message '$errstr' not found in " . var_export($this->errors, TRUE));