Changeset 54734 for branches/6.1/src/wp-includes/class-wpdb.php
- Timestamp:
- 10/31/2022 08:43:56 PM (2 years ago)
- Location:
- branches/6.1
- Files:
-
- 2 edited
Legend:
- Unmodified
- Added
- Removed
-
branches/6.1
-
branches/6.1/src/wp-includes/class-wpdb.php
r54384 r54734 656 656 657 657 /** 658 * Backward compatibility, where wpdb::prepare() has not quoted formatted/argnum placeholders.659 *660 * Historically this could be used for table/field names, or for some string formatting, e.g.661 *662 * $wpdb->prepare( 'WHERE `%1s` = "%1s something %1s" OR %1$s = "%-10s"', 'field_1', 'a', 'b', 'c' );663 *664 * But it's risky, e.g. forgetting to add quotes, resulting in SQL Injection vulnerabilities:665 *666 * $wpdb->prepare( 'WHERE (id = %1s) OR (id = %2$s)', $_GET['id'], $_GET['id'] ); // ?id=id667 *668 * This feature is preserved while plugin authors update their code to use safer approaches:669 *670 * $wpdb->prepare( 'WHERE %1s = %s', $_GET['key'], $_GET['value'] );671 * $wpdb->prepare( 'WHERE %i = %s', $_GET['key'], $_GET['value'] );672 *673 * While changing to false will be fine for queries not using formatted/argnum placeholders,674 * any remaining cases are most likely going to result in SQL errors (good, in a way):675 *676 * $wpdb->prepare( 'WHERE %1s = "%-10s"', 'my_field', 'my_value' );677 * true = WHERE my_field = "my_value "678 * false = WHERE 'my_field' = "'my_value '"679 *680 * But there may be some queries that result in an SQL Injection vulnerability:681 *682 * $wpdb->prepare( 'WHERE id = %1s', $_GET['id'] ); // ?id=id683 *684 * So there may need to be a `_doing_it_wrong()` phase, after we know everyone can use685 * identifier placeholders (%i), but before this feature is disabled or removed.686 *687 * @since 6.1.0688 * @var bool689 */690 private $allow_unsafe_unquoted_parameters = true;691 692 /**693 658 * Whether to use mysqli over mysql. Default false. 694 659 * … … 1399 1364 1400 1365 /** 1401 * Escapes an identifier for a MySQL database, e.g. table/field names.1402 *1403 * @since 6.1.01404 *1405 * @param string $identifier Identifier to escape.1406 * @return string Escaped identifier.1407 */1408 public function escape_identifier( $identifier ) {1409 return '`' . $this->_escape_identifier_value( $identifier ) . '`';1410 }1411 1412 /**1413 * Escapes an identifier value without adding the surrounding quotes.1414 *1415 * - Permitted characters in quoted identifiers include the full Unicode1416 * Basic Multilingual Plane (BMP), except U+0000.1417 * - To quote the identifier itself, you need to double the character, e.g. `a``b`.1418 *1419 * @since 6.1.01420 * @access private1421 *1422 * @link https://dev.mysql.com/doc/refman/8.0/en/identifiers.html1423 *1424 * @param string $identifier Identifier to escape.1425 * @return string Escaped identifier.1426 */1427 private function _escape_identifier_value( $identifier ) {1428 return str_replace( '`', '``', $identifier );1429 }1430 1431 /**1432 1366 * Prepares a SQL query for safe execution. 1433 1367 * … … 1437 1371 * - %f (float) 1438 1372 * - %s (string) 1439 * - %i (identifier, e.g. table/field names)1440 1373 * 1441 1374 * All placeholders MUST be left unquoted in the query string. A corresponding argument … … 1470 1403 * by updating the function signature. The second parameter was changed 1471 1404 * from `$args` to `...$args`. 1472 * @since 6.1.0 Added `%i` for identifiers, e.g. table or field names.1473 * Check support via `wpdb::has_cap( 'identifier_placeholders' )`.1474 * This preserves compatibility with sprintf(), as the C version uses1475 * `%d` and `$i` as a signed integer, whereas PHP only supports `%d`.1476 1405 * 1477 1406 * @link https://www.php.net/sprintf Description of syntax. … … 1505 1434 } 1506 1435 1436 // If args were passed as an array (as in vsprintf), move them up. 1437 $passed_as_array = false; 1438 if ( isset( $args[0] ) && is_array( $args[0] ) && 1 === count( $args ) ) { 1439 $passed_as_array = true; 1440 $args = $args[0]; 1441 } 1442 1443 foreach ( $args as $arg ) { 1444 if ( ! is_scalar( $arg ) && ! is_null( $arg ) ) { 1445 wp_load_translations_early(); 1446 _doing_it_wrong( 1447 'wpdb::prepare', 1448 sprintf( 1449 /* translators: %s: Value type. */ 1450 __( 'Unsupported value type (%s).' ), 1451 gettype( $arg ) 1452 ), 1453 '4.8.2' 1454 ); 1455 } 1456 } 1457 1507 1458 /* 1508 1459 * Specify the formatting allowed in a placeholder. The following are allowed: … … 1525 1476 $query = str_replace( "'%s'", '%s', $query ); // Strip any existing single quotes. 1526 1477 $query = str_replace( '"%s"', '%s', $query ); // Strip any existing double quotes. 1527 1528 // Escape any unescaped percents (i.e. anything unrecognised). 1529 $query = preg_replace( "/%(?:%|$|(?!($allowed_format)?[sdfFi]))/", '%%\\1', $query ); 1530 1531 // Extract placeholders from the query. 1532 $split_query = preg_split( "/(^|[^%]|(?:%%)+)(%(?:$allowed_format)?[sdfFi])/", $query, -1, PREG_SPLIT_DELIM_CAPTURE ); 1533 1534 $split_query_count = count( $split_query ); 1535 /* 1536 * Split always returns with 1 value before the first placeholder (even with $query = "%s"), 1537 * then 3 additional values per placeholder. 1538 */ 1539 $placeholder_count = ( ( $split_query_count - 1 ) / 3 ); 1540 1541 // If args were passed as an array, as in vsprintf(), move them up. 1542 $passed_as_array = ( isset( $args[0] ) && is_array( $args[0] ) && 1 === count( $args ) ); 1543 if ( $passed_as_array ) { 1544 $args = $args[0]; 1545 } 1546 1547 $new_query = ''; 1548 $key = 2; // Keys 0 and 1 in $split_query contain values before the first placeholder. 1549 $arg_id = 0; 1550 $arg_identifiers = array(); 1551 $arg_strings = array(); 1552 1553 while ( $key < $split_query_count ) { 1554 $placeholder = $split_query[ $key ]; 1555 1556 $format = substr( $placeholder, 1, -1 ); 1557 $type = substr( $placeholder, -1 ); 1558 1559 if ( 'f' === $type ) { // Force floats to be locale-unaware. 1560 $type = 'F'; 1561 $placeholder = '%' . $format . $type; 1562 } 1563 1564 if ( 'i' === $type ) { 1565 $placeholder = '`%' . $format . 's`'; 1566 // Using a simple strpos() due to previous checking (e.g. $allowed_format). 1567 $argnum_pos = strpos( $format, '$' ); 1568 1569 if ( false !== $argnum_pos ) { 1570 // sprintf() argnum starts at 1, $arg_id from 0. 1571 $arg_identifiers[] = ( intval( substr( $format, 0, $argnum_pos ) ) - 1 ); 1572 } else { 1573 $arg_identifiers[] = $arg_id; 1574 } 1575 } elseif ( 'd' !== $type && 'F' !== $type ) { 1576 /* 1577 * i.e. ( 's' === $type ), where 'd' and 'F' keeps $placeholder unchanged, 1578 * and we ensure string escaping is used as a safe default (e.g. even if 'x'). 1579 */ 1580 $argnum_pos = strpos( $format, '$' ); 1581 1582 if ( false !== $argnum_pos ) { 1583 $arg_strings[] = ( intval( substr( $format, 0, $argnum_pos ) ) - 1 ); 1584 } 1585 1586 // Unquoted strings for backward compatibility (dangerous). 1587 if ( true !== $this->allow_unsafe_unquoted_parameters || '' === $format ) { 1588 $placeholder = "'%" . $format . "s'"; 1589 } 1590 } 1591 1592 // Glue (-2), any leading characters (-1), then the new $placeholder. 1593 $new_query .= $split_query[ $key - 2 ] . $split_query[ $key - 1 ] . $placeholder; 1594 1595 $key += 3; 1596 $arg_id++; 1597 } 1598 1599 // Replace $query; and add remaining $query characters, or index 0 if there were no placeholders. 1600 $query = $new_query . $split_query[ $key - 2 ]; 1601 1602 $dual_use = array_intersect( $arg_identifiers, $arg_strings ); 1603 1604 if ( count( $dual_use ) ) { 1605 wp_load_translations_early(); 1606 _doing_it_wrong( 1607 'wpdb::prepare', 1608 sprintf( 1609 /* translators: %s: A comma-separated list of arguments found to be a problem. */ 1610 __( 'Arguments (%s) cannot be used for both String and Identifier escaping.' ), 1611 implode( ', ', $dual_use ) 1612 ), 1613 '6.1.0' 1614 ); 1615 1616 return; 1617 } 1478 $query = preg_replace( '/(?<!%)%s/', "'%s'", $query ); // Quote the strings, avoiding escaped strings like %%s. 1479 1480 $query = preg_replace( "/(?<!%)(%($allowed_format)?f)/", '%\\2F', $query ); // Force floats to be locale-unaware. 1481 1482 $query = preg_replace( "/%(?:%|$|(?!($allowed_format)?[sdF]))/", '%%\\1', $query ); // Escape any unescaped percents. 1483 1484 // Count the number of valid placeholders in the query. 1485 $placeholders = preg_match_all( "/(^|[^%]|(%%)+)%($allowed_format)?[sdF]/", $query, $matches ); 1618 1486 1619 1487 $args_count = count( $args ); 1620 1488 1621 if ( $args_count !== $placeholder_count ) { 1622 if ( 1 === $placeholder_count && $passed_as_array ) { 1623 /* 1624 * If the passed query only expected one argument, 1625 * but the wrong number of arguments was sent as an array, bail. 1626 */ 1489 if ( $args_count !== $placeholders ) { 1490 if ( 1 === $placeholders && $passed_as_array ) { 1491 // If the passed query only expected one argument, but the wrong number of arguments were sent as an array, bail. 1627 1492 wp_load_translations_early(); 1628 1493 _doing_it_wrong( … … 1645 1510 /* translators: 1: Number of placeholders, 2: Number of arguments passed. */ 1646 1511 __( 'The query does not contain the correct number of placeholders (%1$d) for the number of arguments passed (%2$d).' ), 1647 $placeholder _count,1512 $placeholders, 1648 1513 $args_count 1649 1514 ), … … 1655 1520 * return an empty string to avoid a fatal error on PHP 8. 1656 1521 */ 1657 if ( $args_count < $placeholder_count ) { 1658 $max_numbered_placeholder = 0; 1659 1660 for ( $i = 2, $l = $split_query_count; $i < $l; $i += 3 ) { 1661 // Assume a leading number is for a numbered placeholder, e.g. '%3$s'. 1662 $argnum = intval( substr( $split_query[ $i ], 1 ) ); 1663 1664 if ( $max_numbered_placeholder < $argnum ) { 1665 $max_numbered_placeholder = $argnum; 1666 } 1667 } 1522 if ( $args_count < $placeholders ) { 1523 $max_numbered_placeholder = ! empty( $matches[3] ) ? max( array_map( 'intval', $matches[3] ) ) : 0; 1668 1524 1669 1525 if ( ! $max_numbered_placeholder || $args_count < $max_numbered_placeholder ) { … … 1674 1530 } 1675 1531 1676 $args_escaped = array(); 1677 1678 foreach ( $args as $i => $value ) { 1679 if ( in_array( $i, $arg_identifiers, true ) ) { 1680 $args_escaped[] = $this->_escape_identifier_value( $value ); 1681 } elseif ( is_int( $value ) || is_float( $value ) ) { 1682 $args_escaped[] = $value; 1683 } else { 1684 if ( ! is_scalar( $value ) && ! is_null( $value ) ) { 1685 wp_load_translations_early(); 1686 _doing_it_wrong( 1687 'wpdb::prepare', 1688 sprintf( 1689 /* translators: %s: Value type. */ 1690 __( 'Unsupported value type (%s).' ), 1691 gettype( $value ) 1692 ), 1693 '4.8.2' 1694 ); 1695 1696 // Preserving old behavior, where values are escaped as strings. 1697 $value = ''; 1698 } 1699 1700 $args_escaped[] = $this->_real_escape( $value ); 1701 } 1702 } 1703 1704 $query = vsprintf( $query, $args_escaped ); 1532 array_walk( $args, array( $this, 'escape_by_ref' ) ); 1533 $query = vsprintf( $query, $args ); 1705 1534 1706 1535 return $this->add_placeholder_escape( $query ); … … 3951 3780 * @since 4.1.0 Added support for the 'utf8mb4' feature. 3952 3781 * @since 4.6.0 Added support for the 'utf8mb4_520' feature. 3953 * @since 6.1.0 Added support for the 'identifier_placeholders' feature.3954 3782 * 3955 3783 * @see wpdb::db_version() 3956 3784 * 3957 3785 * @param string $db_cap The feature to check for. Accepts 'collation', 'group_concat', 3958 * 'subqueries', 'set_charset', 'utf8mb4', 'utf8mb4_520', 3959 * or 'identifier_placeholders'. 3786 * 'subqueries', 'set_charset', 'utf8mb4', or 'utf8mb4_520'. 3960 3787 * @return bool True when the database feature is supported, false otherwise. 3961 3788 */ … … 4002 3829 case 'utf8mb4_520': // @since 4.6.0 4003 3830 return version_compare( $db_version, '5.6', '>=' ); 4004 case 'identifier_placeholders': // @since 6.1.04005 /*4006 * As of WordPress 6.1, wpdb::prepare() supports identifiers via '%i',4007 * e.g. table/field names.4008 */4009 return true;4010 3831 } 4011 3832
Note: See TracChangeset
for help on using the changeset viewer.