CVE-2020-28039: Insufficient Information (NVD-CWE-noinfo)
value: is_protected_meta in wp-includes/meta.php in WordPress before 5.5.2 allows arbitrary file deletion because it does not properly determine whether a meta key is considered protected.
CWE description: There is insufficient information about the issue to classify it; details are unkown or unspecified.
CVSS v3 base score: 9.1
Published: 2020-11-02T21:15Z
Repository: https://github.com/WordPress/wordpress-develop
Language: PHP
Files changed: src/wp-includes/meta.php, tests/phpunit/tests/meta/isProtectedMeta.php
Commit date: 2020-10-29 18:05:21 +0000
Commit message: Meta: Sanitize meta key before checking protection status.

Props zieladam, peterwilsoncc, xknown, whyisjake.
Merges [49377,49381] to trunk.

git-svn-id: https://develop.svn.wordpress.org/trunk@49387 602fd350-edb4-49c9-b593-d223f7449a82

Diff: diff --git a/src/wp-includes/meta.php b/src/wp-includes/meta.php
index e1c1f92e48..fe144fcda6 100644
--- a/src/wp-includes/meta.php
+++ b/src/wp-includes/meta.php
@@ -1157,11 +1157,12 @@ function _get_meta_table( $type ) {
  * @param string $meta_type Optional. Type of object metadata is for. Accepts 'post', 'comment', 'term', 'user',
  *                          or any other object type with an associated meta table. Default empty.
  * @return bool Whether the meta key is considered protected.
  */
 function is_protected_meta( $meta_key, $meta_type = '' ) {
-	$protected = ( '_' === $meta_key[0] );
+	$sanitized_key = preg_replace( "/[^\x20-\x7E\p{L}]/", '', $meta_key );
+	$protected     = strlen( $sanitized_key ) > 0 && ( '_' === $sanitized_key[0] );
 
 	/**
 	 * Filters whether a meta key is considered protected.
 	 *
 	 * @since 3.2.0
diff --git a/tests/phpunit/tests/meta/isProtectedMeta.php b/tests/phpunit/tests/meta/isProtectedMeta.php
new file mode 100644
index 0000000000..c204d381f5
--- /dev/null
+++ b/tests/phpunit/tests/meta/isProtectedMeta.php
@@ -0,0 +1,55 @@
+<?php
+
+/**
+ * @group meta
+ * @covers ::is_protected_meta
+ */
+class Tests_Meta_isProtectedMeta extends WP_UnitTestCase {
+
+	/**
+	 * @dataProvider protected_data
+	 */
+	public function test_protected( $key ) {
+		$this->assertTrue( is_protected_meta( $key ) );
+	}
+
+	public function protected_data() {
+		$protected_keys = array(
+			array( '_wp_attachment' ),
+		);
+		for ( $i = 0, $max = 31; $i < $max; $i ++ ) {
+			$protected_keys[] = array( chr( $i ) . '_wp_attachment' );
+		}
+		for ( $i = 127, $max = 159; $i <= $max; $i ++ ) {
+			$protected_keys[] = array( chr( $i ) . '_wp_attachment' );
+		}
+		$protected_keys[] = array( chr( 95 ) . '_wp_attachment' );
+
+		return $protected_keys;
+	}
+
+	/**
+	 * @dataProvider unprotected_data
+	 */
+	public function test_unprotected( $key ) {
+		$this->assertFalse( is_protected_meta( $key ) );
+	}
+
+	public function unprotected_data() {
+		$unprotected_keys = array(
+			array( 'singleword' ),
+			array( 'two_words' ),
+			array( 'ąŌ_not_so_protected_meta' ),
+		);
+
+		for ( $i = 32, $max = 94; $i <= $max; $i ++ ) {
+			$unprotected_keys[] = array( chr( $i ) . '_wp_attachment' );
+		}
+		for ( $i = 96, $max = 126; $i <= $max; $i ++ ) {
+			$unprotected_keys[] = array( chr( $i ) . '_wp_attachment' );
+		}
+
+		return $unprotected_keys;
+	}
+
+}

Vulnerable code: 	$protected = ( '_' === $meta_key[0] );
Fixed code: 	$sanitized_key = preg_replace( "/[^\x20-\x7E\p{L}]/", '', $meta_key );
	$protected     = strlen( $sanitized_key ) > 0 && ( '_' === $sanitized_key[0] );
<?php

/**
 * @group meta
 * @covers ::is_protected_meta
 */
class Tests_Meta_isProtectedMeta extends WP_UnitTestCase {

	/**
	 * @dataProvider protected_data
	 */
	public function test_protected( $key ) {
		$this->assertTrue( is_protected_meta( $key ) );
	}

	public function protected_data() {
		$protected_keys = array(
			array( '_wp_attachment' ),
		);
		for ( $i = 0, $max = 31; $i < $max; $i ++ ) {
			$protected_keys[] = array( chr( $i ) . '_wp_attachment' );
		}
		for ( $i = 127, $max = 159; $i <= $max; $i ++ ) {
			$protected_keys[] = array( chr( $i ) . '_wp_attachment' );
		}
		$protected_keys[] = array( chr( 95 ) . '_wp_attachment' );

		return $protected_keys;
	}

	/**
	 * @dataProvider unprotected_data
	 */
	public function test_unprotected( $key ) {
		$this->assertFalse( is_protected_meta( $key ) );
	}

	public function unprotected_data() {
		$unprotected_keys = array(
			array( 'singleword' ),
			array( 'two_words' ),
			array( 'ąŌ_not_so_protected_meta' ),
		);

		for ( $i = 32, $max = 94; $i <= $max; $i ++ ) {
			$unprotected_keys[] = array( chr( $i ) . '_wp_attachment' );
		}
		for ( $i = 96, $max = 126; $i <= $max; $i ++ ) {
			$unprotected_keys[] = array( chr( $i ) . '_wp_attachment' );
		}

		return $unprotected_keys;
	}

}