Index: src/wp-includes/option.php
===================================================================
--- src/wp-includes/option.php	(revision 46814)
+++ src/wp-includes/option.php	(working copy)
@@ -1907,7 +1907,8 @@
 		'blogname',
 		array(
 			'show_in_rest' => array(
-				'name' => 'title',
+				'name'   => 'title',
+				'public' => true,
 			),
 			'type'         => 'string',
 			'description'  => __( 'Site title.' ),
@@ -1919,7 +1920,8 @@
 		'blogdescription',
 		array(
 			'show_in_rest' => array(
-				'name' => 'description',
+				'name'   => 'description',
+				'public' => true,
 			),
 			'type'         => 'string',
 			'description'  => __( 'Site tagline.' ),
@@ -2091,6 +2093,7 @@
  *
  * @since 2.7.0
  * @since 4.7.0 `$args` can be passed to set flags on the setting, similar to `register_meta()`.
+ * @since 5.4.0 Added the `$public` parameter to the `$show_in_rest` member of `$args`.
  *
  * @global array $new_whitelist_options
  * @global array $wp_registered_settings
@@ -2101,12 +2104,19 @@
  * @param array  $args {
  *     Data used to describe the setting when registered.
  *
- *     @type string   $type              The type of data associated with this setting.
- *                                       Valid values are 'string', 'boolean', 'integer', and 'number'.
- *     @type string   $description       A description of the data attached to this setting.
- *     @type callable $sanitize_callback A callback function that sanitizes the option's value.
- *     @type bool     $show_in_rest      Whether data associated with this setting should be included in the REST API.
- *     @type mixed    $default           Default value when calling `get_option()`.
+ *     @type string     $type              The type of data associated with this setting.
+ *                                         Valid values are 'string', 'boolean', 'integer', and 'number'.
+ *     @type string     $description       A description of the data attached to this setting.
+ *     @type callable   $sanitize_callback A callback function that sanitizes the option's value.
+ *     @type bool|array $show_in_rest {
+ *         Whether data associated with this setting should be included in the REST API. Optionally passing
+ *         an array will include the setting in the REST API, with additional optional configuration.
+ *
+ *         @type string $name   The name to display this setting in the REST API as. Default `$option_name`.
+ *         @type array  $schema Additional schema data to be included for this option.
+ *         @type bool   $public Whether this option can be shown to all users. Default false.
+ *     }
+ *     @type mixed      $default           Default value when calling `get_option()`.
  * }
  */
 function register_setting( $option_group, $option_name, $args = array() ) {
Index: src/wp-includes/rest-api/endpoints/class-wp-rest-settings-controller.php
===================================================================
--- src/wp-includes/rest-api/endpoints/class-wp-rest-settings-controller.php	(revision 46814)
+++ src/wp-includes/rest-api/endpoints/class-wp-rest-settings-controller.php	(working copy)
@@ -40,16 +40,17 @@
 			'/' . $this->rest_base,
 			array(
 				array(
-					'methods'             => WP_REST_Server::READABLE,
-					'callback'            => array( $this, 'get_item' ),
-					'args'                => array(),
-					'permission_callback' => array( $this, 'get_item_permissions_check' ),
+					'methods'  => WP_REST_Server::READABLE,
+					'callback' => array( $this, 'get_item' ),
+					'args'                => array(
+						'context' => $this->get_context_param( array( 'default' => 'view' ) ),
+					),
 				),
 				array(
 					'methods'             => WP_REST_Server::EDITABLE,
 					'callback'            => array( $this, 'update_item' ),
 					'args'                => $this->get_endpoint_args_for_item_schema( WP_REST_Server::EDITABLE ),
-					'permission_callback' => array( $this, 'get_item_permissions_check' ),
+					'permission_callback' => array( $this, 'update_item_permissions_check' ),
 				),
 				'schema' => array( $this, 'get_public_item_schema' ),
 			)
@@ -58,14 +59,14 @@
 	}
 
 	/**
-	 * Checks if a given request has access to read and manage settings.
+	 * Checks if a given request has access to manage settings.
 	 *
-	 * @since 4.7.0
+	 * @since 5.4.0
 	 *
 	 * @param WP_REST_Request $request Full details about the request.
-	 * @return bool True if the request has read access for the item, otherwise false.
+	 * @return bool True if the request has write access for the item, otherwise false.
 	 */
-	public function get_item_permissions_check( $request ) {
+	public function update_item_permissions_check( $request ) {
 		return current_user_can( 'manage_options' );
 	}
 
@@ -100,7 +101,8 @@
 
 			if ( is_null( $response[ $name ] ) ) {
 				// Default to a null value as "null" in the response means "not set".
-				$response[ $name ] = get_option( $args['option_name'], $args['schema']['default'] );
+				$filter = ( 'edit' === $request['context'] ) ? 'raw' : 'display';
+				$response[ $name ] = get_bloginfo( $args['name'], $filter );
 			}
 
 			/*
@@ -212,9 +214,11 @@
 	 *
 	 * @since 4.7.0
 	 *
+	 * @param bool $return_non_public Optional. Returns options that are set with `show_in_rest`,
+	 *                                but aren't marked public. Default false.
 	 * @return array Array of registered options.
 	 */
-	protected function get_registered_options() {
+	protected function get_registered_options( $return_non_public = false ) {
 		$rest_options = array();
 
 		foreach ( get_registered_settings() as $name => $args ) {
@@ -228,6 +232,11 @@
 				$rest_args = $args['show_in_rest'];
 			}
 
+			// Users without manage_options can only see settings marked public.
+			if ( ! $return_non_public && ! current_user_can( 'manage_options' ) && empty( $rest_args['public'] ) ) {
+				continue;
+			}
+
 			$defaults = array(
 				'name'   => ! empty( $rest_args['name'] ) ? $rest_args['name'] : $name,
 				'schema' => array(),
@@ -277,7 +286,7 @@
 			return $this->add_additional_fields_schema( $this->schema );
 		}
 
-		$options = $this->get_registered_options();
+		$options = $this->get_registered_options( true );
 
 		$schema = array(
 			'$schema'    => 'http://json-schema.org/draft-04/schema#',
Index: tests/phpunit/tests/rest-api/rest-settings-controller.php
===================================================================
--- tests/phpunit/tests/rest-api/rest-settings-controller.php	(revision 46814)
+++ tests/phpunit/tests/rest-api/rest-settings-controller.php	(working copy)
@@ -54,17 +54,41 @@
 	public function test_context_param() {
 	}
 
-	public function test_get_item_is_not_public_not_authenticated() {
+	public function test_get_item_shows_public_items_not_authenticated() {
 		$request  = new WP_REST_Request( 'GET', '/wp/v2/settings' );
 		$response = rest_get_server()->dispatch( $request );
-		$this->assertEquals( 401, $response->get_status() );
+		$data     = $response->get_data();
+		$actual   = array_keys( $data );
+
+		$expected = array(
+			'title',
+			'description',
+		);
+
+		sort( $expected );
+		sort( $actual );
+
+		$this->assertEquals( 200, $response->get_status() );
+		$this->assertEquals( $expected, $actual );
 	}
 
-	public function test_get_item_is_not_public_no_permission() {
+	public function test_get_item_shows_public_items_no_permission() {
 		wp_set_current_user( self::$author );
 		$request  = new WP_REST_Request( 'GET', '/wp/v2/settings' );
 		$response = rest_get_server()->dispatch( $request );
-		$this->assertEquals( 403, $response->get_status() );
+		$data     = $response->get_data();
+		$actual   = array_keys( $data );
+
+		$expected = array(
+			'title',
+			'description',
+		);
+
+		sort( $expected );
+		sort( $actual );
+
+		$this->assertEquals( 200, $response->get_status() );
+		$this->assertEquals( $expected, $actual );
 	}
 
 	public function test_get_items() {
@@ -377,6 +401,21 @@
 		$this->assertEquals( get_option( 'blogname' ), $data['title'] );
 	}
 
+	public function test_update_item_fails_not_authenticated() {
+		$request = new WP_REST_Request( 'PUT', '/wp/v2/settings' );
+		$request->set_param( 'title', 'The new title!' );
+		$response = rest_get_server()->dispatch( $request );
+		$this->assertEquals( 401, $response->get_status() );
+	}
+
+	public function test_update_item_fails_no_permission() {
+		wp_set_current_user( self::$author );
+		$request = new WP_REST_Request( 'PUT', '/wp/v2/settings' );
+		$request->set_param( 'title', 'The new title!' );
+		$response = rest_get_server()->dispatch( $request );
+		$this->assertEquals( 403, $response->get_status() );
+	}
+
 	public function update_setting_custom_callback( $result, $name, $value, $args ) {
 		if ( 'title' === $name && 'The new title!' === $value ) {
 			// Do not allow changing the title in this case
