Make WordPress Core


Ignore:
Timestamp:
06/24/2022 08:33:56 PM (20 months ago)
Author:
davidbaumwald
Message:

Database: Add %i placeholder support to $wpdb->prepare to escape table and column names.

WordPress does not currently provide an explicit method for escaping SQL table and column names. This leads to potential security vulnerabilities, and makes reviewing code for security unnecessarily difficult. Also, static analysis tools also flag the queries as having unescaped SQL input.

Tables and column names in queries are usually in-the-raw, since using the existing %s will straight quote the value, making the query invalid.

This change introduces a new %i placeholder in $wpdb->prepare to properly quote table and column names using backticks.

Props tellyworth, iandunn, craigfrancis, peterwilsoncc, johnbillion, apokalyptik.
Fixes #52506.

File:
1 edited

Legend:

Unmodified
Added
Removed
  • trunk/tests/phpunit/tests/db.php

    r52218 r53575  
    493493        $this->assertTrue( $wpdb->has_cap( 'group_concat' ) );
    494494        $this->assertTrue( $wpdb->has_cap( 'subqueries' ) );
     495        $this->assertTrue( $wpdb->has_cap( 'identifier_placeholders' ) );
    495496        $this->assertTrue( $wpdb->has_cap( 'COLLATION' ) );
    496497        $this->assertTrue( $wpdb->has_cap( 'GROUP_CONCAT' ) );
    497498        $this->assertTrue( $wpdb->has_cap( 'SUBQUERIES' ) );
     499        $this->assertTrue( $wpdb->has_cap( 'IDENTIFIER_PLACEHOLDERS' ) );
    498500        $this->assertSame(
    499501            version_compare( $wpdb->db_version(), '5.0.7', '>=' ),
     
    17181720                "'hello' 'foo##'",
    17191721            ),
    1720         );
     1722            array(
     1723                'SELECT * FROM %i WHERE %i = %d;',
     1724                array( 'my_table', 'my_field', 321 ),
     1725                false,
     1726                'SELECT * FROM `my_table` WHERE `my_field` = 321;',
     1727            ),
     1728            array(
     1729                'WHERE %i = %d;',
     1730                array( 'evil_`_field', 321 ),
     1731                false,
     1732                'WHERE `evil_``_field` = 321;', // To quote the identifier itself, then you need to double the character, e.g. `a``b`.
     1733            ),
     1734            array(
     1735                'WHERE %i = %d;',
     1736                array( 'evil_````````_field', 321 ),
     1737                false,
     1738                'WHERE `evil_````````````````_field` = 321;',
     1739            ),
     1740            array(
     1741                'WHERE %i = %d;',
     1742                array( '``evil_field``', 321 ),
     1743                false,
     1744                'WHERE `````evil_field````` = 321;',
     1745            ),
     1746            array(
     1747                'WHERE %i = %d;',
     1748                array( 'evil\'field', 321 ),
     1749                false,
     1750                'WHERE `evil\'field` = 321;',
     1751            ),
     1752            array(
     1753                'WHERE %i = %d;',
     1754                array( 'evil_\``_field', 321 ),
     1755                false,
     1756                'WHERE `evil_\````_field` = 321;',
     1757            ),
     1758            array(
     1759                'WHERE %i = %d;',
     1760                array( 'evil_%s_field', 321 ),
     1761                false,
     1762                "WHERE `evil_{$wpdb->placeholder_escape()}s_field` = 321;",
     1763            ),
     1764            array(
     1765                'WHERE %i = %d;',
     1766                array( 'value`', 321 ),
     1767                false,
     1768                'WHERE `value``` = 321;',
     1769            ),
     1770            array(
     1771                'WHERE `%i = %d;',
     1772                array( ' AND evil_value', 321 ),
     1773                false,
     1774                'WHERE `` AND evil_value` = 321;', // Won't run (SQL parse error: "Unclosed quote").
     1775            ),
     1776            array(
     1777                'WHERE %i` = %d;',
     1778                array( 'evil_value -- ', 321 ),
     1779                false,
     1780                'WHERE `evil_value -- `` = 321;', // Won't run (SQL parse error: "Unclosed quote").
     1781            ),
     1782            array(
     1783                'WHERE `%i`` = %d;',
     1784                array( ' AND true -- ', 321 ),
     1785                false,
     1786                'WHERE `` AND true -- ``` = 321;', // Won't run (Unknown column '').
     1787            ),
     1788            array(
     1789                'WHERE ``%i` = %d;',
     1790                array( ' AND true -- ', 321 ),
     1791                false,
     1792                'WHERE ``` AND true -- `` = 321;', // Won't run (SQL parse error: "Unclosed quote").
     1793            ),
     1794            array(
     1795                'WHERE %2$i = %1$d;',
     1796                array( '1', 'two' ),
     1797                false,
     1798                'WHERE `two` = 1;',
     1799            ),
     1800            array(
     1801                'WHERE \'%i\' = 1 AND "%i" = 2 AND `%i` = 3 AND ``%i`` = 4 AND %15i = 5',
     1802                array( 'my_field1', 'my_field2', 'my_field3', 'my_field4', 'my_field5' ),
     1803                false,
     1804                'WHERE \'`my_field1`\' = 1 AND "`my_field2`" = 2 AND ``my_field3`` = 3 AND ```my_field4``` = 4 AND `      my_field5` = 5', // Does not remove any existing quotes, always adds it's own (safer).
     1805            ),
     1806            array(
     1807                'WHERE id = %d AND %i LIKE %2$s LIMIT 1',
     1808                array( 123, 'field -- ', false ),
     1809                true, // Incorrect usage.
     1810                null, // Should be rejected, otherwise the `%1$s` could use Identifier escaping, e.g. 'WHERE `field -- ` LIKE field --  LIMIT 1' (thanks @vortfu).
     1811            ),
     1812            array(
     1813                'WHERE %i LIKE %s LIMIT 1',
     1814                array( "field' -- ", "field' -- " ),
     1815                false,
     1816                "WHERE `field' -- ` LIKE 'field\' -- ' LIMIT 1", // In contrast to the above, Identifier vs String escaping is used.
     1817            ),
     1818        );
     1819    }
     1820
     1821    public function test_allow_unsafe_unquoted_parameters() {
     1822        global $wpdb;
     1823
     1824        $sql    = 'WHERE (%i = %s) OR (%10i = %10s) OR (%5$i = %6$s)';
     1825        $values = array( 'field_a', 'string_a', 'field_b', 'string_b', 'field_c', 'string_c' );
     1826
     1827        $default = $wpdb->allow_unsafe_unquoted_parameters;
     1828
     1829        $wpdb->allow_unsafe_unquoted_parameters = true;
     1830
     1831        // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared
     1832        $part = $wpdb->prepare( $sql, $values );
     1833        $this->assertSame( 'WHERE (`field_a` = \'string_a\') OR (`   field_b` =   string_b) OR (`field_c` = string_c)', $part ); // Unsafe, unquoted parameters.
     1834
     1835        $wpdb->allow_unsafe_unquoted_parameters = false;
     1836
     1837        // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared
     1838        $part = $wpdb->prepare( $sql, $values );
     1839        $this->assertSame( 'WHERE (`field_a` = \'string_a\') OR (`   field_b` = \'  string_b\') OR (`field_c` = \'string_c\')', $part );
     1840
     1841        $wpdb->allow_unsafe_unquoted_parameters = $default;
     1842
    17211843    }
    17221844
Note: See TracChangeset for help on using the changeset viewer.