diff --git a/core/modules/rest/lib/Drupal/rest/RequestHandler.php b/core/modules/rest/lib/Drupal/rest/RequestHandler.php
index b8d6d42..41a390b 100644
--- a/core/modules/rest/lib/Drupal/rest/RequestHandler.php
+++ b/core/modules/rest/lib/Drupal/rest/RequestHandler.php
@@ -29,8 +29,13 @@ class RequestHandler extends ContainerAware {
    *   The response object.
    */
   public function handle(Request $request, $id = NULL) {
+    if (!$this->csrfValidation($request)) {
+      return new Response('CSRF validation failed.', 403, array('Content-Type' => 'text/plain'));
+    }
+
     $plugin = $request->attributes->get('_route')->getDefault('_plugin');
     $method = strtolower($request->getMethod());
+
     $resource = $this->container
       ->get('plugin.manager.rest')
       ->getInstance(array('id' => $plugin));
@@ -66,4 +71,43 @@ public function handle(Request $request, $id = NULL) {
     }
     return $response;
   }
+
+  /**
+   * Validates a request to prevent CSRF vulnerabilities.
+   *
+   * This method checks the X-CSRF-Token header on write operations (POST, PUT,
+   * DELETE etc.) if it has been authenticated with session cookies.
+   *
+   * @param \Symfony\Component\HttpFoundation\Request $request
+   *   The request object.
+   *
+   * @return bool
+   *   TRUE if the request was successfully verified, FALSE otherwise.
+   */
+  protected function csrfValidation(Request $request) {
+    $method = $request->getMethod();
+    $cookie = $request->cookies->get(session_name(), FALSE);
+    // This check only applies if
+    // 1. this is a write operation
+    // 2. the user was successfully authenticated and
+    // 3. the request comes with a session cookie.
+    if (!in_array($method, array('GET', 'HEAD', 'OPTIONS', 'TRACE'))
+      && user_is_logged_in()
+      && $cookie
+    ) {
+      $csrf_token = $request->headers->get('X-CSRF-Token');
+      return isset($csrf_token) && drupal_valid_token($csrf_token, 'rest');
+    }
+    return TRUE;
+  }
+
+  /**
+   * Generates a CSRF protecting session token.
+   *
+   * @return \Symfony\Component\HttpFoundation\Response
+   *   The response object.
+   */
+  public function csrfToken() {
+    return new Response(drupal_get_token('rest'), 200, array('Content-Type' => 'text/plain'));
+  }
 }
