Make WordPress Core

Changeset 46898 for branches/5.3


Ignore:
Timestamp:
12/12/2019 06:08:36 PM (7 years ago)
Author:
whyisjake
Message:

Prevent stored XSS through wp_targeted_link_rel().

Brings r46894 to the 5.3 branch.

Props: vortfu, whyisjake, peterwilsoncc, xknown, SergeyBiryukov, flaviozavan.

Location:
branches/5.3
Files:
2 edited

Legend:

Unmodified
Added
Removed
  • branches/5.3/src/wp-includes/formatting.php

    r46564 r46898  
    31473147function wp_targeted_link_rel( $text ) {
    31483148        // Don't run (more expensive) regex if no links with targets.
    3149         if ( stripos( $text, 'target' ) !== false && stripos( $text, '<a ' ) !== false ) {
    3150                 if ( ! is_serialized( $text ) ) {
    3151                         $text = preg_replace_callback( '|<a\s([^>]*target\s*=[^>]*)>|i', 'wp_targeted_link_rel_callback', $text );
     3149        if ( stripos( $text, 'target' ) === false || stripos( $text, '<a ' ) === false || is_serialized( $text ) ) {
     3150                return $text;
     3151        }
     3152
     3153        $script_and_style_regex = '/<(script|style).*?<\/\\1>/si';
     3154
     3155        preg_match_all( $script_and_style_regex, $text, $matches );
     3156        $extra_parts = $matches[0];
     3157        $html_parts  = preg_split( $script_and_style_regex, $text );
     3158
     3159        foreach ( $html_parts as &$part ) {
     3160                $part = preg_replace_callback( '|<a\s([^>]*target\s*=[^>]*)>|i', 'wp_targeted_link_rel_callback', $part );
     3161        }
     3162
     3163        $text = '';
     3164        for ( $i = 0; $i < count( $html_parts ); $i++ ) {
     3165                $text .= $html_parts[ $i ];
     3166                if ( isset( $extra_parts[ $i ] ) ) {
     3167                        $text .= $extra_parts[ $i ];
    31523168                }
    31533169        }
    … …  
    31683184 */
    31693185function wp_targeted_link_rel_callback( $matches ) {
    3170         $link_html = $matches[1];
    3171         $rel_match = array();
     3186        $link_html          = $matches[1];
     3187        $original_link_html = $link_html;
     3188
     3189        // Consider the html escaped if there are no unescaped quotes
     3190        $is_escaped = ! preg_match( '/(^|[^\\\\])[\'"]/', $link_html );
     3191        if ( $is_escaped ) {
     3192                // Replace only the quotes so that they are parsable by wp_kses_hair, leave the rest as is
     3193                $link_html = preg_replace( '/\\\\([\'"])/', '$1', $link_html );
     3194        }
     3195
     3196        $atts = wp_kses_hair( $link_html, wp_allowed_protocols() );
    31723197
    31733198        /**
    … …  
    31813206        $rel = apply_filters( 'wp_targeted_link_rel', 'noopener noreferrer', $link_html );
    31823207
    3183         // Avoid additional regex if the filter removes rel values.
    3184         if ( ! $rel ) {
    3185                 return "<a $link_html>";
    3186         }
    3187 
    3188         // Value with delimiters, spaces around are optional.
    3189         $attr_regex = '|rel\s*=\s*?(\\\\{0,1}["\'])(.*?)\\1|i';
    3190         preg_match( $attr_regex, $link_html, $rel_match );
    3191 
    3192         if ( empty( $rel_match[0] ) ) {
    3193                 // No delimiters, try with a single value and spaces, because `rel =  va"lue` is totally fine...
    3194                 $attr_regex = '|rel\s*=(\s*)([^\s]*)|i';
    3195                 preg_match( $attr_regex, $link_html, $rel_match );
    3196         }
    3197 
    3198         if ( ! empty( $rel_match[0] ) ) {
    3199                 $parts     = preg_split( '|\s+|', strtolower( $rel_match[2] ) );
    3200                 $parts     = array_map( 'esc_attr', $parts );
    3201                 $needed    = explode( ' ', $rel );
    3202                 $parts     = array_unique( array_merge( $parts, $needed ) );
    3203                 $delimiter = trim( $rel_match[1] ) ? $rel_match[1] : '"';
    3204                 $rel       = 'rel=' . $delimiter . trim( implode( ' ', $parts ) ) . $delimiter;
    3205                 $link_html = str_replace( $rel_match[0], $rel, $link_html );
    3206         } elseif ( preg_match( '|target\s*=\s*?\\\\"|', $link_html ) ) {
    3207                 $link_html .= " rel=\\\"$rel\\\"";
    3208         } elseif ( preg_match( '#(target|href)\s*=\s*?\'#', $link_html ) ) {
    3209                 $link_html .= " rel='$rel'";
    3210         } else {
    3211                 $link_html .= " rel=\"$rel\"";
     3208        // Return early if no rel values to be added or if no actual target attribute
     3209        if ( ! $rel || ! isset( $atts['target'] ) ) {
     3210                return "<a $original_link_html>";
     3211        }
     3212
     3213        if ( isset( $atts['rel'] ) ) {
     3214                $all_parts = preg_split( '/\s/', "{$atts['rel']['value']} $rel", -1, PREG_SPLIT_NO_EMPTY );
     3215                $rel       = implode( ' ', array_unique( $all_parts ) );
     3216        }
     3217
     3218        $atts['rel']['whole'] = 'rel="' . esc_attr( $rel ) . '"';
     3219        $link_html            = join( ' ', array_column( $atts, 'whole' ) );
     3220
     3221        if ( $is_escaped ) {
     3222                $link_html = preg_replace( '/[\'"]/', '\\\\$0', $link_html );
    32123223        }
    32133224
  • branches/5.3/tests/phpunit/tests/formatting/WPTargetedLinkRel.php

    r45409 r46898  
    3939        public function test_rel_with_single_quote_delimiter() {
    4040                $content  = '<p>Links: <a href="/" rel=\'existing values\' target="_blank">Existing rel</a></p>';
    41                 $expected = '<p>Links: <a href="/" rel=\'existing values noopener noreferrer\' target="_blank">Existing rel</a></p>';
     41                $expected = '<p>Links: <a href="/" rel="existing values noopener noreferrer" target="_blank">Existing rel</a></p>';
    4242                $this->assertEquals( $expected, wp_targeted_link_rel( $content ) );
    4343        }
    … …  
    5252                $content  = '<p>Links: <a href="/" rel = existing target="_blank">Existing rel</a></p>';
    5353                $expected = '<p>Links: <a href="/" rel="existing noopener noreferrer" target="_blank">Existing rel</a></p>';
    54                 $this->assertEquals( $expected, wp_targeted_link_rel( $content ) );
    55         }
    56 
    57         public function test_rel_value_spaced_and_no_delimiter_and_values_to_escape() {
    58                 $content  = '<p>Links: <a href="/" rel = existing"value target="_blank">Existing rel</a></p>';
    59                 $expected = '<p>Links: <a href="/" rel="existing&quot;value noopener noreferrer" target="_blank">Existing rel</a></p>';
    6054                $this->assertEquals( $expected, wp_targeted_link_rel( $content ) );
    6155        }
    … …  
    115109
    116110        /**
    117          * Ensure correct quotes are used when relation attribute (rel) is missing.
     111         * Ensure the content of style and script tags are not processed
    118112         *
    119113         * @ticket 47244
    120114         */
    121         public function test_wp_targeted_link_rel_should_use_correct_quotes() {
    122                 $content  = '<p>Links: <a href=\'\/\' target=\'_blank\'>No rel<\/a><\/p>';
    123                 $expected = '<p>Links: <a href=\'\/\' target=\'_blank\' rel=\'noopener noreferrer\'>No rel<\/a><\/p>';
    124                 $this->assertEquals( $expected, wp_targeted_link_rel( $content ) );
    125 
    126                 $content  = '<p>Links: <a href=\'\/\' target=_blank>No rel<\/a><\/p>';
    127                 $expected = '<p>Links: <a href=\'\/\' target=_blank rel=\'noopener noreferrer\'>No rel<\/a><\/p>';
     115        public function test_wp_targeted_link_rel_skips_style_and_scripts() {
     116                $content  = '<style><a href="/" target=a></style><p>Links: <script>console.log("<a href=\'/\' target=a>hi</a>");</script><script>alert(1);</script>here <a href="/" target=_blank>aq</a></p><script>console.log("<a href=\'last\' target=\'_blank\'")</script>';
     117                $expected = '<style><a href="/" target=a></style><p>Links: <script>console.log("<a href=\'/\' target=a>hi</a>");</script><script>alert(1);</script>here <a href="/" target="_blank" rel="noopener noreferrer">aq</a></p><script>console.log("<a href=\'last\' target=\'_blank\'")</script>';
    128118                $this->assertEquals( $expected, wp_targeted_link_rel( $content ) );
    129119        }
    … …  
    140130        }
    141131
     132        public function test_wp_targeted_link_rel_tab_separated_values_are_split() {
     133                $content  = "<p>Links: <a href=\"/\" target=\"_blank\" rel=\"ugc\t\tnoopener\t\">No rel</a></p>";
     134                $expected = '<p>Links: <a href="/" target="_blank" rel="ugc noopener noreferrer">No rel</a></p>';
     135                $this->assertEquals( $expected, wp_targeted_link_rel( $content ) );
     136        }
     137
    142138}
Note: See TracChangeset for help on using the changeset viewer.