 .../Core/Access/CsrfRequestHeaderAccessCheck.php   |  5 +++-
 core/modules/rest/src/Tests/CreateTest.php         | 12 ++++++---
 core/modules/rest/src/Tests/DeleteTest.php         | 13 +++++++---
 core/modules/rest/src/Tests/RESTTestBase.php       | 29 ++++++++++++++--------
 core/modules/rest/src/Tests/UpdateTest.php         | 10 +++++---
 5 files changed, 48 insertions(+), 21 deletions(-)

diff --git a/core/lib/Drupal/Core/Access/CsrfRequestHeaderAccessCheck.php b/core/lib/Drupal/Core/Access/CsrfRequestHeaderAccessCheck.php
index 53f326d..5548c7f 100644
--- a/core/lib/Drupal/Core/Access/CsrfRequestHeaderAccessCheck.php
+++ b/core/lib/Drupal/Core/Access/CsrfRequestHeaderAccessCheck.php
@@ -97,12 +97,15 @@ public function access(Request $request, AccountInterface $account) {
       && $account->isAuthenticated()
       && $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');
       // @todo Remove validate call using 'rest' in 8.3.
       //   Kept here for sessions active during update.
       if (!$this->csrfToken->validate($csrf_token, self::TOKEN_KEY)
         && !$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.
diff --git a/core/modules/rest/src/Tests/CreateTest.php b/core/modules/rest/src/Tests/CreateTest.php
index 8c7db07..2454d2a 100644
--- a/core/modules/rest/src/Tests/CreateTest.php
+++ b/core/modules/rest/src/Tests/CreateTest.php
@@ -366,10 +366,16 @@ public function assertCreateEntityOverRestApi($entity_type, $serialized = NULL)
     // 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.
     // Try first without the CSRF token, which should fail.
-    $this->httpRequest('entity/' . $entity_type, 'POST', $serialized, $this->defaultMimeType, TRUE);
-    $this->assertResponse(403, 'X-CSRF-Token request header is missing');
+    $url = Url::fromUri('internal:/entity/' . $entity_type)->setOption('query', ['_format' => $this->defaultFormat]);
+    $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.
-    $response = $this->httpRequest('entity/' . $entity_type, 'POST', $serialized, $this->defaultMimeType);
+    $response = $this->httpRequest($url, 'POST', $serialized, $this->defaultMimeType);
     $this->assertResponse(201);
 
     // Make sure that the response includes an entity in the body and check the
diff --git a/core/modules/rest/src/Tests/DeleteTest.php b/core/modules/rest/src/Tests/DeleteTest.php
index a144342..88db0fd 100644
--- a/core/modules/rest/src/Tests/DeleteTest.php
+++ b/core/modules/rest/src/Tests/DeleteTest.php
@@ -38,10 +38,17 @@ public function testDelete() {
       $entity = $this->entityCreate($entity_type);
       $entity->save();
       // Try first to delete over REST API without the CSRF token.
-      $this->httpRequest($entity->urlInfo(), 'DELETE', NULL, NULL, TRUE);
-      $this->assertResponse(403, 'X-CSRF-Token request header is missing');
+      $url = $entity->toUrl()->setRouteParameter('_format', $this->defaultFormat);
+      $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.
-      $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
       // update.
       $storage = $this->container->get('entity_type.manager')
diff --git a/core/modules/rest/src/Tests/RESTTestBase.php b/core/modules/rest/src/Tests/RESTTestBase.php
index 2a75227..7cb0f1b 100644
--- a/core/modules/rest/src/Tests/RESTTestBase.php
+++ b/core/modules/rest/src/Tests/RESTTestBase.php
@@ -85,19 +85,22 @@ protected function setUp() {
    *   The body for POST and PUT.
    * @param string $mime_type
    *   The MIME type of the transmitted content.
-   * @param bool $forget_xcsrf_token
-   *   If TRUE, the CSRF token won't be included in request.
+   * @param bool $csrf_token
+   *   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
    *   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)) {
       $mime_type = $this->defaultMimeType;
     }
     if (!in_array($method, array('GET', 'HEAD', 'OPTIONS', 'TRACE'))) {
       // GET the CSRF token first for writing requests.
-      $token = $this->drupalGet('session/token');
+      $requested_token = $this->drupalGet('session/token');
     }
 
     $url = $this->buildUrl($url);
@@ -132,9 +135,9 @@ protected function httpRequest($url, $method, $body = NULL, $mime_type = NULL, $
           CURLOPT_POSTFIELDS => $body,
           CURLOPT_URL => $url,
           CURLOPT_NOBODY => FALSE,
-          CURLOPT_HTTPHEADER => !$forget_xcsrf_token ? array(
+          CURLOPT_HTTPHEADER => $csrf_token !== FALSE ? array(
             'Content-Type: ' . $mime_type,
-            'X-CSRF-Token: ' . $token,
+            'X-CSRF-Token: ' . ($csrf_token === NULL ? $requested_token : $csrf_token),
           ) : array(
             'Content-Type: ' . $mime_type,
           ),
@@ -148,9 +151,9 @@ protected function httpRequest($url, $method, $body = NULL, $mime_type = NULL, $
           CURLOPT_POSTFIELDS => $body,
           CURLOPT_URL => $url,
           CURLOPT_NOBODY => FALSE,
-          CURLOPT_HTTPHEADER => !$forget_xcsrf_token ? array(
+          CURLOPT_HTTPHEADER => $csrf_token !== FALSE ? array(
             'Content-Type: ' . $mime_type,
-            'X-CSRF-Token: ' . $token,
+            'X-CSRF-Token: ' . ($csrf_token === NULL ? $requested_token : $csrf_token),
           ) : array(
             'Content-Type: ' . $mime_type,
           ),
@@ -164,9 +167,9 @@ protected function httpRequest($url, $method, $body = NULL, $mime_type = NULL, $
           CURLOPT_POSTFIELDS => $body,
           CURLOPT_URL => $url,
           CURLOPT_NOBODY => FALSE,
-          CURLOPT_HTTPHEADER => !$forget_xcsrf_token ? array(
+          CURLOPT_HTTPHEADER => $csrf_token !== FALSE ? array(
             'Content-Type: ' . $mime_type,
-            'X-CSRF-Token: ' . $token,
+            'X-CSRF-Token: ' . ($csrf_token === NULL ? $requested_token : $csrf_token),
           ) : array(
             'Content-Type: ' . $mime_type,
           ),
@@ -179,7 +182,9 @@ protected function httpRequest($url, $method, $body = NULL, $mime_type = NULL, $
           CURLOPT_CUSTOMREQUEST => 'DELETE',
           CURLOPT_URL => $url,
           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;
     }
@@ -197,6 +202,8 @@ protected function httpRequest($url, $method, $body = NULL, $mime_type = NULL, $
 
     $this->verbose($method . ' request to: ' . $url .
       '<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 body: ' . $this->responseBody);
 
diff --git a/core/modules/rest/src/Tests/UpdateTest.php b/core/modules/rest/src/Tests/UpdateTest.php
index cdef82c..d3784ed 100644
--- a/core/modules/rest/src/Tests/UpdateTest.php
+++ b/core/modules/rest/src/Tests/UpdateTest.php
@@ -381,9 +381,13 @@ protected function patchEntity(EntityInterface $entity, array $read_only_fields,
     $serialized = $serializer->serialize($normalized, $format, $context);
 
     // Try first without CSRF token which should fail.
-    $this->httpRequest($url, 'PATCH', $serialized, $mime_type, TRUE);
-    $this->assertResponse(403, 'X-CSRF-Token request header is missing');
-
+    $this->httpRequest($url, 'PATCH', $serialized, $mime_type, FALSE);
+    $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.
     $this->httpRequest($url, 'PATCH', $serialized, $mime_type);
     $this->assertResponse(200);
