You are here

class RequestSanitizerTest in Drupal 9

Same name and namespace in other branches
  1. 8 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 17

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",
    ]);
  }

  /**
   * 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);

    // 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 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?\0bar=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));
  }

}

Members

Namesort descending Modifiers Type Description Overrides
PhpUnitWarnings::$deprecationWarnings private static property Deprecation warnings from PHPUnit to raise with @trigger_error().
PhpUnitWarnings::addWarning public function Converts PHPUnit deprecation warnings to E_USER_DEPRECATED.
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 function Data provider for testing acceptable destinations.
RequestSanitizerTest::providerTestRequestSanitization public function Data provider for testRequestSanitization.
RequestSanitizerTest::providerTestSanitizedDestinations public function Data provider for testing sanitized destinations.
RequestSanitizerTest::setUp protected function Overrides UnitTestCase::setUp
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::$randomGenerator protected property The random generator.
UnitTestCase::$root protected property The app root. 1
UnitTestCase::assertArrayEquals Deprecated protected function Asserts if two arrays are equal by sorting them first.
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::getConfigStorageStub public function Returns a stub config storage that returns the supplied configuration.
UnitTestCase::getContainerWithCacheTagsInvalidator protected function Sets up a container with a cache tags invalidator.
UnitTestCase::getRandomGenerator protected function Gets the random generator for the utility methods.
UnitTestCase::getStringTranslationStub public function Returns a stub translation manager that just returns the passed string.
UnitTestCase::randomMachineName public function Generates a unique random string containing letters and numbers.
UnitTestCase::setUpBeforeClass public static function