diff --git src/wp-includes/class-wp-customize-manager.php src/wp-includes/class-wp-customize-manager.php
index 5a9770a..797a07d 100644
--- src/wp-includes/class-wp-customize-manager.php
+++ src/wp-includes/class-wp-customize-manager.php
@@ -1827,6 +1827,7 @@ final class WP_Customize_Manager {
 	 *     @type string $status   Post status. Optional. If supplied, the save will be transactional and a post revision will be allowed.
 	 *     @type string $title    Post title. Optional.
 	 *     @type string $date_gmt Date in GMT. Optional.
+	 *     @type int    $user_id  ID for user who is saving the changeset. Optional, defaults to the current user ID.
 	 * }
 	 *
 	 * @return array|WP_Error Returns array on success and WP_Error with array data on error.
@@ -1839,11 +1840,16 @@ final class WP_Customize_Manager {
 				'title' => null,
 				'data' => array(),
 				'date_gmt' => null,
+				'user_id' => get_current_user_id(),
 			),
 			$args
 		);
 
 		$changeset_post_id = $this->changeset_post_id();
+		$existing_changeset_data = array();
+		if ( $changeset_post_id ) {
+			$existing_changeset_data = $this->get_changeset_post_data( $changeset_post_id );
+		}
 
 		// The request was made via wp.customize.previewer.save().
 		$update_transactionally = (bool) $args['status'];
@@ -1863,6 +1869,37 @@ final class WP_Customize_Manager {
 		) );
 		$this->add_dynamic_settings( array_keys( $post_values ) ); // Ensure settings get created even if they lack an input value.
 
