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
diff --git a/web/core/tests/Drupal/Tests/Core/Security/RequestSanitizerTest.php b/web/core/tests/Drupal/Tests/Core/Security/RequestSanitizerTest.php
new file mode 100644 (file)
index 0000000..e828de0
--- /dev/null
@@ -0,0 +1,370 @@
+<?php
+
+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() {
+    parent::setUp();
+    $this->errors = [];
+    set_error_handler([$this, "errorHandler"]);
+  }
+
+  /**
+   * 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 $whitelist
+   *   An array of keys to whitelist and not sanitize.
+   *
+   * @dataProvider providerTestRequestSanitization
+   */
+  public function testRequestSanitization(Request $request, array $expected = [], array $expected_errors = NULL, array $whitelist = []) {
+    // 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, $whitelist, is_null($expected_errors) ? FALSE : TRUE);
+
+    // Normalise 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 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 whitelist'] = [$request, ['query' => ['q' => 'index.php', 'foo' => ['#bar' => 'foo']]], [], ['#bar']];
+
+    $request = new Request([], ['#field' => 'value']);
+    $tests['no sanitization POST whitelist'] = [$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 whitelist'] = [$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) {
+    // 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) {
+    // 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) {
+    // 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) {
+    // 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 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 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) {
+    $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.
+   */
+  protected function assertError($errstr, $errno) {
+    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));
+  }
+
+}