diff --git a/core/lib/Drupal/Core/Form/FormBuilder.php b/core/lib/Drupal/Core/Form/FormBuilder.php
index fe6d104..e6150c2 100644
--- a/core/lib/Drupal/Core/Form/FormBuilder.php
+++ b/core/lib/Drupal/Core/Form/FormBuilder.php
@@ -105,6 +105,33 @@ class FormBuilder implements FormBuilderInterface, FormValidatorInterface, FormS
   protected $formCache;
 
   /**
+   * Defines element value callables which are safe to run even when the form
+   * state has an invalid CSRF token.
+   *
+   * Excluded from this list on purpose:
+   *  - Drupal\file\Element\ManagedFile::valueCallback
+   *  - Drupal\Core\Datetime\Element\Datelist::valueCallback
+   *  - Drupal\Core\Datetime\Element\Datetime::valueCallback
+   *  - Drupal\Core\Render\Element\ImageButton::valueCallback
+   *  - Drupal\file\Plugin\Field\FieldWidget\FileWidget::value
+   *  - color_palette_color_value
+   *
+   * @var array
+   */
+  protected $safeCoreValueCallables = [
+    'Drupal\Core\Render\Element\Checkbox::valueCallback',
+    'Drupal\Core\Render\Element\Checkboxes::valueCallback',
+    'Drupal\Core\Render\Element\PasswordConfirm::valueCallback',
+    'Drupal\Core\Render\Element\Radios::valueCallback',
+    'Drupal\Core\Render\Element\Range::valueCallback',
+    'Drupal\Core\Render\Element\Select::valueCallback',
+    'Drupal\Core\Render\Element\Tableselect::valueCallback',
+    'Drupal\Core\Render\Element\Textarea::valueCallback',
+    'Drupal\Core\Render\Element\Textfield::valueCallback',
+    'Drupal\Core\Render\Element\Token::valueCallback',
+  ];
+
+  /**
    * Constructs a new FormBuilder.
    *
    * @param \Drupal\Core\Form\FormValidatorInterface $form_validator
@@ -521,11 +548,6 @@ public function processForm($form_id, &$form, FormStateInterface &$form_state) {
 
     // Only process the input if we have a correct form submission.
     if ($form_state->isProcessingInput()) {
-      // Form constructors may explicitly set #token to FALSE when cross site
-      // request forgery is irrelevant to the form, such as search forms.
-      if (isset($form['#token']) && $form['#token'] === FALSE) {
-        unset($form['#token']);
-      }
       // Form values for programmed form submissions typically do not include a
       // value for the submit button. But without a triggering element, a
       // potentially existing #limit_validation_errors property on the primary
@@ -648,25 +670,23 @@ public function prepareForm($form_id, &$form, FormStateInterface &$form_state) {
     // since tokens are session-bound and forms displayed to anonymous users are
     // very likely cached, we cannot assign a token for them.
     // During installation, there is no $user yet.
-    if ($user && $user->isAuthenticated() && !$form_state->isProgrammed()) {
-      // Form constructors may explicitly set #token to FALSE when cross site
-      // request forgery is irrelevant to the form, such as search forms.
-      if (isset($form['#token']) && $form['#token'] === FALSE) {
-        unset($form['#token']);
-      }
-      // Otherwise, generate a public token based on the form id.
-      else {
-        $form['#token'] = $form_id;
-        $form['form_token'] = array(
-          '#id' => Html::getUniqueId('edit-' . $form_id . '-form-token'),
-          '#type' => 'token',
-          '#default_value' => $this->csrfToken->get($form['#token']),
-          // Form processing and validation requires this value, so ensure the
-          // submitted form value appears literally, regardless of custom #tree
-          // and #parents being set elsewhere.
-          '#parents' => array('form_token'),
-        );
-      }
+    // Form constructors may explicitly set #token to FALSE when cross site
+    // request forgery is irrelevant to the form, such as search forms.
+    if ($form_state->isProgrammed() || (isset($form['#token']) && $form['#token'] === FALSE)) {
+      unset($form['#token']);
+    }
+    elseif ($user && $user->isAuthenticated()) {
+      // Generate a public token based on the form id.
+      $form['#token'] = $form_id;
+      $form['form_token'] = array(
+        '#id' => Html::getUniqueId('edit-' . $form_id . '-form-token'),
+        '#type' => 'token',
+        '#default_value' => $this->csrfToken->get($form['#token']),
+        // Form processing and validation requires this value, so ensure the
+        // submitted form value appears literally, regardless of custom #tree
+        // and #parents being set elsewhere.
+        '#parents' => array('form_token'),
+      );
     }
 
     if (isset($form_id)) {
@@ -743,6 +763,13 @@ protected function buildFormAction() {
   /**
    * {@inheritdoc}
    */
