Make WordPress Core

Changeset 51963


Ignore:
Timestamp:
11/01/2021 02:12:09 AM (3 months ago)
Author:
pento
Message:

KSES: Add options for restricting tags based upon their attributes.

This change adds two now attribute-related config options to KSES:

  • An array of allowed values can be defined for attributes. If the attribute value doesn't fall into the list, the attribute will be removed from the tag.
  • Attributes can be marked as required. If a required attribute is not present, KSES will remove all attributes from the tag. As KSES doesn't match opening and closing tags, it's not possible to safely remove the tag itself, the safest fallback is to strip all attributes from the tag, instead.

Included with this change is an implementation of these options, allowing the <object> tag to be stored in posts, but only when it has a type attribute set to application/pdf.

Props pento, swissspidy, peterwilsoncc, dd32, jorbin.
Fixes #54261.

Location:
trunk
Files:
2 edited

Legend:

Unmodified
Added
Removed
  • trunk/src/wp-includes/kses.php

    r51729 r51963  
    271271            'lang'     => true,
    272272            'xml:lang' => true,
     273        ),
     274        'object'     => array(
     275            'data' => true,
     276            'type' => array(
     277                'required' => true,
     278                'values'   => array( 'application/pdf' ),
     279            ),
    273280        ),
    274281        'p'          => array(
     
    11661173    $attrarr = wp_kses_hair( $attr, $allowed_protocols );
    11671174
     1175    // Check if there are attributes that are required.
     1176    $required_attrs = array_filter(
     1177        $allowed_html[ $element_low ],
     1178        function( $required_attr_limits ) {
     1179            return isset( $required_attr_limits['required'] ) && true === $required_attr_limits['required'];
     1180        }
     1181    );
     1182
     1183    // If a required attribute check fails, we can return nothing for a self-closing tag,
     1184    // but for a non-self-closing tag the best option is to return the element with attributes,
     1185    // as KSES doesn't handle matching the relevant closing tag.
     1186    $stripped_tag = '';
     1187    if ( empty( $xhtml_slash ) ) {
     1188        $stripped_tag = "<$element>";
     1189    }
     1190
    11681191    // Go through $attrarr, and save the allowed attributes for this element
    11691192    // in $attr2.
    11701193    $attr2 = '';
    11711194    foreach ( $attrarr as $arreach ) {
     1195        // Check if this attribute is required.
     1196        $required = isset( $required_attrs[ strtolower( $arreach['name'] ) ] );
     1197
    11721198        if ( wp_kses_attr_check( $arreach['name'], $arreach['value'], $arreach['whole'], $arreach['vless'], $element, $allowed_html ) ) {
    11731199            $attr2 .= ' ' . $arreach['whole'];
     1200
     1201            // If this was a required attribute, we can mark it as found.
     1202            if ( $required ) {
     1203                unset( $required_attrs[ strtolower( $arreach['name'] ) ] );
     1204            }
     1205        } elseif ( $required ) {
     1206            // This attribute was required, but didn't pass the check. The entire tag is not allowed.
     1207            return $stripped_tag;
    11741208        }
     1209    }
     1210
     1211    // If some required attributes weren't set, the entire tag is not allowed.
     1212    if ( ! empty( $required_attrs ) ) {
     1213        return $stripped_tag;
    11751214    }
    11761215
     
    16011640            }
    16021641            break;
     1642
     1643        case 'values':
     1644            /*
     1645             * The values check is used when you want to make sure that the attribute
     1646             * has one of the given values.
     1647             */
     1648
     1649            if ( false === array_search( strtolower( $value ), $checkvalue, true ) ) {
     1650                $ok = false;
     1651            }
     1652            break;
    16031653    } // End switch.
    16041654
  • trunk/tests/phpunit/tests/kses.php

    r51397 r51963  
    14971497        $this->assertSame( $html, wp_kses_post( $html ) );
    14981498    }
     1499
     1500    /**
     1501     * Test that object tags are allowed under limited circumstances.
     1502     *
     1503     * @ticket 54261
     1504     *
     1505     * @dataProvider data_wp_kses_object_tag_allowed
     1506     *
     1507     * @param string $html     A string of HTML to test.
     1508     * @param string $expected The expected result from KSES.
     1509     */
     1510    function test_wp_kses_object_tag_allowed( $html, $expected ) {
     1511        $this->assertSame( $expected, wp_kses_post( $html ) );
     1512    }
     1513
     1514    /**
     1515     * Data provider for test_wp_kses_object_tag_allowed().
     1516     */
     1517    function data_wp_kses_object_tag_allowed() {
     1518        return array(
     1519            'valid value for type'                    => array(
     1520                '<object type="application/pdf" data="https://wordpress.org/foo.pdf" />',
     1521                '<object type="application/pdf" data="https://wordpress.org/foo.pdf" />',
     1522            ),
     1523            'invalid value for type'                  => array(
     1524                '<object type="application/exe" data="https://wordpress.org/foo.exe" />',
     1525                '',
     1526            ),
     1527            'multiple type attributes, last invalid'  => array(
     1528                '<object type="application/pdf" type="application/exe" data="https://wordpress.org/foo.pdf" />',
     1529                '<object type="application/pdf" data="https://wordpress.org/foo.pdf" />',
     1530            ),
     1531            'multiple type attributes, first uppercase, last invalid' => array(
     1532                '<object TYPE="application/pdf" type="application/exe" data="https://wordpress.org/foo.pdf" />',
     1533                '<object TYPE="application/pdf" data="https://wordpress.org/foo.pdf" />',
     1534            ),
     1535            'multiple type attributes, last upper case and invalid' => array(
     1536                '<object type="application/pdf" TYPE="application/exe" data="https://wordpress.org/foo.pdf" />',
     1537                '<object type="application/pdf" data="https://wordpress.org/foo.pdf" />',
     1538            ),
     1539            'multiple type attributes, first invalid' => array(
     1540                '<object type="application/exe" type="application/pdf" data="https://wordpress.org/foo.pdf" />',
     1541                '',
     1542            ),
     1543            'multiple type attributes, first upper case and invalid' => array(
     1544                '<object TYPE="application/exe" type="application/pdf" data="https://wordpress.org/foo.pdf" />',
     1545                '',
     1546            ),
     1547            'multiple type attributes, first invalid, last uppercase' => array(
     1548                '<object type="application/exe" TYPE="application/pdf" data="https://wordpress.org/foo.pdf" />',
     1549                '',
     1550            ),
     1551            'multiple object tags, last invalid'      => array(
     1552                '<object type="application/pdf" data="https://wordpress.org/foo.pdf" /><object type="application/exe" data="https://wordpress.org/foo.exe" />',
     1553                '<object type="application/pdf" data="https://wordpress.org/foo.pdf" />',
     1554            ),
     1555            'multiple object tags, first invalid'     => array(
     1556                '<object type="application/exe" data="https://wordpress.org/foo.exe" /><object type="application/pdf" data="https://wordpress.org/foo.pdf" />',
     1557                '<object type="application/pdf" data="https://wordpress.org/foo.pdf" />',
     1558            ),
     1559            'type attribute with partially incorrect value' => array(
     1560                '<object type="application/pdfa" data="https://wordpress.org/foo.pdf" />',
     1561                '',
     1562            ),
     1563            'type attribute with empty value'         => array(
     1564                '<object type="" data="https://wordpress.org/foo.pdf" />',
     1565                '',
     1566            ),
     1567            'type attribute with no value'            => array(
     1568                '<object type data="https://wordpress.org/foo.pdf" />',
     1569                '',
     1570            ),
     1571            'no type attribute'                       => array(
     1572                '<object data="https://wordpress.org/foo.pdf" />',
     1573                '',
     1574            ),
     1575        );
     1576    }
     1577
     1578    /**
     1579     * Test that object tags will continue to function if they've been added using the
     1580     * 'wp_kses_allowed_html' filter.
     1581     *
     1582     * @ticket 54261
     1583     */
     1584    function test_wp_kses_object_added_in_html_filter() {
     1585        $html = <<<HTML
     1586<object type="application/pdf" data="https://wordpress.org/foo.pdf" />
     1587<object type="application/x-shockwave-flash" data="https://wordpress.org/foo.swf">
     1588    <param name="foo" value="bar" />
     1589</object>
     1590HTML;
     1591
     1592        add_filter( 'wp_kses_allowed_html', array( $this, 'filter_wp_kses_object_added_in_html_filter' ), 10, 2 );
     1593
     1594        $filtered_html = wp_kses_post( $html );
     1595
     1596        remove_filter( 'wp_kses_allowed_html', array( $this, 'filter_wp_kses_object_added_in_html_filter' ) );
     1597
     1598        $this->assertSame( $html, $filtered_html );
     1599    }
     1600
     1601    function filter_wp_kses_object_added_in_html_filter( $tags, $context ) {
     1602        if ( 'post' === $context ) {
     1603            $tags['object'] = array(
     1604                'type' => true,
     1605                'data' => true,
     1606            );
     1607
     1608            $tags['param'] = array(
     1609                'name'  => true,
     1610                'value' => true,
     1611            );
     1612        }
     1613
     1614        return $tags;
     1615    }
     1616
     1617    /**
     1618     * Test that attributes with a list of allowed values are filtered correctly.
     1619     *
     1620     * @ticket 54261
     1621     *
     1622     * @dataProvider data_wp_kses_allowed_values_list
     1623     *
     1624     * @param string $html         A string of HTML to test.
     1625     * @param string $expected     The expected result from KSES.
     1626     * @param array  $allowed_html The allowed HTML to pass to KSES.
     1627     */
     1628    function test_wp_kses_allowed_values_list( $html, $expected, $allowed_html ) {
     1629        $this->assertSame( $expected, wp_kses( $html, $allowed_html ) );
     1630    }
     1631
     1632    /**
     1633     * Data provider for test_wp_kses_allowed_values_list().
     1634     */
     1635    function data_wp_kses_allowed_values_list() {
     1636        $data = array(
     1637            'valid dir attribute value'             => array(
     1638                '<p dir="ltr">foo</p>',
     1639                '<p dir="ltr">foo</p>',
     1640            ),
     1641            'valid dir attribute value, upper case' => array(
     1642                '<p DIR="RTL">foo</p>',
     1643                '<p DIR="RTL">foo</p>',
     1644            ),
     1645            'invalid dir attribute value'           => array(
     1646                '<p dir="up">foo</p>',
     1647                '<p>foo</p>',
     1648            ),
     1649            'dir attribute with empty value'        => array(
     1650                '<p dir="">foo</p>',
     1651                '<p>foo</p>',
     1652            ),
     1653            'dir attribute with no value'           => array(
     1654                '<p dir>foo</p>',
     1655                '<p>foo</p>',
     1656            ),
     1657        );
     1658
     1659        return array_map(
     1660            function ( $datum ) {
     1661                $datum[] = array(
     1662                    'p' => array(
     1663                        'dir' => array(
     1664                            'values' => array( 'ltr', 'rtl' ),
     1665                        ),
     1666                    ),
     1667                );
     1668
     1669                return $datum;
     1670            },
     1671            $data
     1672        );
     1673    }
     1674
     1675    /**
     1676     * Test that attributes with the required flag are handled correctly.
     1677     *
     1678     * @ticket 54261
     1679     *
     1680     * @dataProvider data_wp_kses_required_attribute
     1681     *
     1682     * @param string $html         A string of HTML to test.
     1683     * @param string $expected     The expected result from KSES.
     1684     * @param array  $allowed_html The allowed HTML to pass to KSES.
     1685     */
     1686    function test_wp_kses_required_attribute( $html, $expected, $allowed_html ) {
     1687        $this->assertSame( $expected, wp_kses( $html, $allowed_html ) );
     1688    }
     1689
     1690    /**
     1691     * Data provider for test_wp_kses_required_attribute().
     1692     */
     1693    function data_wp_kses_required_attribute() {
     1694        $data = array(
     1695            'valid dir attribute value'             => array(
     1696                '<p dir="ltr">foo</p>', // Test HTML.
     1697                '<p dir="ltr">foo</p>', // Expected result when dir is not required.
     1698                '<p dir="ltr">foo</p>', // Expected result when dir is required.
     1699                '<p dir="ltr">foo</p>', // Expected result when dir is required, but has no value filter.
     1700            ),
     1701            'valid dir attribute value, upper case' => array(
     1702                '<p DIR="RTL">foo</p>',
     1703                '<p DIR="RTL">foo</p>',
     1704                '<p DIR="RTL">foo</p>',
     1705                '<p DIR="RTL">foo</p>',
     1706            ),
     1707            'invalid dir attribute value'           => array(
     1708                '<p dir="up">foo</p>',
     1709                '<p>foo</p>',
     1710                '<p>foo</p>',
     1711                '<p dir="up">foo</p>',
     1712            ),
     1713            'dir attribute with empty value'        => array(
     1714                '<p dir="">foo</p>',
     1715                '<p>foo</p>',
     1716                '<p>foo</p>',
     1717                '<p dir="">foo</p>',
     1718            ),
     1719            'dir attribute with no value'           => array(
     1720                '<p dir>foo</p>',
     1721                '<p>foo</p>',
     1722                '<p>foo</p>',
     1723                '<p dir>foo</p>',
     1724            ),
     1725            'dir attribute not set'                 => array(
     1726                '<p>foo</p>',
     1727                '<p>foo</p>',
     1728                '<p>foo</p>',
     1729                '<p>foo</p>',
     1730            ),
     1731        );
     1732
     1733        $return_data = array();
     1734
     1735        foreach ( $data as $description => $datum ) {
     1736            // Test that the required flag defaults to false.
     1737            $return_data[ "$description - required flag not set" ] = array(
     1738                $datum[0],
     1739                $datum[1],
     1740                array(
     1741                    'p' => array(
     1742                        'dir' => array(
     1743                            'values' => array( 'ltr', 'rtl' ),
     1744                        ),
     1745                    ),
     1746                ),
     1747            );
     1748
     1749            // Test when the attribute is not required, but has allowed values.
     1750            $return_data[ "$description - required flag set to false" ] = array(
     1751                $datum[0],
     1752                $datum[1],
     1753                array(
     1754                    'p' => array(
     1755                        'dir' => array(
     1756                            'required' => false,
     1757                            'values'   => array( 'ltr', 'rtl' ),
     1758                        ),
     1759                    ),
     1760                ),
     1761            );
     1762
     1763            // Test when the attribute is required, but has allowed values.
     1764            $return_data[ "$description - required flag set to true" ] = array(
     1765                $datum[0],
     1766                $datum[2],
     1767                array(
     1768                    'p' => array(
     1769                        'dir' => array(
     1770                            'required' => true,
     1771                            'values'   => array( 'ltr', 'rtl' ),
     1772                        ),
     1773                    ),
     1774                ),
     1775            );
     1776
     1777            // Test when the attribute is required, but has no allowed values.
     1778            $return_data[ "$description - required flag set to true, no allowed values specified" ] = array(
     1779                $datum[0],
     1780                $datum[3],
     1781                array(
     1782                    'p' => array(
     1783                        'dir' => array(
     1784                            'required' => true,
     1785                        ),
     1786                    ),
     1787                ),
     1788            );
     1789        }
     1790
     1791        return $return_data;
     1792    }
    14991793}
Note: See TracChangeset for help on using the changeset viewer.