+		/*
+		 * Get list of IDs for settings that have values different from what is currently
+		 * saved in the changeset. By skipping any values that are already the same, the
+		 * subset of changed settings can be passed into validate_setting_values to prevent
+		 * an underprivileged modifying a single setting for which they have the capability
+		 * from being blocked from saving. This also prevents a user from touching of the
+		 * previous saved settings and overriding the associated user_id if they made no change.
+		 */
+		$changed_setting_ids = array();
+		foreach ( $post_values as $setting_id => $setting_value ) {
+			$setting = $this->get_setting( $setting_id );
+
+			if ( $setting && 'theme_mod' === $setting->type ) {
+				$prefixed_setting_id = $this->get_stylesheet() . '::' . $setting->id;
+			} else {
+				$prefixed_setting_id = $setting_id;
+			}
+
+			$is_value_changed = (
+				! isset( $existing_changeset_data[ $prefixed_setting_id ] )
+				||
+				! array_key_exists( 'value', $existing_changeset_data[ $prefixed_setting_id ] )
+				||
+				$existing_changeset_data[ $prefixed_setting_id ]['value'] !== $setting_value
+			);
+			if ( $is_value_changed ) {
+				$changed_setting_ids[] = $setting_id;
+			}
+		}
+		$post_values = wp_array_slice_assoc( $post_values, $changed_setting_ids );
+
 		/**
 		 * Fires before save validation happens.
 		 *
@@ -1943,7 +1980,10 @@ final class WP_Customize_Manager {
 				$data[ $changeset_setting_id ] = array_merge(
 					$data[ $changeset_setting_id ],
 					$setting_params,
-					array( 'type' => $setting->type )
+					array(
+						'type' => $setting->type,
+						'user_id' => $args['user_id'],
+					)
 				);
 			}
 		}
@@ -2121,29 +2161,38 @@ final class WP_Customize_Manager {
 		$previous_changeset_data    = $this->_changeset_data;
 		$this->_changeset_data      = $publishing_changeset_data;
 
-		// Ensure that other theme mods are stashed.
-		$other_theme_mod_settings = array();
-		if ( did_action( 'switch_theme' ) ) {
-			$namespace_pattern = '/^(?P<stylesheet>.+?)::(?P<setting_id>.+)$/';
-			$matches = array();
-			foreach ( $this->_changeset_data as $raw_setting_id => $setting_params ) {
-				$is_other_theme_mod = (
-					isset( $setting_params['value'] )
-					&&
-					isset( $setting_params['type'] )
-					&&
-					'theme_mod' === $setting_params['type']
-					&&
-					preg_match( $namespace_pattern, $raw_setting_id, $matches )
-					&&
-					$this->get_stylesheet() !== $matches['stylesheet']
-				);
-				if ( $is_other_theme_mod ) {
-					if ( ! isset( $other_theme_mod_settings[ $matches['stylesheet'] ] ) ) {
-						$other_theme_mod_settings[ $matches['stylesheet'] ] = array();
-					}
-					$other_theme_mod_settings[ $matches['stylesheet'] ][ $matches['setting_id'] ] = $setting_params;
+		// Parse changeset data to identify theme mod settings and user IDs associated with settings to be saved.
+		$setting_user_ids = array();
+		$theme_mod_settings = array();
+		$namespace_pattern = '/^(?P<stylesheet>.+?)::(?P<setting_id>.+)$/';
+		$matches = array();
+		foreach ( $this->_changeset_data as $raw_setting_id => $setting_params ) {
+			$actual_setting_id = null;
+			$is_theme_mod_setting = (
+				isset( $setting_params['value'] )
+				&&
+				isset( $setting_params['type'] )
+				&&
+				'theme_mod' === $setting_params['type']
+				&&
+				preg_match( $namespace_pattern, $raw_setting_id, $matches )
+			);
+			if ( $is_theme_mod_setting ) {
+				if ( ! isset( $theme_mod_settings[ $matches['stylesheet'] ] ) ) {
+					$theme_mod_settings[ $matches['stylesheet'] ] = array();
 				}
+				$theme_mod_settings[ $matches['stylesheet'] ][ $matches['setting_id'] ] = $setting_params;
+
+				if ( $this->get_stylesheet() === $matches['stylesheet'] ) {
+					$actual_setting_id = $matches['setting_id'];
+				}
+			} else {
+				$actual_setting_id = $raw_setting_id;
+			}
+
+			// Keep track of the user IDs for settings actually for this theme.
+			if ( $actual_setting_id && isset( $setting_params['user_id'] ) ) {
+				$setting_user_ids[ $actual_setting_id ] = $setting_params['user_id'];
 			}
 		}
 
@@ -2173,21 +2222,38 @@ final class WP_Customize_Manager {
 		$original_setting_capabilities = array();
 		foreach ( $changeset_setting_ids as $setting_id ) {
 			$setting = $this->get_setting( $setting_id );
-			if ( $setting ) {
+			if ( $setting && ! isset( $setting_user_ids[ $setting_id ] ) ) {
 				$original_setting_capabilities[ $setting->id ] = $setting->capability;
 				$setting->capability = 'exist';
 			}
 		}
 
+		$original_user_id = get_current_user_id();
 		foreach ( $changeset_setting_ids as $setting_id ) {
 			$setting = $this->get_setting( $setting_id );
 			if ( $setting ) {
+				/*
+				 * Set the current user to match the user who saved the value into
+				 * the changeset so that any filters that apply during the save
+				 * process will respect the original user's capabilities. This
+				 * will ensure, for example, that KSES won't strip unsafe HTML
+				 * when a scheduled changeset publishes via WP Cron.
+				 */
+				if ( isset( $setting_user_ids[ $setting_id ] ) ) {
+					wp_set_current_user( $setting_user_ids[ $setting_id ] );
+				} else {
+					wp_set_current_user( $original_user_id );
+				}
+
 				$setting->save();
 			}
 		}
+		wp_set_current_user( $original_user_id );
 
 		// Update the stashed theme mod settings, removing the active theme's stashed settings, if activated.
 		if ( did_action( 'switch_theme' ) ) {
+			$other_theme_mod_settings = $theme_mod_settings;
+			unset( $other_theme_mod_settings[ $this->get_stylesheet() ] );
 			$this->update_stashed_theme_mod_settings( $other_theme_mod_settings );
 		}
 