+  public function setInvalidTokenError(FormStateInterface $form_state) {
+    $this->formValidator->setInvalidTokenError($form_state);
+  }
+
+  /**
+   * {@inheritdoc}
+   */
   public function validateForm($form_id, &$form, FormStateInterface &$form_state) {
     $this->formValidator->validateForm($form_id, $form, $form_state);
   }
@@ -817,6 +844,20 @@ public function doBuildForm($form_id, &$element, FormStateInterface &$form_state
       $input = $form_state->getUserInput();
       if ($form_state->isProgrammed() || (!empty($input) && (isset($input['form_id']) && ($input['form_id'] == $form_id)))) {
         $form_state->setProcessInput();
+        if (isset($element['#token'])) {
+          $input = $form_state->getUserInput();
+          if (empty($input['form_token']) || !$this->csrfToken->validate($input['form_token'], $element['#token'])) {
+            // Set an early form error to block certain input processing since
+            // that opens the door for CSRF vulnerabilities.
+            $this->setInvalidTokenError($form_state);
+
+            // This value is checked in self::handleInputElement().
+            $form_state->setInvalidToken(TRUE);
+
+            // Make sure file uploads do not get processed.
+            $_FILES = array();
+          }
+        }
       }
       else {
         $form_state->setProcessInput(FALSE);
@@ -989,6 +1030,24 @@ public function doBuildForm($form_id, &$element, FormStateInterface &$form_state
   }
 
   /**
+   * Helper function to normalize the different callable formats.
+   */
+  protected function valueCallableIsSafe($value_callable) {
+    // The same static class method callable may be formatted in two array and
+    // two string forms:
+    // ['\Classname', 'methodname']
+    // ['Classname', 'methodname']
+    // '\Classname::methodname'
+    // 'Classname::methodname'
+    if (is_callable($value_callable, FALSE, $callable_name)) {
+      // The third parameter of is_callable() is set to a string form, but we
+      // still have to normalize further by stripping a leading '\'.
+      return in_array(ltrim($callable_name, '\\'), $this->safeCoreValueCallables);
+    }
+    return FALSE;
+  }
+
+  /**
    * Adds the #name and #value properties of an input element before rendering.
    */
   protected function handleInputElement($form_id, &$element, FormStateInterface &$form_state) {
@@ -1077,7 +1136,14 @@ protected function handleInputElement($form_id, &$element, FormStateInterface &$
         // If we have input for the current element, assign it to the #value
         // property, optionally filtered through $value_callback.
         if ($input_exists) {
-          $element['#value'] = call_user_func_array($value_callable, array(&$element, $input, &$form_state));
+          // Skip all value callbacks except safe ones like text if the CSRF
+          // token was invalid.
+          if (!$form_state->hasInvalidToken() || $this->valueCallableIsSafe($value_callable)) {
+            $element['#value'] = call_user_func_array($value_callable, array(&$element, $input, &$form_state));
+          }
+          else {
+            $input = NULL;
+          }
 
           if (!isset($element['#value']) && isset($input)) {
             $element['#value'] = $input;
diff --git a/core/lib/Drupal/Core/Form/FormState.php b/core/lib/Drupal/Core/Form/FormState.php
index 9cb0b0d..bd47f1e 100644
--- a/core/lib/Drupal/Core/Form/FormState.php
+++ b/core/lib/Drupal/Core/Form/FormState.php
@@ -105,6 +105,19 @@ class FormState implements FormStateInterface {
   protected $rebuild = FALSE;
 
   /**
+   * If set to TRUE the form will skip calling form element value callbacks,
+   * except for a select list of callbacks provided by Drupal core that are
+   * known to be safe.
+   *
+   * This property is uncacheable.
+   *
+   * @see self::setInvalidToken()
+   *
+   * @var bool
+   */
+  protected $invalidToken = FALSE;
+
+  /**
    * Used when a form needs to return some kind of a
    * \Symfony\Component\HttpFoundation\Response object, e.g., a
    * \Symfony\Component\HttpFoundation\BinaryFileResponse when triggering a
@@ -1284,6 +1297,21 @@ public function cleanValues() {
   }
 
   /**
+   * {@inheritdoc}
+   */
+  public function setInvalidToken($invalid_token) {
+    $this->invalidToken = (bool) $invalid_token;
+    return $this;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function hasInvalidToken() {
+    return $this->invalidToken;
+  }
+
+  /**
    * Wraps ModuleHandler::loadInclude().
    */
   protected function moduleLoadInclude($module, $type, $name = NULL) {
diff --git a/core/lib/Drupal/Core/Form/FormStateInterface.php b/core/lib/Drupal/Core/Form/FormStateInterface.php
index fd46f70..11b089b 100644
--- a/core/lib/Drupal/Core/Form/FormStateInterface.php
+++ b/core/lib/Drupal/Core/Form/FormStateInterface.php
@@ -562,6 +562,24 @@ public function setRebuild($rebuild = TRUE);
   public function isRebuilding();
 
   /**
+   * Flags the form state as having or not an invalid token.
+   *
+   * @param bool $invalid_token
+   *   Whether the form has an invalid token.
+   *
+   * @return $this
+   */
+  public function setInvalidToken($invalid_token);
+
+  /**
+   * Determines if the form has an invalid token.
+   *
+   * @return bool
+   *   TRUE if the form has an invalid token, FALSE otherwise.
+   */
+  public function hasInvalidToken();
+
+  /**
    * Converts support notations for a form callback to a valid callable.
    *
    * Specifically, supports methods on the form/callback object as strings when
diff --git a/core/lib/Drupal/Core/Form/FormValidator.php b/core/lib/Drupal/Core/Form/FormValidator.php
index 96f2320..10678e3 100644
--- a/core/lib/Drupal/Core/Form/FormValidator.php
+++ b/core/lib/Drupal/Core/Form/FormValidator.php
@@ -105,13 +105,12 @@ public function validateForm($form_id, &$form, FormStateInterface &$form_state)
     }
 
     // If the session token was set by self::prepareForm(), ensure that it
-    // matches the current user's session.
+    // matches the current user's session. This is duplicate to code in
+    // FormBuilder::doBuildForm() but left to protect any custom form handling
+    // code.
     if (isset($form['#token'])) {
-      if (!$this->csrfToken->validate($form_state->getValue('form_token'), $form['#token'])) {
-        $url = $this->requestStack->getCurrentRequest()->getRequestUri();
-
-        // Setting this error will cause the form to fail validation.
-        $form_state->setErrorByName('form_token', $this->t('The form has become outdated. Copy any unsaved work in the form below and then <a href="@link">reload this page</a>.', array('@link' => $url)));
+      if (!$this->csrfToken->validate($form_state->getValue('form_token'), $form['#token']) || $form_state->hasInvalidToken()) {
+        $this->setInvalidTokenError($form_state);
 
         // Stop here and don't run any further validation handlers, because they
         // could invoke non-safe operations which opens the door for CSRF
@@ -128,6 +127,16 @@ public function validateForm($form_id, &$form, FormStateInterface &$form_state)
   }
 
   /**
+   * {@inheritdoc}
+   */
+  public function setInvalidTokenError(FormStateInterface $form_state) {
+    $url = $this->requestStack->getCurrentRequest()->getRequestUri();
+
+    // Setting this error will cause the form to fail validation.
+    $form_state->setErrorByName('form_token', $this->t('The form has become outdated. Copy any unsaved work in the form below and then <a href="@link">reload this page</a>.', array('@link' => $url)));
+  }
+
+  /**
    * Handles validation errors for forms with limited validation.
    *
    * If validation errors are limited then remove any non validated form values,
diff --git a/core/lib/Drupal/Core/Form/FormValidatorInterface.php b/core/lib/Drupal/Core/Form/FormValidatorInterface.php
index 6f1cc52..d2c8945 100644
--- a/core/lib/Drupal/Core/Form/FormValidatorInterface.php
+++ b/core/lib/Drupal/Core/Form/FormValidatorInterface.php
@@ -54,4 +54,14 @@ public function executeValidateHandlers(&$form, FormStateInterface &$form_state)
    */
   public function validateForm($form_id, &$form, FormStateInterface &$form_state);
 
+  /**
+   * Sets a form_token error on the given form state.
+   *
+   * @param \Drupal\Core\Form\FormStateInterface $form_state
+   *   The current state of the form.
+   *
+   * @return $this
+   */
+  public function setInvalidTokenError(FormStateInterface $form_state);
+
 }
diff --git a/core/modules/file/src/Tests/FileManagedFileElementTest.php b/core/modules/file/src/Tests/FileManagedFileElementTest.php
index c91c494..9953217 100644
--- a/core/modules/file/src/Tests/FileManagedFileElementTest.php
+++ b/core/modules/file/src/Tests/FileManagedFileElementTest.php
@@ -37,6 +37,18 @@ function testManagedFile() {
           $this->drupalPostForm($path, array(), t('Save'));
           $this->assertRaw(t('The file ids are %fids.', array('%fids' => implode(',', array()))), 'Submitted without a file.');
 
+          // Submit with a file, but with an invalid form token. Ensure the file
+          // was not saved.
+          $last_fid_prior = $this->getLastFileId();
+          $edit = [
+            $file_field_name => drupal_realpath($test_file->getFileUri()),
+            'form_token' => 'invalid token',
+          ];
+          $this->drupalPostForm($path, $edit, t('Save'));
+          $this->assertText('The form has become outdated. Copy any unsaved work in the form below');
+          $last_fid = $this->getLastFileId();
+          $this->assertEqual($last_fid_prior, $last_fid, 'File was not saved when uploaded with an invalid form token.');
+
           // Submit a new file, without using the Upload button.
           $last_fid_prior = $this->getLastFileId();
           $edit = array($file_field_name => drupal_realpath($test_file->getFileUri()));
diff --git a/core/modules/system/src/Tests/Form/FormTest.php b/core/modules/system/src/Tests/Form/FormTest.php
index 67508a2..93e3ab0 100644
--- a/core/modules/system/src/Tests/Form/FormTest.php
+++ b/core/modules/system/src/Tests/Form/FormTest.php
@@ -232,6 +232,35 @@ function testRequiredCheckboxesRadio() {
   }
 
   /**
+   * Tests that input is retained for safe elements even with an invalid token.
+   *
+   * Submits a test form containing several types of form elements.
+   */
+  function testInputWithInvalidToken() {
+    // We need to be logged in to have CSRF tokens.
+    $account = $this->createUser();
+    $this->drupalLogin($account);
+    // Submit again with required fields set but an invalid form token and
+    // verify that all the values are retained.
+    $edit = array(
+      'textfield' => $this->randomString(),
+      'checkboxes[bar]' => TRUE,
+      'select' => 'bar',
+      'radios' => 'foo',
+      'form_token' => 'invalid token',
+    );
+    $this->drupalPostForm('form-test/validate-required', $edit, 'Submit');
+    $this->assertFieldByXpath('//div[contains(@class, "error")]', NULL, 'Error message is displayed with invalid token even when required fields are filled.');
+    $this->assertText('The form has become outdated. Copy any unsaved work in the form below');
+    // Verify that input elements retained the posted values.
+    $this->assertFieldByName('textfield', $edit['textfield']);
+    $this->assertNoFieldChecked('edit-checkboxes-foo');
+    $this->assertFieldChecked('edit-checkboxes-bar');
+    $this->assertOptionSelected('edit-select', 'bar');
+    // @todo - radios are not retaining the value.
+  }
+
+  /**
    * Tests validation for required textfield element without title.
    *
    * Submits a test form containing a textfield form element without title.
diff --git a/core/tests/Drupal/Tests/Core/Form/FormBuilderTest.php b/core/tests/Drupal/Tests/Core/Form/FormBuilderTest.php
index 11f66ef..9c6d727 100644
--- a/core/tests/Drupal/Tests/Core/Form/FormBuilderTest.php
+++ b/core/tests/Drupal/Tests/Core/Form/FormBuilderTest.php
@@ -12,6 +12,7 @@
 use Drupal\Core\Access\AccessResultForbidden;
 use Drupal\Core\DependencyInjection\ContainerInjectionInterface;
 use Drupal\Core\Form\EnforcedResponseException;
+use Drupal\Core\Form\FormBuilder;
 use Drupal\Core\Form\FormBuilderInterface;
 use Drupal\Core\Form\FormInterface;
 use Drupal\Core\Form\FormState;
@@ -681,6 +682,47 @@ public function providerTestChildAccessInheritance() {
     return $data;
   }
 
+  /**
+   * @covers ::valueCallableIsSafe
+   *
+   * @dataProvider providerTestValueCallableIsSafe
+   */
+  public function testValueCallableIsSafe($callback, $expected) {
+    $method = new \ReflectionMethod(FormBuilder::class, 'valueCallableIsSafe');
+    $method->setAccessible(true);
+    $is_safe = $method->invoke($this->formBuilder, $callback);
+    $this->assertSame($expected, $is_safe);
+  }
+
+  public function providerTestValueCallableIsSafe() {
+    $data = [];
+    $data['string_no_slash'] = [
+      'Drupal\Core\Render\Element\Token::valueCallback',
+      TRUE,
+    ];
+    $data['string_with_slash'] = [
+      '\Drupal\Core\Render\Element\Token::valueCallback',
+      TRUE,
+    ];
+    $data['array_no_slash'] = [
+      ['Drupal\Core\Render\Element\Token', 'valueCallback'],
+      TRUE,
+    ];
+    $data['array_with_slash'] = [
+      ['\Drupal\Core\Render\Element\Token', 'valueCallback'],
+      TRUE,
+    ];
+    $data['nonexistent'] = [
+      'Foo::bar',
+      FALSE,
+    ];
+    $data['closure'] = [
+      function () {},
+      FALSE,
+    ];
+    return $data;
+  }
+
 }
 
 class TestForm implements FormInterface {
