Updated Drupal to 8.6. This goes with the following updates because it's possible...
[yaffs-website] / web / core / tests / Drupal / Tests / Core / Security / RequestSanitizerTest.php
1 <?php
2
3 namespace Drupal\Tests\Core\Security;
4
5 use Drupal\Core\Security\RequestSanitizer;
6 use Drupal\Tests\UnitTestCase;
7 use Symfony\Component\HttpFoundation\Request;
8
9 /**
10  * Tests RequestSanitizer class.
11  *
12  * @coversDefaultClass \Drupal\Core\Security\RequestSanitizer
13  * @runTestsInSeparateProcesses
14  * @preserveGlobalState disabled
15  * @group Security
16  */
17 class RequestSanitizerTest extends UnitTestCase {
18
19   /**
20    * Log of errors triggered during sanitization.
21    *
22    * @var array
23    */
24   protected $errors;
25
26   /**
27    * {@inheritdoc}
28    */
29   protected function setUp() {
30     parent::setUp();
31     $this->errors = [];
32     set_error_handler([$this, "errorHandler"]);
33   }
34
35   /**
36    * Tests RequestSanitizer class.
37    *
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
47    *   disabled.
48    * @param array $whitelist
49    *   An array of keys to whitelist and not sanitize.
50    *
51    * @dataProvider providerTestRequestSanitization
52    */
53   public function testRequestSanitization(Request $request, array $expected = [], array $expected_errors = NULL, array $whitelist = []) {
54     // Set up globals.
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');
61
62     $request = RequestSanitizer::sanitize($request, $whitelist, is_null($expected_errors) ? FALSE : TRUE);
63
64     // Normalise the expected data.
65     $expected += ['cookies' => [], 'query' => [], 'request' => []];
66     $expected_query_string = http_build_query($expected['query']);
67
68     // Test the request.
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());
75
76     // Test PHP globals.
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']);
83
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);
88       }
89     }
90     else {
91       $this->assertEquals([], $this->errors);
92     }
93   }
94
95   /**
96    * Data provider for testRequestSanitization.
97    *
98    * @return array
99    */
100   public function providerTestRequestSanitization() {
101     $tests = [];
102
103     $request = new Request(['q' => 'index.php']);
104     $tests['no sanitization GET'] = [$request, ['query' => ['q' => 'index.php']]];
105
106     $request = new Request([], ['field' => 'value']);
107     $tests['no sanitization POST'] = [$request, ['request' => ['field' => 'value']]];
108
109     $request = new Request([], [], [], ['key' => 'value']);
110     $tests['no sanitization COOKIE'] = [$request, ['cookies' => ['key' => 'value']]];
111
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']]];
114
115     $request = new Request(['q' => 'index.php']);
116     $tests['no sanitization GET log'] = [$request, ['query' => ['q' => 'index.php']], []];
117
118     $request = new Request([], ['field' => 'value']);
119     $tests['no sanitization POST log'] = [$request, ['request' => ['field' => 'value']], []];
120
121     $request = new Request([], [], [], ['key' => 'value']);
122     $tests['no sanitization COOKIE log'] = [$request, ['cookies' => ['key' => 'value']], []];
123
124     $request = new Request(['#q' => 'index.php']);
125     $tests['sanitization GET'] = [$request];
126
127     $request = new Request([], ['#field' => 'value']);
128     $tests['sanitization POST'] = [$request];
129
130     $request = new Request([], [], [], ['#key' => 'value']);
131     $tests['sanitization COOKIE'] = [$request];
132
133     $request = new Request(['#q' => 'index.php'], ['#field' => 'value'], [], ['#key' => 'value']);
134     $tests['sanitization GET, POST, COOKIE'] = [$request];
135
136     $request = new Request(['#q' => 'index.php']);
137     $tests['sanitization GET log'] = [$request, [], ['Potentially unsafe keys removed from query string parameters (GET): #q']];
138
139     $request = new Request([], ['#field' => 'value']);
140     $tests['sanitization POST log'] = [$request, [], ['Potentially unsafe keys removed from request body parameters (POST): #field']];
141
142     $request = new Request([], [], [], ['#key' => 'value']);
143     $tests['sanitization COOKIE log'] = [$request, [], ['Potentially unsafe keys removed from cookie parameters: #key']];
144
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']];
147
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']];
150
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']];
153
154     $request = new Request([], ['#field' => 'value']);
155     $tests['no sanitization POST whitelist'] = [$request, ['request' => ['#field' => 'value']], [], ['#field']];
156
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']];
159
160     $request = new Request(['#q' => 'index.php']);
161     $request->attributes->set(RequestSanitizer::SANITIZED, TRUE);
162     $tests['already sanitized request'] = [$request, ['query' => ['#q' => 'index.php']]];
163
164     $request = new Request(['destination' => 'whatever?%23test=value']);
165     $tests['destination removal GET'] = [$request];
166
167     $request = new Request([], ['destination' => 'whatever?%23test=value']);
168     $tests['destination removal POST'] = [$request];
169
170     $request = new Request([], [], [], ['destination' => 'whatever?%23test=value']);
171     $tests['destination removal COOKIE'] = [$request];
172
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']];
175
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']];
178
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']];
181
182     $request = new Request(['destination' => 'whatever?q[%23test]=value']);
183     $tests['destination removal subkey'] = [$request];
184
185     $request = new Request(['destination' => 'whatever?q[%23test]=value']);
186     $tests['destination whitelist'] = [$request, ['query' => ['destination' => 'whatever?q[%23test]=value']], [], ['#test']];
187
188     $request = new Request(['destination' => "whatever?\x00bar=base&%23test=value"]);
189     $tests['destination removal zero byte'] = [$request];
190
191     $request = new Request(['destination' => 'whatever?q=value']);
192     $tests['destination kept'] = [$request, ['query' => ['destination' => 'whatever?q=value']]];
193
194     $request = new Request(['destination' => 'whatever']);
195     $tests['destination no query'] = [$request, ['query' => ['destination' => 'whatever']]];
196
197     return $tests;
198   }
199
200   /**
201    * Tests acceptable destinations are not removed from GET requests.
202    *
203    * @param string $destination
204    *   The destination string to test.
205    *
206    * @dataProvider providerTestAcceptableDestinations
207    */
208   public function testAcceptableDestinationGet($destination) {
209     // Set up a GET request.
210     $request = $this->createRequestForTesting(['destination' => $destination]);
211
212     $request = RequestSanitizer::sanitize($request, [], TRUE);
213
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);
220   }
221
222   /**
223    * Tests unacceptable destinations are removed from GET requests.
224    *
225    * @param string $destination
226    *   The destination string to test.
227    *
228    * @dataProvider providerTestSanitizedDestinations
229    */
230   public function testSanitizedDestinationGet($destination) {
231     // Set up a GET request.
232     $request = $this->createRequestForTesting(['destination' => $destination]);
233
234     $request = RequestSanitizer::sanitize($request, [], TRUE);
235
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);
242   }
243
244   /**
245    * Tests acceptable destinations are not removed from POST requests.
246    *
247    * @param string $destination
248    *   The destination string to test.
249    *
250    * @dataProvider providerTestAcceptableDestinations
251    */
252   public function testAcceptableDestinationPost($destination) {
253     // Set up a POST request.
254     $request = $this->createRequestForTesting([], ['destination' => $destination]);
255
256     $request = RequestSanitizer::sanitize($request, [], TRUE);
257
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);
264   }
265
266   /**
267    * Tests unacceptable destinations are removed from GET requests.
268    *
269    * @param string $destination
270    *   The destination string to test.
271    *
272    * @dataProvider providerTestSanitizedDestinations
273    */
274   public function testSanitizedDestinationPost($destination) {
275     // Set up a POST request.
276     $request = $this->createRequestForTesting([], ['destination' => $destination]);
277
278     $request = RequestSanitizer::sanitize($request, [], TRUE);
279
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);
286   }
287
288   /**
289    * Creates a request and sets PHP globals for testing.
290    *
291    * @param array $query
292    *   (optional) The GET parameters.
293    * @param array $request
294    *   (optional) The POST parameters.
295    *
296    * @return \Symfony\Component\HttpFoundation\Request
297    *   The request object.
298    */
299   protected function createRequestForTesting(array $query = [], array $request = []) {
300     $request = new Request($query, $request);
301
302     // Set up globals.
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');
309     return $request;
310   }
311
312   /**
313    * Data provider for testing acceptable destinations.
314    */
315   public function providerTestAcceptableDestinations() {
316     $data = [];
317     // Standard internal example node path is present in the 'destination'
318     // parameter.
319     $data[] = ['node'];
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)'];
326     return $data;
327   }
328
329   /**
330    * Data provider for testing sanitized destinations.
331    */
332   public function providerTestSanitizedDestinations() {
333     $data = [];
334     // External URL without scheme is not allowed.
335     $data[] = ['//example.com/test'];
336     // External URL is not allowed.
337     $data[] = ['http://example.com'];
338     return $data;
339   }
340
341   /**
342    * Catches and logs errors to $this->errors.
343    *
344    * @param int $errno
345    *   The severity level of the error.
346    * @param string $errstr
347    *   The error message.
348    */
349   public function errorHandler($errno, $errstr) {
350     $this->errors[] = compact('errno', 'errstr');
351   }
352
353   /**
354    * Asserts that the expected error has been logged.
355    *
356    * @param string $errstr
357    *   The error message.
358    * @param int $errno
359    *   The severity level of the error.
360    */
361   protected function assertError($errstr, $errno) {
362     foreach ($this->errors as $error) {
363       if ($error['errstr'] === $errstr && $error['errno'] === $errno) {
364         return;
365       }
366     }
367     $this->fail("Error with level $errno and message '$errstr' not found in " . var_export($this->errors, TRUE));
368   }
369
370 }