Make WordPress Core


Ignore:
Timestamp:
12/12/2019 05:51:35 PM (5 years ago)
Author:
whyisjake
Message:

Prevent stored XSS through wp_targeted_link_rel().

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

File:
1 edited

Legend:

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

    r46660 r46894  
    31493149function wp_targeted_link_rel( $text ) {
    31503150    // Don't run (more expensive) regex if no links with targets.
    3151     if ( stripos( $text, 'target' ) !== false && stripos( $text, '<a ' ) !== false ) {
    3152         if ( ! is_serialized( $text ) ) {
    3153             $text = preg_replace_callback( '|<a\s([^>]*target\s*=[^>]*)>|i', 'wp_targeted_link_rel_callback', $text );
     3151    if ( stripos( $text, 'target' ) === false || stripos( $text, '<a ' ) === false || is_serialized( $text ) ) {
     3152        return $text;
     3153    }
     3154
     3155    $script_and_style_regex = '/<(script|style).*?<\/\\1>/si';
     3156
     3157    preg_match_all( $script_and_style_regex, $text, $matches );
     3158    $extra_parts = $matches[0];
     3159    $html_parts  = preg_split( $script_and_style_regex, $text );
     3160
     3161    foreach ( $html_parts as &$part ) {
     3162        $part = preg_replace_callback( '|<a\s([^>]*target\s*=[^>]*)>|i', 'wp_targeted_link_rel_callback', $part );
     3163    }
     3164
     3165    $text = '';
     3166    for ( $i = 0; $i < count( $html_parts ); $i++ ) {
     3167        $text .= $html_parts[ $i ];
     3168        if ( isset( $extra_parts[ $i ] ) ) {
     3169            $text .= $extra_parts[ $i ];
    31543170        }
    31553171    }
     
    31703186 */
    31713187function wp_targeted_link_rel_callback( $matches ) {
    3172     $link_html = $matches[1];
    3173     $rel_match = array();
     3188    $link_html          = $matches[1];
     3189    $original_link_html = $link_html;
     3190
     3191    // Consider the html escaped if there are no unescaped quotes
     3192    $is_escaped = ! preg_match( '/(^|[^\\\\])[\'"]/', $link_html );
     3193    if ( $is_escaped ) {
     3194        // Replace only the quotes so that they are parsable by wp_kses_hair, leave the rest as is
     3195        $link_html = preg_replace( '/\\\\([\'"])/', '$1', $link_html );
     3196    }
     3197
     3198    $atts = wp_kses_hair( $link_html, wp_allowed_protocols() );
    31743199
    31753200    /**
     
    31833208    $rel = apply_filters( 'wp_targeted_link_rel', 'noopener noreferrer', $link_html );
    31843209
    3185     // Avoid additional regex if the filter removes rel values.
    3186     if ( ! $rel ) {
    3187         return "<a $link_html>";
    3188     }
    3189 
    3190     // Value with delimiters, spaces around are optional.
    3191     $attr_regex = '|rel\s*=\s*?(\\\\{0,1}["\'])(.*?)\\1|i';
    3192     preg_match( $attr_regex, $link_html, $rel_match );
    3193 
    3194     if ( empty( $rel_match[0] ) ) {
    3195         // No delimiters, try with a single value and spaces, because `rel =  va"lue` is totally fine...
    3196         $attr_regex = '|rel\s*=(\s*)([^\s]*)|i';
    3197         preg_match( $attr_regex, $link_html, $rel_match );
    3198     }
    3199 
    3200     if ( ! empty( $rel_match[0] ) ) {
    3201         $parts     = preg_split( '|\s+|', strtolower( $rel_match[2] ) );
    3202         $parts     = array_map( 'esc_attr', $parts );
    3203         $needed    = explode( ' ', $rel );
    3204         $parts     = array_unique( array_merge( $parts, $needed ) );
    3205         $delimiter = trim( $rel_match[1] ) ? $rel_match[1] : '"';
    3206         $rel       = 'rel=' . $delimiter . trim( implode( ' ', $parts ) ) . $delimiter;
    3207         $link_html = str_replace( $rel_match[0], $rel, $link_html );
    3208     } elseif ( preg_match( '|target\s*=\s*?\\\\"|', $link_html ) ) {
    3209         $link_html .= " rel=\\\"$rel\\\"";
    3210     } elseif ( preg_match( '#(target|href)\s*=\s*?\'#', $link_html ) ) {
    3211         $link_html .= " rel='$rel'";
    3212     } else {
    3213         $link_html .= " rel=\"$rel\"";
     3210    // Return early if no rel values to be added or if no actual target attribute
     3211    if ( ! $rel || ! isset( $atts['target'] ) ) {
     3212        return "<a $original_link_html>";
     3213    }
     3214
     3215    if ( isset( $atts['rel'] ) ) {
     3216        $all_parts = preg_split( '/\s/', "{$atts['rel']['value']} $rel", -1, PREG_SPLIT_NO_EMPTY );
     3217        $rel       = implode( ' ', array_unique( $all_parts ) );
     3218    }
     3219
     3220    $atts['rel']['whole'] = 'rel="' . esc_attr( $rel ) . '"';
     3221    $link_html            = join( ' ', array_column( $atts, 'whole' ) );
     3222
     3223    if ( $is_escaped ) {
     3224        $link_html = preg_replace( '/[\'"]/', '\\\\$0', $link_html );
    32143225    }
    32153226
Note: See TracChangeset for help on using the changeset viewer.