Index: src/wp-includes/wp-db.php
===================================================================
--- src/wp-includes/wp-db.php	(revision 41547)
+++ src/wp-includes/wp-db.php	(working copy)
@@ -1248,16 +1248,65 @@
 			}
 		}
 
-		$query = str_replace( "'%s'", '%s', $query ); // in case someone mistakenly already singlequoted it
-		$query = str_replace( '"%s"', '%s', $query ); // doublequote unquoting
-		$query = preg_replace( '|(?<!%)%f|' , '%F', $query ); // Force floats to be locale unaware
-		$query = preg_replace( '|(?<!%)%s|', "'%s'", $query ); // quote the strings, avoiding escaped strings like %%s
-		$query = preg_replace( '/%(?:%|$|([^dsF]))/', '%%\\1', $query ); // escape any unescaped percents 
+		$query = preg_replace( '#[\'"]%(\d+\$)?s[\'"]#', '%$1s', $query ); // Remove mistakenly single/doublequoted placeholders
+
+		// Add single quotes around placeholders, but only for an odd number of leading % signs.
+		// Force floats to be locale unaware, but only for an odd number of leading % signs.
+		$query = preg_replace_callback( '#(%+)(\d+\$)?([fs])#', array( $this, '_normalize_prepare_placehoders' ), $query );
+
 		array_walk( $args, array( $this, 'escape_by_ref' ) );
 		return @vsprintf( $query, $args );
 	}
 
 	/**
+	 * Make sure the %f and %s placeholders are prepared correctly.
+	 *
+	 * Apply single quotes around an %s placeholder.
+	 * Make the %f placeholder locale-unaware.
+	 *
+	 * Make sure we only do this around the actual placeholder
+	 *  disregarding any of the leading % signs where available.
+	 *
+	 * @since 4.8.2
+	 * @see wpdb::prepare
+	 *
+	 * @param array $matches The matches from within the replace callback above.
+	 * @return string
+	 */
+	public function _normalize_prepare_placehoders( $matches ) {
+		// Just to make sure we received the needed stuff.
+		if ( ! is_array( $matches ) || empty( $matches ) ) {
+			return '';
+		}
+
+		$placeholder = $matches[0];
+
+		// Return as is, not sure what to normalize here.
+		if ( count( $matches ) < 3 ) {
+			return $placeholder;
+		}
+
+		// The number of leading % signs.
+		$npercent = strlen( $matches[1] );
+
+		// An even number of leading % signs renders the placeholder inert.
+		if ( $npercent % 2 == 0 ) {
+			return $placeholder;
+		}
+
+		switch ( $matches[3] ) {
+			case 'f':
+				// Replace just the needed part.
+				return str_replace( 'f', 'F', $placeholder );
+			case 's':
+				// Quote just the needed part.
+				return substr( $placeholder, 0, $npercent - 1 ) . "'" . substr( $placeholder, $npercent - 1 ) . "'";
+			default:
+				return $placeholder;
+		}
+	}
+
+	/**
 	 * First half of escaping for LIKE special characters % and _ before preparing for MySQL.
 	 *
 	 * Use this only before wpdb::prepare() or esc_sql().  Reversing the order is very bad for security.
Index: tests/phpunit/tests/db.php
===================================================================
--- tests/phpunit/tests/db.php	(revision 41547)
+++ tests/phpunit/tests/db.php	(working copy)
@@ -1118,12 +1118,72 @@
 	}
 
 	/**
-	 *
+	 * @ticket 41925
 	 */
-	function test_prepare_with_unescaped_percents() {
+	function test_prepare_with_numbered_parameters_and_leading_escapes() {
 		global $wpdb;
 
-		$sql = $wpdb->prepare( '%d %1$d %%% %', 1 );
-		$this->assertEquals( '1 %1$d %% %', $sql );
+		$sql = $wpdb->prepare( '%1$d %%% % %%1$d%% %%%1$d%%', 1 );
+		$this->assertEquals( '1 %% %1$d% %1%', $sql );
+
+		$sql = $wpdb->prepare( '%d %2$s', 1, 'hello' );
+		$this->assertEquals( "1 'hello'", $sql );
+
+		$sql = $wpdb->prepare( "'%s'", 'hello' );
+		$this->assertEquals( "'hello'", $sql );
+
+		$sql = $wpdb->prepare( '"%s"', 'hello' );
+		$this->assertEquals( "'hello'", $sql );
+
+		$sql = $wpdb->prepare( "%s '%1\$s'", 'hello' );
+		$this->assertEquals( "'hello' 'hello'", $sql );
+
+		$sql = $wpdb->prepare( "%s '%1\$s'", 'hello' );
+		$this->assertEquals( "'hello' 'hello'", $sql );
+
+		$sql = $wpdb->prepare( '%s "%1$s"', 'hello' );
+		$this->assertEquals( "'hello' 'hello'", $sql );
+
+		$sql = $wpdb->prepare( "%%s %%'%1\$s'", 'hello' );
+		$this->assertEquals( "%s %'hello'", $sql );
+
+		$sql = $wpdb->prepare( '%%s %%"%1$s"', 'hello' );
+		$this->assertEquals( "%s %'hello'", $sql );
+
+		$sql = $wpdb->prepare( '%%f %%"%1$f"', 3 );
+		$this->assertEquals( '%f %"3.000000"', $sql );
 	}
+
+	function test_placeholder_normalization() {
+		global $wpdb;
+
+		$prepare_pattern = '#(%+)(\d+\$)?([fs])#';
+
+		$sql = preg_replace_callback( $prepare_pattern, array( $wpdb, '_normalize_prepare_placehoders' ), '%s' );
+		$this->assertEquals( "'%s'", $sql );
+
+		$sql = preg_replace_callback( $prepare_pattern, array( $wpdb, '_normalize_prepare_placehoders' ), '%%s' );
+		$this->assertEquals( "%%s", $sql );
+
+		$sql = preg_replace_callback( $prepare_pattern, array( $wpdb, '_normalize_prepare_placehoders' ), '%%%s' );
+		$this->assertEquals( "%%'%s'", $sql );
+
+		$sql = preg_replace_callback( $prepare_pattern, array( $wpdb, '_normalize_prepare_placehoders' ), '%f' );
+		$this->assertEquals( '%F', $sql );
+
+		$sql = preg_replace_callback( $prepare_pattern, array( $wpdb, '_normalize_prepare_placehoders' ), '%%f' );
+		$this->assertEquals( '%%f', $sql );
+
+		$sql = preg_replace_callback( $prepare_pattern, array( $wpdb, '_normalize_prepare_placehoders' ), '%%%f' );
+		$this->assertEquals( '%%%F', $sql );
+
+		$sql = preg_replace_callback( $prepare_pattern, array( $wpdb, '_normalize_prepare_placehoders' ), '%%d' );
+		$this->assertEquals( '%%d', $sql );
+
+		$sql = preg_replace_callback( $prepare_pattern, array( $wpdb, '_normalize_prepare_placehoders' ), '%%%d' );
+		$this->assertEquals( '%%%d', $sql );
+
+		$sql = preg_replace_callback( $prepare_pattern, array( $wpdb, '_normalize_prepare_placehoders' ), 'nothing to see here' );
+		$this->assertEquals( 'nothing to see here', $sql );
+	}
 }