diff --git a/core/modules/rest/lib/Drupal/rest/Tests/CreateTest.php b/core/modules/rest/lib/Drupal/rest/Tests/CreateTest.php
index b47f296..c5b298a 100644
--- a/core/modules/rest/lib/Drupal/rest/Tests/CreateTest.php
+++ b/core/modules/rest/lib/Drupal/rest/Tests/CreateTest.php
@@ -53,7 +53,7 @@ public function testCreate() {
 
     // Get the new entity ID from the location header and try to read it from
     // the database.
-    $location_url = $this->responseHeaders['location'];
+    $location_url = $this->drupalGetHeader('location');
     $url_parts = explode('/', $location_url);
     $id = end($url_parts);
     $loaded_entity = entity_load($entity_type, $id);
@@ -68,16 +68,32 @@ public function testCreate() {
       $this->assertEqual($send_value, $actual_value, 'Created property ' . $property . ' expected: ' . $send_value . ', actual: ' . $actual_value);
     }
 
+    $loaded_entity->delete();
+    // Try to create an entity without the CSRF token.
+    $this->curlExec(array(
+      CURLOPT_HTTPGET => FALSE,
+      CURLOPT_POST => TRUE,
+      CURLOPT_CUSTOMREQUEST => 'POST',
+      CURLOPT_POSTFIELDS => $serialized,
+      CURLOPT_URL => url('entity/' . $entity_type, array('absolute' => TRUE)),
+      CURLOPT_NOBODY => FALSE,
+      CURLOPT_HTTPHEADER => array('Content-Type: application/vnd.drupal.ld+json'),
+    ));
+    $this->assertResponse(403);
+    $this->assertFalse(entity_load_multiple($entity_type, NULL, TRUE), 'No entity has been created in the database.');
+
     // Try to create an entity without proper permissions.
     $this->drupalLogout();
     $this->httpRequest('entity/' . $entity_type, 'POST', $serialized, 'application/vnd.drupal.ld+json');
     $this->assertResponse(403);
+    $this->assertFalse(entity_load_multiple($entity_type, NULL, TRUE), 'No entity has been created in the database.');
 
     // Try to create a resource which is not web API enabled.
     $this->enableService(FALSE);
     $this->drupalLogin($account);
     $this->httpRequest('entity/entity_test', 'POST', $serialized, 'application/vnd.drupal.ld+json');
     $this->assertResponse(404);
+    $this->assertFalse(entity_load_multiple($entity_type, NULL, TRUE), 'No entity has been created in the database.');
 
     // @todo Once EntityNG is implemented for other entity types add a security
     // test. It should not be possible for example to create a test entity on a
diff --git a/core/modules/rest/lib/Drupal/rest/Tests/DeleteTest.php b/core/modules/rest/lib/Drupal/rest/Tests/DeleteTest.php
index 66f85c4..ed2d04d 100644
--- a/core/modules/rest/lib/Drupal/rest/Tests/DeleteTest.php
+++ b/core/modules/rest/lib/Drupal/rest/Tests/DeleteTest.php
@@ -40,9 +40,6 @@ public function testDelete() {
       // Create a user account that has the required permissions to delete
       // resources via the web API.
       $account = $this->drupalCreateUser(array('restful delete entity:' . $entity_type));
-      // Reset cURL here because it is confused from our previously used cURL
-      // options.
-      unset($this->curlHandle);
       $this->drupalLogin($account);
 
       // Create an entity programmatically.
@@ -74,9 +71,6 @@ public function testDelete() {
     // Try to delete a resource which is not web API enabled.
     $this->enableService(FALSE);
     $account = $this->drupalCreateUser();
-    // Reset cURL here because it is confused from our previously used cURL
-    // options.
-    unset($this->curlHandle);
     $this->drupalLogin($account);
     $this->httpRequest('entity/user/' . $account->id(), 'DELETE');
     $user = entity_load('user', $account->id(), TRUE);
diff --git a/core/modules/rest/lib/Drupal/rest/Tests/RESTTestBase.php b/core/modules/rest/lib/Drupal/rest/Tests/RESTTestBase.php
index 3036977..f7dbdb6 100644
--- a/core/modules/rest/lib/Drupal/rest/Tests/RESTTestBase.php
+++ b/core/modules/rest/lib/Drupal/rest/Tests/RESTTestBase.php
@@ -15,13 +15,6 @@
 abstract class RESTTestBase extends WebTestBase {
 
   /**
-   * Stores HTTP response headers from the last HTTP request.
-   *
-   * @var array
-   */
-  protected $responseHeaders;
-
-  /**
    * Helper function to issue a HTTP request with simpletest's cURL.
    *
    * @param string $url
@@ -34,6 +27,10 @@
    *   The MIME type of the transmitted content.
    */
   protected function httpRequest($url, $method, $body = NULL, $format = 'application/ld+json') {
+    if (!in_array($method, array('GET', 'HEAD', 'OPTIONS', 'TRACE'))) {
+      // GET the CSRF token first for writing requests.
+      $token = $this->drupalGet('rest/session/token');
+    }
     switch ($method) {
       case 'GET':
         // Set query if there are additional GET parameters.
@@ -53,7 +50,10 @@ protected function httpRequest($url, $method, $body = NULL, $format = 'applicati
           CURLOPT_POSTFIELDS => $body,
           CURLOPT_URL => url($url, array('absolute' => TRUE)),
           CURLOPT_NOBODY => FALSE,
-          CURLOPT_HTTPHEADER => array('Content-Type: ' . $format),
+          CURLOPT_HTTPHEADER => array(
+            'Content-Type: ' . $format,
+            'X-CSRF-Token: ' . $token,
+          ),
         );
         break;
 
@@ -64,7 +64,10 @@ protected function httpRequest($url, $method, $body = NULL, $format = 'applicati
           CURLOPT_POSTFIELDS => $body,
           CURLOPT_URL => url($url, array('absolute' => TRUE)),
           CURLOPT_NOBODY => FALSE,
-          CURLOPT_HTTPHEADER => array('Content-Type: ' . $format),
+          CURLOPT_HTTPHEADER => array(
+            'Content-Type: ' . $format,
+            'X-CSRF-Token: ' . $token,
+          ),
         );
         break;
 
@@ -75,7 +78,10 @@ protected function httpRequest($url, $method, $body = NULL, $format = 'applicati
           CURLOPT_POSTFIELDS => $body,
           CURLOPT_URL => url($url, array('absolute' => TRUE)),
           CURLOPT_NOBODY => FALSE,
-          CURLOPT_HTTPHEADER => array('Content-Type: ' . $format),
+          CURLOPT_HTTPHEADER => array(
+            'Content-Type: ' . $format,
+            'X-CSRF-Token: ' . $token,
+          ),
         );
         break;
 
@@ -85,29 +91,21 @@ protected function httpRequest($url, $method, $body = NULL, $format = 'applicati
           CURLOPT_CUSTOMREQUEST => 'DELETE',
           CURLOPT_URL => url($url, array('absolute' => TRUE)),
           CURLOPT_NOBODY => FALSE,
+          CURLOPT_HTTPHEADER => array('X-CSRF-Token: ' . $token),
         );
         break;
     }
-    // Include all HTTP headers in the response.
-    $curl_options[CURLOPT_HEADER] = TRUE;
 
     $response = $this->curlExec($curl_options);
-
-    list($header, $body) = explode("\r\n\r\n", $response, 2);
-    $header_lines = explode("\r\n", $header);
-    foreach ($header_lines as $line) {
-      $parts = explode(':', $line, 2);
-      // Store the header keys lower cased to be more robust. Headers are case
-      // insensitive according to RFC 2616.
-      $this->responseHeaders[strtolower($parts[0])] = isset($parts[1]) ? trim($parts[1]) : '';
-    }
+    $headers = $this->drupalGetHeaders();
+    $headers = implode("\n", $headers);
 
     $this->verbose($method . ' request to: ' . $url .
       '<hr />Code: ' . curl_getinfo($this->curlHandle, CURLINFO_HTTP_CODE) .
-      '<hr />Response headers: ' . $header .
-      '<hr />Response body: ' . $body);
+      '<hr />Response headers: ' . $headers .
+      '<hr />Response body: ' . $response);
 
-    return $body;
+    return $response;
   }
 
   /**
@@ -198,7 +196,20 @@ protected function enableService($resource_type) {
    *   TRUE if the assertion succeeded, FALSE otherwise.
    */
   protected function assertHeader($header, $value, $message = '', $group = 'Browser') {
-    $match = isset($this->responseHeaders[$header]) && $this->responseHeaders[$header] == $value;
-    return $this->assertTrue($match, $message ? $message : 'HTTP response header ' . $header . ' with value ' . $value . ' found.', $group);
+    $header_value = $this->drupalGetHeader($header);
+    return $this->assertTrue($header_value == $value, $message ? $message : 'HTTP response header ' . $header . ' with value ' . $value . ' found.', $group);
+  }
+
+  /**
+   * Overrides WebTestBase::drupalLogin().
+   */
+  protected function drupalLogin($user) {
+    if (isset($this->curlHandle)) {
+      // cURL quirk: when setting CURLOPT_CUSTOMREQUEST to anything other than
+      // POST in httpRequest() it has to be restored to POST here. Otherwise the
+      // POST request to login a user will not work.
+      curl_setopt($this->curlHandle, CURLOPT_CUSTOMREQUEST, 'POST');
+    }
+    parent::drupalLogin($user);
   }
 }
diff --git a/core/modules/rest/lib/Drupal/rest/Tests/ReadTest.php b/core/modules/rest/lib/Drupal/rest/Tests/ReadTest.php
index 874ad13..0578fc3 100644
--- a/core/modules/rest/lib/Drupal/rest/Tests/ReadTest.php
+++ b/core/modules/rest/lib/Drupal/rest/Tests/ReadTest.php
@@ -42,9 +42,6 @@ public function testRead() {
       // Create a user account that has the required permissions to delete
       // resources via the web API.
       $account = $this->drupalCreateUser(array('restful get entity:' . $entity_type));
-      // Reset cURL here because it is confused from our previously used cURL
-      // options.
-      unset($this->curlHandle);
       $this->drupalLogin($account);
 
       // Create an entity programmatically.
@@ -80,9 +77,6 @@ public function testRead() {
     }
     // Try to read a resource which is not web API enabled.
     $account = $this->drupalCreateUser();
-    // Reset cURL here because it is confused from our previously used cURL
-    // options.
-    unset($this->curlHandle);
     $this->drupalLogin($account);
     $response = $this->httpRequest('entity/user/' . $account->id(), 'GET', NULL, 'application/vnd.drupal.ld+json');
     $this->assertResponse(404);
diff --git a/core/modules/rest/lib/Drupal/rest/Tests/UpdateTest.php b/core/modules/rest/lib/Drupal/rest/Tests/UpdateTest.php
index 83729aa..fa65a2f 100644
--- a/core/modules/rest/lib/Drupal/rest/Tests/UpdateTest.php
+++ b/core/modules/rest/lib/Drupal/rest/Tests/UpdateTest.php
@@ -89,9 +89,6 @@ public function testPatchUpdate() {
 
     // Try to update a resource which is not web API enabled.
     $this->enableService(FALSE);
-    // Reset cURL here because it is confused from our previously used cURL
-    // options.
-    unset($this->curlHandle);
     $this->drupalLogin($account);
     $this->httpRequest('entity/' . $entity_type . '/' . $entity->id(), 'PATCH', $serialized, 'application/vnd.drupal.ld+json');
     $this->assertResponse(404);
@@ -162,9 +159,6 @@ public function testPutUpdate() {
 
     // Try to update a resource which is not web API enabled.
     $this->enableService(FALSE);
-    // Reset cURL here because it is confused from our previously used cURL
-    // options.
-    unset($this->curlHandle);
     $this->drupalLogin($account);
     $this->httpRequest('entity/' . $entity_type . '/' . $entity->id(), 'PUT', $serialized, 'application/vnd.drupal.ld+json');
     $this->assertResponse(404);
diff --git a/core/modules/rest/rest.routing.yml b/core/modules/rest/rest.routing.yml
new file mode 100644
index 0000000..c7ef69e
--- /dev/null
+++ b/core/modules/rest/rest.routing.yml
@@ -0,0 +1,6 @@
+rest.csrftoken:
+  pattern: '/rest/session/token'
+  defaults:
+    _controller: '\Drupal\rest\RequestHandler::csrfToken'
+  requirements:
+    _access: 'TRUE'
