diff --git a/src/wp-includes/rest-api.php b/src/wp-includes/rest-api.php
index c5093cc33b..070cefe8d8 100644
--- a/src/wp-includes/rest-api.php
+++ b/src/wp-includes/rest-api.php
@@ -1561,3 +1561,25 @@ function rest_preload_api_request( $memo, $path ) {
 
 	return $memo;
 }
+
+/**
+ * Parses the "_embed" parameter into the list of resources to embed.
+ *
+ * @since 5.4.0
+ *
+ * @param string|array $embed Raw "_embed" parameter value.
+ * @return true|string[] Either true to embed all embeds, or a list of relations to embed.
+ */
+function rest_parse_embed_param( $embed ) {
+	if ( ! $embed || 'true' === $embed || '1' === $embed ) {
+		return true;
+	}
+
+	$rels = wp_parse_list( $embed );
+
+	if ( ! $rels ) {
+		return true;
+	}
+
+	return $rels;
+}
diff --git a/src/wp-includes/rest-api/class-wp-rest-server.php b/src/wp-includes/rest-api/class-wp-rest-server.php
index e07e214d97..a7a8dfa613 100644
--- a/src/wp-includes/rest-api/class-wp-rest-server.php
+++ b/src/wp-includes/rest-api/class-wp-rest-server.php
@@ -386,7 +386,8 @@ class WP_REST_Server {
 			}
 
 			// Embed links inside the request.
-			$result = $this->response_to_data( $result, isset( $_GET['_embed'] ) );
+			$embed  = isset( $_GET['_embed'] ) ? rest_parse_embed_param( $_GET['_embed'] ) : false;
+			$result = $this->response_to_data( $result, $embed );
 
 			/**
 			 * Filters the API response.
@@ -431,9 +432,10 @@ class WP_REST_Server {
 	 * Converts a response to data to send.
 	 *
 	 * @since 4.4.0
+	 * @since 5.4.0 The $embed parameter can now contain a list of link relations to include.
 	 *
 	 * @param WP_REST_Response $response Response object.
-	 * @param bool             $embed    Whether links should be embedded.
+	 * @param bool|string[]    $embed    Whether to embed all links, a filtered list of link relations, or no links.
 	 * @return array {
 	 *     Data with sub-requests embedded.
 	 *
@@ -452,9 +454,11 @@ class WP_REST_Server {
 		if ( $embed ) {
 			// Determine if this is a numeric array.
 			if ( wp_is_numeric_array( $data ) ) {
-				$data = array_map( array( $this, 'embed_links' ), $data );
+				foreach ( $data as $key => $link ) {
+					$data[ $key ] = $this->embed_links( $data, $embed );
+				}
 			} else {
-				$data = $this->embed_links( $data );
+				$data = $this->embed_links( $data, $embed );
 			}
 		}
 
@@ -548,8 +552,10 @@ class WP_REST_Server {
 	 * Embeds the links from the data into the request.
 	 *
 	 * @since 4.4.0
+	 * @since 5.4.0 The $embed parameter can now contain a list of link relations to include.
 	 *
-	 * @param array $data Data from the request.
+	 * @param array         $data  Data from the request.
+	 * @param bool|string[] $embed Whether to embed all links or a filtered list of link relations.
 	 * @return array {
 	 *     Data with sub-requests embedded.
 	 *
@@ -557,7 +563,7 @@ class WP_REST_Server {
 	 *     @type array [$_embedded] Embeddeds.
 	 * }
 	 */