diff --git tests/phpunit/tests/customize/manager.php tests/phpunit/tests/customize/manager.php
index a28d834..e4c7f93 100644
--- tests/phpunit/tests/customize/manager.php
+++ tests/phpunit/tests/customize/manager.php
@@ -416,6 +416,7 @@ class Tests_WP_Customize_Manager extends WP_UnitTestCase {
 	 * @covers WP_Customize_Manager::save_changeset_post()
 	 */
 	function test_save_changeset_post_without_theme_activation() {
+		global $wp_customize;
 		wp_set_current_user( self::$admin_user_id );
 
 		$did_action = array(
@@ -428,22 +429,24 @@ class Tests_WP_Customize_Manager extends WP_UnitTestCase {
 		$manager = new WP_Customize_Manager( array(
 			'changeset_uuid' => $uuid,
 		) );
+		$wp_customize = $manager;
 		$manager->register_controls();
 		$manager->set_post_value( 'blogname', 'Changeset Title' );
 		$manager->set_post_value( 'blogdescription', 'Changeset Tagline' );
 
+		$pre_saved_data = array(
+			'blogname' => array(
+				'value' => 'Overridden Changeset Title',
+			),
+			'blogdescription' => array(
+				'custom' => 'something',
+			),
+		);
 		$r = $manager->save_changeset_post( array(
 			'status' => 'auto-draft',
 			'title' => 'Auto Draft',
 			'date_gmt' => '2010-01-01 00:00:00',
-			'data' => array(
-				'blogname' => array(
-					'value' => 'Overridden Changeset Title',
-				),
-				'blogdescription' => array(
-					'custom' => 'something',
-				),
-			),
+			'data' => $pre_saved_data,
 		) );
 		$this->assertInternalType( 'array', $r );
 
@@ -453,8 +456,14 @@ class Tests_WP_Customize_Manager extends WP_UnitTestCase {
 		$this->assertNotNull( $post_id );
 		$saved_data = json_decode( get_post( $post_id )->post_content, true );
 		$this->assertEquals( $manager->unsanitized_post_values(), wp_list_pluck( $saved_data, 'value' ) );
-		$this->assertEquals( 'Overridden Changeset Title', $saved_data['blogname']['value'] );
-		$this->assertEquals( 'something', $saved_data['blogdescription']['custom'] );
+		$this->assertEquals( $pre_saved_data['blogname']['value'], $saved_data['blogname']['value'] );
+		$this->assertEquals( $pre_saved_data['blogdescription']['custom'], $saved_data['blogdescription']['custom'] );
+		foreach ( $saved_data as $setting_id => $setting_params ) {
+			$this->assertArrayHasKey( 'type', $setting_params );
+			$this->assertEquals( 'option', $setting_params['type'] );
+			$this->assertArrayHasKey( 'user_id', $setting_params );
+			$this->assertEquals( self::$admin_user_id, $setting_params['user_id'] );
+		}
 		$this->assertEquals( 'Auto Draft', get_post( $post_id )->post_title );
 		$this->assertEquals( 'auto-draft', get_post( $post_id )->post_status );
 		$this->assertEquals( '2010-01-01 00:00:00', get_post( $post_id )->post_date_gmt );
@@ -510,6 +519,7 @@ class Tests_WP_Customize_Manager extends WP_UnitTestCase {
 		$manager = new WP_Customize_Manager( array(
 			'changeset_uuid' => $uuid,
 		) );
+		$wp_customize = $manager;
 		$manager->register_controls(); // That is, register settings.
 		$r = $manager->save_changeset_post( array(
 			'status' => null,
@@ -545,8 +555,9 @@ class Tests_WP_Customize_Manager extends WP_UnitTestCase {
 
 		// Publish the changeset.
 		$manager = new WP_Customize_Manager( array( 'changeset_uuid' => $uuid ) );
-		$manager->register_controls();
-		$GLOBALS['wp_customize'] = $manager;
+		$wp_customize = $manager;
+		do_action( 'customize_register', $wp_customize );
+		$original_capabilities = wp_list_pluck( $manager->settings(), 'capability' );
 		$r = $manager->save_changeset_post( array(
 			'status' => 'publish',
 			'data' => array(
@@ -558,13 +569,14 @@ class Tests_WP_Customize_Manager extends WP_UnitTestCase {
 		$this->assertInternalType( 'array', $r );
 		$this->assertEquals( 'Do it live \o/', get_option( 'blogname' ) );
 		$this->assertEquals( 'trash', get_post_status( $post_id ) ); // Auto-trashed.
+		$this->assertEquals( $original_capabilities, wp_list_pluck( $manager->settings(), 'capability' ) );
 
 		// Test revisions.
 		add_post_type_support( 'customize_changeset', 'revisions' );
 		$uuid = wp_generate_uuid4();
 		$manager = new WP_Customize_Manager( array( 'changeset_uuid' => $uuid ) );
-		$manager->register_controls();
-		$GLOBALS['wp_customize'] = $manager;
+		$wp_customize = $manager;
+		do_action( 'customize_register', $manager );
 
 		$manager->set_post_value( 'blogname', 'Hello Surface' );
 		$manager->save_changeset_post( array( 'status' => 'auto-draft' ) );
@@ -626,6 +638,7 @@ class Tests_WP_Customize_Manager extends WP_UnitTestCase {
 	 * @covers WP_Customize_Manager::update_stashed_theme_mod_settings()
 	 */
 	function test_save_changeset_post_with_theme_activation() {
+		global $wp_customize;
 		wp_set_current_user( self::$admin_user_id );
 
 		$preview_theme = $this->get_inactive_core_theme();
@@ -642,8 +655,8 @@ class Tests_WP_Customize_Manager extends WP_UnitTestCase {
 			'changeset_uuid' => $uuid,
 			'theme' => $preview_theme,
 		) );
-		$manager->register_controls();
-		$GLOBALS['wp_customize'] = $manager;
+		$wp_customize = $manager;
+		do_action( 'customize_register', $manager );
 
 		$manager->set_post_value( 'blogname', 'Hello Preview Theme' );
 		$post_values = $manager->unsanitized_post_values();
@@ -655,6 +668,200 @@ class Tests_WP_Customize_Manager extends WP_UnitTestCase {
 	}
 
 	/**
+	 * Test saving changesets with varying users and capabilities.
+	 *
+	 * @covers WP_Customize_Manager::save_changeset_post()
+	 */
+	function test_save_changeset_post_with_varying_users() {
+		global $wp_customize;
+
+		add_theme_support( 'custom-background' );
+		wp_set_current_user( self::$admin_user_id );
+		$other_admin_user_id = self::factory()->user->create( array( 'role' => 'administrator' ) );
+
+		$uuid = wp_generate_uuid4();
+		$manager = new WP_Customize_Manager( array(
+			'changeset_uuid' => $uuid,
+		) );
+		$wp_customize = $manager;
+		do_action( 'customize_register', $manager );
+		$manager->add_setting( 'scratchpad', array(
+			'type' => 'option',
+			'capability' => 'exist',
+		) );
+
+		// Create initial set of
+		$r = $manager->save_changeset_post( array(
+			'status' => 'auto-draft',
+			'data' => array(
+				'blogname' => array(
+					'value' => 'Admin 1 Title',
+				),
+				'scratchpad' => array(
+					'value' => 'Admin 1 Scratch',
+				),
+				'background_color' => array(
+					'value' => '#000000',
+				),
+			),
+		) );
+		$this->assertInternalType( 'array', $r );
+		$this->assertEquals(
+			array_fill_keys( array( 'blogname', 'scratchpad', 'background_color' ), true ),
+			$r['setting_validities']
+		);
+		$post_id = $manager->find_changeset_post_id( $uuid );
+		$data = json_decode( get_post( $post_id )->post_content, true );
+		$this->assertEquals( self::$admin_user_id, $data['blogname']['user_id'] );
+		$this->assertEquals( self::$admin_user_id, $data['scratchpad']['user_id'] );
+		$this->assertEquals( self::$admin_user_id, $data[ $this->manager->get_stylesheet() . '::background_color' ]['user_id'] );
+
+		// Attempt to save just one setting under a different user.
+		wp_set_current_user( $other_admin_user_id );
+		$r = $manager->save_changeset_post( array(
+			'status' => 'auto-draft',
+			'data' => array(
+				'blogname' => array(
+					'value' => 'Admin 2 Title',
+				),
+				'background_color' => array(
+					'value' => '#FFFFFF',
+				),
+			),
+		) );
+		$this->assertInternalType( 'array', $r );
+		$this->assertEquals(
+			array_fill_keys( array( 'blogname', 'background_color' ), true ),
+			$r['setting_validities']
+		);
+		$data = json_decode( get_post( $post_id )->post_content, true );
+		$this->assertEquals( 'Admin 2 Title', $data['blogname']['value'] );
+		$this->assertEquals( $other_admin_user_id, $data['blogname']['user_id'] );
+		$this->assertEquals( 'Admin 1 Scratch', $data['scratchpad']['value'] );
+		$this->assertEquals( self::$admin_user_id, $data['scratchpad']['user_id'] );
+		$this->assertEquals( '#FFFFFF', $data[ $this->manager->get_stylesheet() . '::background_color' ]['value'] );
+		$this->assertEquals( $other_admin_user_id, $data[ $this->manager->get_stylesheet() . '::background_color' ]['user_id'] );
+
+		// Attempt to save now as under-privileged user.
+		$r = $manager->save_changeset_post( array(
+			'status' => 'auto-draft',
+			'data' => array(
+				'scratchpad' => array(
+					'value' => 'Subscriber Scratch',
+				),
+			),
+			'user_id' => self::$subscriber_user_id,
+		) );
+		$this->assertInternalType( 'array', $r );
+		$this->assertEquals(
+			array_fill_keys( array( 'scratchpad' ), true ),
+			$r['setting_validities']
+		);
+		$data = json_decode( get_post( $post_id )->post_content, true );
+		$this->assertEquals( $other_admin_user_id, $data['blogname']['user_id'] );
+		$this->assertEquals( self::$subscriber_user_id, $data['scratchpad']['user_id'] );
+		$this->assertEquals( $other_admin_user_id, $data[ $this->manager->get_stylesheet() . '::background_color' ]['user_id'] );
+
+		// Manually update the changeset so that the user_id context is not included.
+		$data = json_decode( get_post( $post_id )->post_content, true );
+		$data['blogdescription']['value'] = 'Programmatically-supplied Tagline';
+		wp_update_post( wp_slash( array( 'ID' => $post_id, 'post_content' => wp_json_encode( $data ) ) ) );
+
+		// Ensure the modifying user set as the current user when each is saved, simulating WP Cron envronment.
+		wp_set_current_user( 0 );
+		$save_counts = array();
+		foreach ( array_keys( $data ) as $setting_id ) {
+			$setting_id = preg_replace( '/^.+::/', '', $setting_id );
+			$save_counts[ $setting_id ] = did_action( sprintf( 'customize_save_%s', $setting_id ) );
+		}
+		$this->filtered_setting_current_user_ids = array();
+		foreach ( $manager->settings() as $setting ) {
+			add_filter( sprintf( 'customize_sanitize_%s', $setting->id ), array( $this, 'filter_customize_setting_to_log_current_user' ), 10, 2 );
+		}
+		wp_update_post( array( 'ID' => $post_id, 'post_status' => 'publish' ) );
+		foreach ( array_keys( $data ) as $setting_id ) {
+			$setting_id = preg_replace( '/^.+::/', '', $setting_id );
+			$this->assertEquals( $save_counts[ $setting_id ] + 1, did_action( sprintf( 'customize_save_%s', $setting_id ) ), $setting_id );
+		}
+		$this->assertEqualSets( array( 'blogname', 'blogdescription', 'background_color', 'scratchpad' ), array_keys( $this->filtered_setting_current_user_ids ) );
+		$this->assertEquals( $other_admin_user_id, $this->filtered_setting_current_user_ids['blogname'] );
+		$this->assertEquals( 0, $this->filtered_setting_current_user_ids['blogdescription'] );
+		$this->assertEquals( self::$subscriber_user_id, $this->filtered_setting_current_user_ids['scratchpad'] );
+		$this->assertEquals( $other_admin_user_id, $this->filtered_setting_current_user_ids['background_color'] );
+		$this->assertEquals( 'Subscriber Scratch', get_option( 'scratchpad' ) );
+	}
+
+	/**
+	 * Test writing changesets and publishing with users who can unfiltered_html and those who cannot.
+	 *
+	 * @covers WP_Customize_Manager::save_changeset_post()
+	 */
+	function test_save_changeset_post_with_varying_unfiltered_html_cap() {
+		global $wp_customize;
+		grant_super_admin( self::$admin_user_id );
+		$this->assertTrue( user_can( self::$admin_user_id, 'unfiltered_html' ) );
+		$this->assertFalse( user_can( self::$subscriber_user_id, 'unfiltered_html' ) );
+		wp_set_current_user( 0 );
+		$setting_args = array(
+			'type' => 'option',
+			'capability' => 'exist',
+			'sanitize_callback' => array( $this, 'filter_sanitize_scratchpad' ),
+		);
+
+		// Attempt scratchpad with user who has unfiltered_html.
+		$wp_customize = new WP_Customize_Manager();
+		$wp_customize->add_setting( 'scratchpad', $setting_args );
+		$wp_customize->set_post_value( 'scratchpad', 'Unfiltered<script>evil</script>' );
+		$wp_customize->save_changeset_post( array(
+			'status' => 'auto-draft',
+			'user_id' => self::$admin_user_id,
+		) );
+		$wp_customize->save_changeset_post( array( 'status' => 'publish' ) );
+		$this->assertEquals( 'Unfiltered<script>evil</script>', get_option( 'scratchpad' ) );
+
+		// Attempt scratchpad with user who doesn't have unfiltered_html.
+		$wp_customize = new WP_Customize_Manager();
+		$wp_customize->add_setting( 'scratchpad', $setting_args );
+		$wp_customize->set_post_value( 'scratchpad', 'Unfiltered<script>evil</script>' );
+		$wp_customize->save_changeset_post( array(
+			'status' => 'auto-draft',
+			'user_id' => self::$subscriber_user_id,
+		) );
+		$wp_customize->save_changeset_post( array( 'status' => 'publish' ) );
+		$this->assertEquals( 'Unfilteredevil', get_option( 'scratchpad' ) );
+	}
+
+	/**
+	 * Sanitize scratchpad as if it is post_content so kses filters apply.
+	 *
+	 * @param string $value Value.
+	 * @return string Value.
+	 */
+	function filter_sanitize_scratchpad( $value ) {
+		return apply_filters( 'content_save_pre', $value );
+	}
+
+	/**
+	 * Current user when settings are filtered.
+	 *
+	 * @var array
+	 */
+	protected $filtered_setting_current_user_ids = array();
+
+	/**
+	 * Filter setting to capture the current user when the filter applies.
+	 *
+	 * @param mixed                $value   Setting value.
+	 * @param WP_Customize_Setting $setting Setting.
+	 *
+	 * @return mixed Value.
+	 */
+	function filter_customize_setting_to_log_current_user( $value, $setting ) {
+		$this->filtered_setting_current_user_ids[ $setting->id ] = get_current_user_id();
+		return $value;
+	}
+
+	/**
 	 * Test WP_Customize_Manager::is_cross_domain().
 	 *
 	 * @ticket 30937
