Skip to content

Commit

Permalink
KSES: Add options for restricting tags based upon their attributes.
Browse files Browse the repository at this point in the history
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.



git-svn-id: https://develop.svn.wordpress.org/trunk@51963 602fd350-edb4-49c9-b593-d223f7449a82
  • Loading branch information
pento committed Nov 1, 2021
1 parent bb6c5db commit 9ca3e8f
Show file tree
Hide file tree
Showing 2 changed files with 344 additions and 0 deletions.
50 changes: 50 additions & 0 deletions src/wp-includes/kses.php
Original file line number Diff line number Diff line change
Expand Up @@ -271,6 +271,13 @@
'lang' => true,
'xml:lang' => true,
),
'object' => array(
'data' => true,
'type' => array(
'required' => true,
'values' => array( 'application/pdf' ),
),
),
'p' => array(
'align' => true,
'dir' => true,
Expand Down Expand Up @@ -1165,15 +1172,47 @@ function wp_kses_attr( $element, $attr, $allowed_html, $allowed_protocols ) {
// Split it.
$attrarr = wp_kses_hair( $attr, $allowed_protocols );

// Check if there are attributes that are required.
$required_attrs = array_filter(
$allowed_html[ $element_low ],
function( $required_attr_limits ) {
return isset( $required_attr_limits['required'] ) && true === $required_attr_limits['required'];
}
);

// If a required attribute check fails, we can return nothing for a self-closing tag,
// but for a non-self-closing tag the best option is to return the element with attributes,
// as KSES doesn't handle matching the relevant closing tag.
$stripped_tag = '';
if ( empty( $xhtml_slash ) ) {
$stripped_tag = "<$element>";
}

// Go through $attrarr, and save the allowed attributes for this element
// in $attr2.
$attr2 = '';
foreach ( $attrarr as $arreach ) {
// Check if this attribute is required.
$required = isset( $required_attrs[ strtolower( $arreach['name'] ) ] );

if ( wp_kses_attr_check( $arreach['name'], $arreach['value'], $arreach['whole'], $arreach['vless'], $element, $allowed_html ) ) {
$attr2 .= ' ' . $arreach['whole'];

// If this was a required attribute, we can mark it as found.
if ( $required ) {
unset( $required_attrs[ strtolower( $arreach['name'] ) ] );
}
} elseif ( $required ) {
// This attribute was required, but didn't pass the check. The entire tag is not allowed.
return $stripped_tag;
}
}

// If some required attributes weren't set, the entire tag is not allowed.
if ( ! empty( $required_attrs ) ) {
return $stripped_tag;
}

// Remove any "<" or ">" characters.
$attr2 = preg_replace( '/[<>]/', '', $attr2 );

Expand Down Expand Up @@ -1600,6 +1639,17 @@ function wp_kses_check_attr_val( $value, $vless, $checkname, $checkvalue ) {
$ok = false;
}
break;

case 'values':
/*
* The values check is used when you want to make sure that the attribute
* has one of the given values.
*/

if ( false === array_search( strtolower( $value ), $checkvalue, true ) ) {
$ok = false;
}
break;
} // End switch.

return $ok;
Expand Down
294 changes: 294 additions & 0 deletions tests/phpunit/tests/kses.php
Original file line number Diff line number Diff line change
Expand Up @@ -1496,4 +1496,298 @@ function test_wp_kses_main_tag_standard_attributes() {

$this->assertSame( $html, wp_kses_post( $html ) );
}

/**
* Test that object tags are allowed under limited circumstances.
*
* @ticket 54261
*
* @dataProvider data_wp_kses_object_tag_allowed
*
* @param string $html A string of HTML to test.
* @param string $expected The expected result from KSES.
*/
function test_wp_kses_object_tag_allowed( $html, $expected ) {
$this->assertSame( $expected, wp_kses_post( $html ) );
}

/**
* Data provider for test_wp_kses_object_tag_allowed().
*/
function data_wp_kses_object_tag_allowed() {
return array(
'valid value for type' => array(
'<object type="application/pdf" data="https://wordpress.org/foo.pdf" />',
'<object type="application/pdf" data="https://wordpress.org/foo.pdf" />',
),
'invalid value for type' => array(
'<object type="application/exe" data="https://wordpress.org/foo.exe" />',
'',
),
'multiple type attributes, last invalid' => array(
'<object type="application/pdf" type="application/exe" data="https://wordpress.org/foo.pdf" />',
'<object type="application/pdf" data="https://wordpress.org/foo.pdf" />',
),
'multiple type attributes, first uppercase, last invalid' => array(
'<object TYPE="application/pdf" type="application/exe" data="https://wordpress.org/foo.pdf" />',
'<object TYPE="application/pdf" data="https://wordpress.org/foo.pdf" />',
),
'multiple type attributes, last upper case and invalid' => array(
'<object type="application/pdf" TYPE="application/exe" data="https://wordpress.org/foo.pdf" />',
'<object type="application/pdf" data="https://wordpress.org/foo.pdf" />',
),
'multiple type attributes, first invalid' => array(
'<object type="application/exe" type="application/pdf" data="https://wordpress.org/foo.pdf" />',
'',
),
'multiple type attributes, first upper case and invalid' => array(
'<object TYPE="application/exe" type="application/pdf" data="https://wordpress.org/foo.pdf" />',
'',
),
'multiple type attributes, first invalid, last uppercase' => array(
'<object type="application/exe" TYPE="application/pdf" data="https://wordpress.org/foo.pdf" />',
'',
),
'multiple object tags, last invalid' => array(
'<object type="application/pdf" data="https://wordpress.org/foo.pdf" /><object type="application/exe" data="https://wordpress.org/foo.exe" />',
'<object type="application/pdf" data="https://wordpress.org/foo.pdf" />',
),
'multiple object tags, first invalid' => array(
'<object type="application/exe" data="https://wordpress.org/foo.exe" /><object type="application/pdf" data="https://wordpress.org/foo.pdf" />',
'<object type="application/pdf" data="https://wordpress.org/foo.pdf" />',
),
'type attribute with partially incorrect value' => array(
'<object type="application/pdfa" data="https://wordpress.org/foo.pdf" />',
'',
),
'type attribute with empty value' => array(
'<object type="" data="https://wordpress.org/foo.pdf" />',
'',
),
'type attribute with no value' => array(
'<object type data="https://wordpress.org/foo.pdf" />',
'',
),
'no type attribute' => array(
'<object data="https://wordpress.org/foo.pdf" />',
'',
),
);
}

/**
* Test that object tags will continue to function if they've been added using the
* 'wp_kses_allowed_html' filter.
*
* @ticket 54261
*/
function test_wp_kses_object_added_in_html_filter() {
$html = <<<HTML
<object type="application/pdf" data="https://wordpress.org/foo.pdf" />
<object type="application/x-shockwave-flash" data="https://wordpress.org/foo.swf">
<param name="foo" value="bar" />
</object>
HTML;

add_filter( 'wp_kses_allowed_html', array( $this, 'filter_wp_kses_object_added_in_html_filter' ), 10, 2 );

$filtered_html = wp_kses_post( $html );

remove_filter( 'wp_kses_allowed_html', array( $this, 'filter_wp_kses_object_added_in_html_filter' ) );

$this->assertSame( $html, $filtered_html );
}

function filter_wp_kses_object_added_in_html_filter( $tags, $context ) {
if ( 'post' === $context ) {
$tags['object'] = array(
'type' => true,
'data' => true,
);

$tags['param'] = array(
'name' => true,
'value' => true,
);
}

return $tags;
}

/**
* Test that attributes with a list of allowed values are filtered correctly.
*
* @ticket 54261
*
* @dataProvider data_wp_kses_allowed_values_list
*
* @param string $html A string of HTML to test.
* @param string $expected The expected result from KSES.
* @param array $allowed_html The allowed HTML to pass to KSES.
*/
function test_wp_kses_allowed_values_list( $html, $expected, $allowed_html ) {
$this->assertSame( $expected, wp_kses( $html, $allowed_html ) );
}

/**
* Data provider for test_wp_kses_allowed_values_list().
*/
function data_wp_kses_allowed_values_list() {
$data = array(
'valid dir attribute value' => array(
'<p dir="ltr">foo</p>',
'<p dir="ltr">foo</p>',
),
'valid dir attribute value, upper case' => array(
'<p DIR="RTL">foo</p>',
'<p DIR="RTL">foo</p>',
),
'invalid dir attribute value' => array(
'<p dir="up">foo</p>',
'<p>foo</p>',
),
'dir attribute with empty value' => array(
'<p dir="">foo</p>',
'<p>foo</p>',
),
'dir attribute with no value' => array(
'<p dir>foo</p>',
'<p>foo</p>',
),
);

return array_map(
function ( $datum ) {
$datum[] = array(
'p' => array(
'dir' => array(
'values' => array( 'ltr', 'rtl' ),
),
),
);

return $datum;
},
$data
);
}

/**
* Test that attributes with the required flag are handled correctly.
*
* @ticket 54261
*
* @dataProvider data_wp_kses_required_attribute
*
* @param string $html A string of HTML to test.
* @param string $expected The expected result from KSES.
* @param array $allowed_html The allowed HTML to pass to KSES.
*/
function test_wp_kses_required_attribute( $html, $expected, $allowed_html ) {
$this->assertSame( $expected, wp_kses( $html, $allowed_html ) );
}

/**
* Data provider for test_wp_kses_required_attribute().
*/
function data_wp_kses_required_attribute() {
$data = array(
'valid dir attribute value' => array(
'<p dir="ltr">foo</p>', // Test HTML.
'<p dir="ltr">foo</p>', // Expected result when dir is not required.
'<p dir="ltr">foo</p>', // Expected result when dir is required.
'<p dir="ltr">foo</p>', // Expected result when dir is required, but has no value filter.
),
'valid dir attribute value, upper case' => array(
'<p DIR="RTL">foo</p>',
'<p DIR="RTL">foo</p>',
'<p DIR="RTL">foo</p>',
'<p DIR="RTL">foo</p>',
),
'invalid dir attribute value' => array(
'<p dir="up">foo</p>',
'<p>foo</p>',
'<p>foo</p>',
'<p dir="up">foo</p>',
),
'dir attribute with empty value' => array(
'<p dir="">foo</p>',
'<p>foo</p>',
'<p>foo</p>',
'<p dir="">foo</p>',
),
'dir attribute with no value' => array(
'<p dir>foo</p>',
'<p>foo</p>',
'<p>foo</p>',
'<p dir>foo</p>',
),
'dir attribute not set' => array(
'<p>foo</p>',
'<p>foo</p>',
'<p>foo</p>',
'<p>foo</p>',
),
);

$return_data = array();

foreach ( $data as $description => $datum ) {
// Test that the required flag defaults to false.
$return_data[ "$description - required flag not set" ] = array(
$datum[0],
$datum[1],
array(
'p' => array(
'dir' => array(
'values' => array( 'ltr', 'rtl' ),
),
),
),
);

// Test when the attribute is not required, but has allowed values.
$return_data[ "$description - required flag set to false" ] = array(
$datum[0],
$datum[1],
array(
'p' => array(
'dir' => array(
'required' => false,
'values' => array( 'ltr', 'rtl' ),
),
),
),
);

// Test when the attribute is required, but has allowed values.
$return_data[ "$description - required flag set to true" ] = array(
$datum[0],
$datum[2],
array(
'p' => array(
'dir' => array(
'required' => true,
'values' => array( 'ltr', 'rtl' ),
),
),
),
);

// Test when the attribute is required, but has no allowed values.
$return_data[ "$description - required flag set to true, no allowed values specified" ] = array(
$datum[0],
$datum[3],
array(
'p' => array(
'dir' => array(
'required' => true,
),
),
),
);
}

return $return_data;
}
}

0 comments on commit 9ca3e8f

Please sign in to comment.