-	protected function embed_links( $data ) {
+	protected function embed_links( $data, $embed = true ) {
 		if ( empty( $data['_links'] ) ) {
 			return $data;
 		}
@@ -565,6 +571,11 @@ class WP_REST_Server {
 		$embedded = array();
 
 		foreach ( $data['_links'] as $rel => $links ) {
+			// If a list of relations was specified, and the link relation is not in the whitelist, don't process the link.
+			if ( is_array( $embed ) && ! in_array( $rel, $embed, true ) ) {
+				continue;
+			}
+
 			$embeds = array();
 
 			foreach ( $links as $item ) {
diff --git a/tests/phpunit/tests/rest-api.php b/tests/phpunit/tests/rest-api.php
index 1efb81a348..15d0852627 100644
--- a/tests/phpunit/tests/rest-api.php
+++ b/tests/phpunit/tests/rest-api.php
@@ -906,4 +906,31 @@ class Tests_REST_API extends WP_UnitTestCase {
 		$this->assertEquals( '/wp/v2/posts', $request->get_route() );
 		$this->assertEquals( 'GET', $request->get_method() );
 	}
+
+	/**
+	 * @dataProvider _dp_rest_parse_embed_param
+	 */
+	public function test_rest_parse_embed_param( $expected, $embed ) {
+		$this->assertEquals( $expected, rest_parse_embed_param( $embed ) );
+	}
+
+	public function _dp_rest_parse_embed_param() {
+		return array(
+			array( true, '' ),
+			array( true, null ),
+			array( true, '1' ),
+			array( true, 'true' ),
+			array( true, array() ),
+			array( array( 'author' ), 'author' ),
+			array( array( 'author', 'replies' ), 'author,replies' ),
+			array( array( 'author', 'replies' ), 'author,replies ' ),
+			array( array( 'wp:term' ), 'wp:term' ),
+			array( array( 'wp:term', 'wp:attachment' ), 'wp:term,wp:attachment' ),
+			array( array( 'author' ), array( 'author' ) ),
+			array( array( 'author', 'replies' ), array( 'author', 'replies' ) ),
+			array( array( 'https://api.w.org/term' ), 'https://api.w.org/term' ),
+			array( array( 'https://api.w.org/term', 'https://api.w.org/attachment' ), 'https://api.w.org/term,https://api.w.org/attachment' ),
+			array( array( 'https://api.w.org/term', 'https://api.w.org/attachment' ), array( 'https://api.w.org/term', 'https://api.w.org/attachment' ) ),
+		);
+	}
 }
diff --git a/tests/phpunit/tests/rest-api/rest-server.php b/tests/phpunit/tests/rest-api/rest-server.php
index 74d957cbe5..ae29c43c5a 100644
--- a/tests/phpunit/tests/rest-api/rest-server.php
+++ b/tests/phpunit/tests/rest-api/rest-server.php
@@ -743,6 +743,66 @@ class Tests_REST_Server extends WP_Test_REST_TestCase {
 		$this->assertEquals( $self_not_filtered, $data['_links']['self'][0] );
 	}
 
+	/**
+	 * @dataProvider _dp_response_to_data_embedding
+	 */
+	public function test_response_to_data_embedding( $expected, $embed ) {
+		$response = new WP_REST_Response();
+		$response->add_link( 'author', rest_url( '404' ), array( 'embeddable' => true ) );
+		$response->add_link( 'https://api.w.org/term', rest_url( '404' ), array( 'embeddable' => true ) );
+		$response->add_link( 'https://wordpress.org', rest_url( '404' ), array( 'embeddable' => true ) );
+		$response->add_link( 'no-embed', rest_url( '404' ) );
+
+		$data = rest_get_server()->response_to_data( $response, $embed );
+
+		if ( false === $expected ) {
+			$this->assertArrayNotHasKey( '_embedded', $data );
+		} else {
+			$this->assertEqualSets( $expected, array_keys( $data['_embedded'] ) );
+		}
+	}
+
+	public function _dp_response_to_data_embedding() {
+		return array(
+			array(
+				array( 'author', 'wp:term', 'https://wordpress.org' ),
+				true,
+			),
+			array(
+				array( 'author', 'wp:term', 'https://wordpress.org' ),
+				array( 'author', 'wp:term', 'https://wordpress.org' ),
+			),
+			array(
+				array( 'author' ),
+				array( 'author' ),
+			),
+			array(
+				array( 'wp:term' ),
+				array( 'wp:term' ),
+			),
+			array(
+				array( 'https://wordpress.org' ),
+				array( 'https://wordpress.org' ),
+			),
+			array(
+				array( 'author', 'wp:term' ),
+				array( 'author', 'wp:term' ),
+			),
+			array(
+				false,
+				false,
+			),
+			array(
+				false,
+				array( 'no-embed' ),
+			),
+			array(
+				array( 'author' ),
+				array( 'author', 'no-embed' ),
+			),
+		);
+	}
+
 	public function test_get_index() {
 		$server = new WP_REST_Server();
 		$server->register_route(
