Issue #2795965 by Wim Leers, neclimdul: REST requests with invalid X-CSRF-Token header get "missing " mesage
parent
7f43b7ba67
commit
676f38d8c7
|
@ -97,12 +97,15 @@ class CsrfRequestHeaderAccessCheck implements AccessCheckInterface {
|
||||||
&& $account->isAuthenticated()
|
&& $account->isAuthenticated()
|
||||||
&& $this->sessionConfiguration->hasSession($request)
|
&& $this->sessionConfiguration->hasSession($request)
|
||||||
) {
|
) {
|
||||||
|
if (!$request->headers->has('X-CSRF-Token')) {
|
||||||
|
return AccessResult::forbidden()->setReason('X-CSRF-Token request header is missing')->setCacheMaxAge(0);
|
||||||
|
}
|
||||||
$csrf_token = $request->headers->get('X-CSRF-Token');
|
$csrf_token = $request->headers->get('X-CSRF-Token');
|
||||||
// @todo Remove validate call using 'rest' in 8.3.
|
// @todo Remove validate call using 'rest' in 8.3.
|
||||||
// Kept here for sessions active during update.
|
// Kept here for sessions active during update.
|
||||||
if (!$this->csrfToken->validate($csrf_token, self::TOKEN_KEY)
|
if (!$this->csrfToken->validate($csrf_token, self::TOKEN_KEY)
|
||||||
&& !$this->csrfToken->validate($csrf_token, 'rest')) {
|
&& !$this->csrfToken->validate($csrf_token, 'rest')) {
|
||||||
return AccessResult::forbidden()->setReason('X-CSRF-Token request header is missing')->setCacheMaxAge(0);
|
return AccessResult::forbidden()->setReason('X-CSRF-Token request header is invalid')->setCacheMaxAge(0);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// Let other access checkers decide if the request is legit.
|
// Let other access checkers decide if the request is legit.
|
||||||
|
|
|
@ -366,10 +366,16 @@ class CreateTest extends RESTTestBase {
|
||||||
// Note: this will fail with PHP 5.6 when always_populate_raw_post_data is
|
// Note: this will fail with PHP 5.6 when always_populate_raw_post_data is
|
||||||
// set to something other than -1. See https://www.drupal.org/node/2456025.
|
// set to something other than -1. See https://www.drupal.org/node/2456025.
|
||||||
// Try first without the CSRF token, which should fail.
|
// Try first without the CSRF token, which should fail.
|
||||||
$this->httpRequest('entity/' . $entity_type, 'POST', $serialized, $this->defaultMimeType, TRUE);
|
$url = Url::fromUri('internal:/entity/' . $entity_type)->setOption('query', ['_format' => $this->defaultFormat]);
|
||||||
$this->assertResponse(403, 'X-CSRF-Token request header is missing');
|
$this->httpRequest($url, 'POST', $serialized, $this->defaultMimeType, FALSE);
|
||||||
|
$this->assertResponse(403);
|
||||||
|
$this->assertRaw('X-CSRF-Token request header is missing');
|
||||||
|
// Then try with an invalid CSRF token.
|
||||||
|
$this->httpRequest($url, 'POST', $serialized, $this->defaultMimeType, 'invalid-csrf-token');
|
||||||
|
$this->assertResponse(403);
|
||||||
|
$this->assertRaw('X-CSRF-Token request header is invalid');
|
||||||
// Then try with the CSRF token.
|
// Then try with the CSRF token.
|
||||||
$response = $this->httpRequest('entity/' . $entity_type, 'POST', $serialized, $this->defaultMimeType);
|
$response = $this->httpRequest($url, 'POST', $serialized, $this->defaultMimeType);
|
||||||
$this->assertResponse(201);
|
$this->assertResponse(201);
|
||||||
|
|
||||||
// Make sure that the response includes an entity in the body and check the
|
// Make sure that the response includes an entity in the body and check the
|
||||||
|
|
|
@ -38,10 +38,17 @@ class DeleteTest extends RESTTestBase {
|
||||||
$entity = $this->entityCreate($entity_type);
|
$entity = $this->entityCreate($entity_type);
|
||||||
$entity->save();
|
$entity->save();
|
||||||
// Try first to delete over REST API without the CSRF token.
|
// Try first to delete over REST API without the CSRF token.
|
||||||
$this->httpRequest($entity->urlInfo(), 'DELETE', NULL, NULL, TRUE);
|
$url = $entity->toUrl()->setRouteParameter('_format', $this->defaultFormat);
|
||||||
$this->assertResponse(403, 'X-CSRF-Token request header is missing');
|
$this->httpRequest($url, 'DELETE', NULL, 'application/hal+json', FALSE);
|
||||||
|
$this->assertResponse(403);
|
||||||
|
$this->assertRaw('X-CSRF-Token request header is missing');
|
||||||
|
// Then try with an invalid CSRF token.
|
||||||
|
$this->httpRequest($url, 'DELETE', NULL, 'application/hal+json', 'invalid-csrf-token');
|
||||||
|
$this->assertResponse(403);
|
||||||
|
$this->assertRaw('X-CSRF-Token request header is invalid');
|
||||||
// Delete it over the REST API.
|
// Delete it over the REST API.
|
||||||
$response = $this->httpRequest($entity->urlInfo(), 'DELETE');
|
$response = $this->httpRequest($url, 'DELETE');
|
||||||
|
$this->assertResponse(204);
|
||||||
// Clear the static cache with entity_load(), otherwise we won't see the
|
// Clear the static cache with entity_load(), otherwise we won't see the
|
||||||
// update.
|
// update.
|
||||||
$storage = $this->container->get('entity_type.manager')
|
$storage = $this->container->get('entity_type.manager')
|
||||||
|
|
|
@ -85,19 +85,22 @@ abstract class RESTTestBase extends WebTestBase {
|
||||||
* The body for POST and PUT.
|
* The body for POST and PUT.
|
||||||
* @param string $mime_type
|
* @param string $mime_type
|
||||||
* The MIME type of the transmitted content.
|
* The MIME type of the transmitted content.
|
||||||
* @param bool $forget_xcsrf_token
|
* @param bool $csrf_token
|
||||||
* If TRUE, the CSRF token won't be included in request.
|
* If NULL, a CSRF token will be retrieved and used. If FALSE, omit the
|
||||||
|
* X-CSRF-Token request header (to simulate developer error). Otherwise, the
|
||||||
|
* passed in value will be used as the value for the X-CSRF-Token request
|
||||||
|
* header (to simulate developer error, by sending an invalid CSRF token).
|
||||||
*
|
*
|
||||||
* @return string
|
* @return string
|
||||||
* The content returned from the request.
|
* The content returned from the request.
|
||||||
*/
|
*/
|
||||||
protected function httpRequest($url, $method, $body = NULL, $mime_type = NULL, $forget_xcsrf_token = FALSE) {
|
protected function httpRequest($url, $method, $body = NULL, $mime_type = NULL, $csrf_token = NULL) {
|
||||||
if (!isset($mime_type)) {
|
if (!isset($mime_type)) {
|
||||||
$mime_type = $this->defaultMimeType;
|
$mime_type = $this->defaultMimeType;
|
||||||
}
|
}
|
||||||
if (!in_array($method, array('GET', 'HEAD', 'OPTIONS', 'TRACE'))) {
|
if (!in_array($method, array('GET', 'HEAD', 'OPTIONS', 'TRACE'))) {
|
||||||
// GET the CSRF token first for writing requests.
|
// GET the CSRF token first for writing requests.
|
||||||
$token = $this->drupalGet('session/token');
|
$requested_token = $this->drupalGet('session/token');
|
||||||
}
|
}
|
||||||
|
|
||||||
$url = $this->buildUrl($url);
|
$url = $this->buildUrl($url);
|
||||||
|
@ -132,9 +135,9 @@ abstract class RESTTestBase extends WebTestBase {
|
||||||
CURLOPT_POSTFIELDS => $body,
|
CURLOPT_POSTFIELDS => $body,
|
||||||
CURLOPT_URL => $url,
|
CURLOPT_URL => $url,
|
||||||
CURLOPT_NOBODY => FALSE,
|
CURLOPT_NOBODY => FALSE,
|
||||||
CURLOPT_HTTPHEADER => !$forget_xcsrf_token ? array(
|
CURLOPT_HTTPHEADER => $csrf_token !== FALSE ? array(
|
||||||
'Content-Type: ' . $mime_type,
|
'Content-Type: ' . $mime_type,
|
||||||
'X-CSRF-Token: ' . $token,
|
'X-CSRF-Token: ' . ($csrf_token === NULL ? $requested_token : $csrf_token),
|
||||||
) : array(
|
) : array(
|
||||||
'Content-Type: ' . $mime_type,
|
'Content-Type: ' . $mime_type,
|
||||||
),
|
),
|
||||||
|
@ -148,9 +151,9 @@ abstract class RESTTestBase extends WebTestBase {
|
||||||
CURLOPT_POSTFIELDS => $body,
|
CURLOPT_POSTFIELDS => $body,
|
||||||
CURLOPT_URL => $url,
|
CURLOPT_URL => $url,
|
||||||
CURLOPT_NOBODY => FALSE,
|
CURLOPT_NOBODY => FALSE,
|
||||||
CURLOPT_HTTPHEADER => !$forget_xcsrf_token ? array(
|
CURLOPT_HTTPHEADER => $csrf_token !== FALSE ? array(
|
||||||
'Content-Type: ' . $mime_type,
|
'Content-Type: ' . $mime_type,
|
||||||
'X-CSRF-Token: ' . $token,
|
'X-CSRF-Token: ' . ($csrf_token === NULL ? $requested_token : $csrf_token),
|
||||||
) : array(
|
) : array(
|
||||||
'Content-Type: ' . $mime_type,
|
'Content-Type: ' . $mime_type,
|
||||||
),
|
),
|
||||||
|
@ -164,9 +167,9 @@ abstract class RESTTestBase extends WebTestBase {
|
||||||
CURLOPT_POSTFIELDS => $body,
|
CURLOPT_POSTFIELDS => $body,
|
||||||
CURLOPT_URL => $url,
|
CURLOPT_URL => $url,
|
||||||
CURLOPT_NOBODY => FALSE,
|
CURLOPT_NOBODY => FALSE,
|
||||||
CURLOPT_HTTPHEADER => !$forget_xcsrf_token ? array(
|
CURLOPT_HTTPHEADER => $csrf_token !== FALSE ? array(
|
||||||
'Content-Type: ' . $mime_type,
|
'Content-Type: ' . $mime_type,
|
||||||
'X-CSRF-Token: ' . $token,
|
'X-CSRF-Token: ' . ($csrf_token === NULL ? $requested_token : $csrf_token),
|
||||||
) : array(
|
) : array(
|
||||||
'Content-Type: ' . $mime_type,
|
'Content-Type: ' . $mime_type,
|
||||||
),
|
),
|
||||||
|
@ -179,7 +182,9 @@ abstract class RESTTestBase extends WebTestBase {
|
||||||
CURLOPT_CUSTOMREQUEST => 'DELETE',
|
CURLOPT_CUSTOMREQUEST => 'DELETE',
|
||||||
CURLOPT_URL => $url,
|
CURLOPT_URL => $url,
|
||||||
CURLOPT_NOBODY => FALSE,
|
CURLOPT_NOBODY => FALSE,
|
||||||
CURLOPT_HTTPHEADER => !$forget_xcsrf_token ? array('X-CSRF-Token: ' . $token) : array(),
|
CURLOPT_HTTPHEADER => $csrf_token !== FALSE ? array(
|
||||||
|
'X-CSRF-Token: ' . ($csrf_token === NULL ? $requested_token : $csrf_token),
|
||||||
|
) : array(),
|
||||||
);
|
);
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
@ -197,6 +202,8 @@ abstract class RESTTestBase extends WebTestBase {
|
||||||
|
|
||||||
$this->verbose($method . ' request to: ' . $url .
|
$this->verbose($method . ' request to: ' . $url .
|
||||||
'<hr />Code: ' . curl_getinfo($this->curlHandle, CURLINFO_HTTP_CODE) .
|
'<hr />Code: ' . curl_getinfo($this->curlHandle, CURLINFO_HTTP_CODE) .
|
||||||
|
(isset($curl_options[CURLOPT_HTTPHEADER]) ? '<hr />Request headers: ' . nl2br(print_r($curl_options[CURLOPT_HTTPHEADER], TRUE)) : '' ) .
|
||||||
|
(isset($curl_options[CURLOPT_POSTFIELDS]) ? '<hr />Request body: ' . nl2br(print_r($curl_options[CURLOPT_POSTFIELDS], TRUE)) : '' ) .
|
||||||
'<hr />Response headers: ' . nl2br(print_r($headers, TRUE)) .
|
'<hr />Response headers: ' . nl2br(print_r($headers, TRUE)) .
|
||||||
'<hr />Response body: ' . $this->responseBody);
|
'<hr />Response body: ' . $this->responseBody);
|
||||||
|
|
||||||
|
|
|
@ -381,9 +381,13 @@ class UpdateTest extends RESTTestBase {
|
||||||
$serialized = $serializer->serialize($normalized, $format, $context);
|
$serialized = $serializer->serialize($normalized, $format, $context);
|
||||||
|
|
||||||
// Try first without CSRF token which should fail.
|
// Try first without CSRF token which should fail.
|
||||||
$this->httpRequest($url, 'PATCH', $serialized, $mime_type, TRUE);
|
$this->httpRequest($url, 'PATCH', $serialized, $mime_type, FALSE);
|
||||||
$this->assertResponse(403, 'X-CSRF-Token request header is missing');
|
$this->assertResponse(403);
|
||||||
|
$this->assertRaw('X-CSRF-Token request header is missing');
|
||||||
|
// Then try with an invalid CSRF token.
|
||||||
|
$this->httpRequest($url, 'PATCH', $serialized, $mime_type, 'invalid-csrf-token');
|
||||||
|
$this->assertResponse(403);
|
||||||
|
$this->assertRaw('X-CSRF-Token request header is invalid');
|
||||||
// Then try with CSRF token.
|
// Then try with CSRF token.
|
||||||
$this->httpRequest($url, 'PATCH', $serialized, $mime_type);
|
$this->httpRequest($url, 'PATCH', $serialized, $mime_type);
|
||||||
$this->assertResponse(200);
|
$this->assertResponse(200);
|
||||||
|
|
Loading…
Reference in New Issue