Plugin Directory

Changeset 3749602


Ignore:
Timestamp:
10/16/2026 03:02:34 AM (10 days ago)
Author:
themifyme
Message:

Fix: XSS issue

Location:
themify-popup
Files:
113 added
8 edited

Legend:

Unmodified
Added
Removed
  • themify-popup/trunk/includes/system.php

    r3730038 r3749602  
    66                add_action( 'init', array( __CLASS__, 'i18n' ), 5 );
    77                add_action( 'init', array( __CLASS__, 'register_post_type' ) );
     8                add_action( 'admin_init', array( __CLASS__, 'cleanup_stored_css_meta' ) );
     9                add_action( 'save_post_themify_popup', array( __CLASS__, 'sanitize_saved_meta' ), PHP_INT_MAX );
    810                add_action( 'wp_loaded', array( __CLASS__, 'loaded' ) );
    911                if ( is_admin() ) {
    … …  
    533535            return $types;
    534536    }
     537
     538        /**
     539         * Drop CSS meta that is not a length, unit, or color.
     540         * Runs after the metabox save so already-stored payloads are removed on update.
     541         *
     542         * @param int $post_id Popup ID.
     543         */
     544        public static function sanitize_saved_meta( $post_id ) {
     545                if ( wp_is_post_revision( $post_id ) || wp_is_post_autosave( $post_id ) ) {
     546                        return;
     547                }
     548
     549                $units = array(
     550                        'popup_width_unit' => array( 'px', '%', 'em' ),
     551                        'popup_height_unit' => array( 'px', '%' ),
     552                );
     553                foreach ( $units as $key => $allowed ) {
     554                        $value = get_post_meta( $post_id, $key, true );
     555                        if ( ! is_string( $value ) || '' === $value ) {
     556                                if ( ! is_string( $value ) && '' !== $value && false !== $value ) {
     557                                        delete_post_meta( $post_id, $key );
     558                                }
     559                                continue;
     560                        }
     561                        if ( '' === themify_popup_css_unit( $value, $allowed ) ) {
     562                                delete_post_meta( $post_id, $key );
     563                        }
     564                }
     565
     566                foreach ( array( 'popup_width', 'popup_height' ) as $key ) {
     567                        $value = get_post_meta( $post_id, $key, true );
     568                        if ( ! is_string( $value ) || '' === $value ) {
     569                                if ( ! is_string( $value ) && '' !== $value && false !== $value ) {
     570                                        delete_post_meta( $post_id, $key );
     571                                }
     572                                continue;
     573                        }
     574                        $number = themify_popup_css_number( $value );
     575                        if ( '' === $number ) {
     576                                delete_post_meta( $post_id, $key );
     577                        } elseif ( $number !== $value ) {
     578                                update_post_meta( $post_id, $key, $number );
     579                        }
     580                }
     581
     582                $color = get_post_meta( $post_id, 'popup_overlay_color', true );
     583                if ( ! is_string( $color ) || '' === $color ) {
     584                        if ( ! is_string( $color ) && '' !== $color && false !== $color ) {
     585                                delete_post_meta( $post_id, 'popup_overlay_color' );
     586                        }
     587                        return;
     588                }
     589                $clean = themify_popup_sanitize_css_color( $color );
     590                if ( '' === $clean ) {
     591                        delete_post_meta( $post_id, 'popup_overlay_color' );
     592                } elseif ( $clean !== $color ) {
     593                        update_post_meta( $post_id, 'popup_overlay_color', $clean );
     594                }
     595        }
     596
     597        /**
     598         * Remove unsafe CSS meta saved before values were validated.
     599         */
     600        public static function cleanup_stored_css_meta() {
     601                if ( get_option( 'themify_popup_css_meta_sanitized' ) ) {
     602                        return;
     603                }
     604                $ids = get_posts( array(
     605                        'post_type' => 'themify_popup',
     606                        'post_status' => 'any',
     607                        'posts_per_page' => -1,
     608                        'fields' => 'ids',
     609                        'no_found_rows' => true,
     610                ) );
     611                foreach ( $ids as $id ) {
     612                        self::sanitize_saved_meta( $id );
     613                }
     614                update_option( 'themify_popup_css_meta_sanitized', '1', false );
     615        }
    535616}
    536617
    … …  
    567648 */
    568649function themify_popup_get_custom_css() {
    569         $css = str_replace( '%POPUP%', '#themify-popup-' . get_the_id(), themify_popup_get( 'custom_css', '' ) );
     650        $css = themify_popup_get( 'custom_css', '' );
     651        if ( ! is_string( $css ) || '' === $css ) {
     652                return '';
     653        }
     654        $css = str_replace( '%POPUP%', '#themify-popup-' . (int) get_the_id(), $css );
    570655        if ( function_exists( 'themify_sanitize_builder_custom_css' ) ) {
    571                 return themify_sanitize_builder_custom_css( $css );
    572         }
    573         $css = preg_replace( '/<\/style/i', '', $css );
    574         $css = preg_replace( '/<script/i', '', $css );
    575         return $css;
     656                $css = themify_sanitize_builder_custom_css( $css );
     657        }
     658        return themify_popup_escape_inline_style( $css );
    576659}
     660
     661/**
     662 * CSS length. Returns an empty string when the value is not a plain number.
     663 *
     664 * @param mixed $value Raw meta value.
     665 * @return string
     666 */
     667function themify_popup_css_number( $value ) {
     668        if ( is_int( $value ) || is_float( $value ) ) {
     669                $value = (string) $value;
     670        }
     671        if ( ! is_string( $value ) ) {
     672                return '';
     673        }
     674        $value = trim( $value );
     675        if ( ! preg_match( '/^(?:\d+\.?\d*|\.\d+)$/', $value ) ) {
     676                return '';
     677        }
     678        $number = (float) $value;
     679        if ( ! is_finite( $number ) ) {
     680                return '';
     681        }
     682        $formatted = rtrim( rtrim( sprintf( '%.6F', $number ), '0' ), '.' );
     683        return '' === $formatted ? '0' : $formatted;
     684}
     685
     686/**
     687 * CSS unit restricted to an allow-list. Returns an empty string when invalid.
     688 *
     689 * @param mixed $unit    Raw meta value.
     690 * @param array $allowed Allowed units.
     691 * @return string
     692 */
     693function themify_popup_css_unit( $unit, $allowed ) {
     694        if ( ! is_string( $unit ) ) {
     695                return '';
     696        }
     697        $unit = strtolower( trim( $unit ) );
     698        return in_array( $unit, $allowed, true ) ? $unit : '';
     699}
     700
     701/**
     702 * Color value safe to place in a stylesheet.
     703 * Accepts hex, rgb/rgba/hsl/hsla, and alphabetic color keywords.
     704 *
     705 * @param mixed $color Raw meta value.
     706 * @return string
     707 */
     708function themify_popup_sanitize_css_color( $color ) {
     709        if ( ! is_string( $color ) ) {
     710                return '';
     711        }
     712        $color = trim( $color );
     713        if ( '' === $color ) {
     714                return '';
     715        }
     716        if ( preg_match( '/^#(?:[0-9a-fA-F]{3}|[0-9a-fA-F]{4}|[0-9a-fA-F]{6}|[0-9a-fA-F]{8})$/', $color ) ) {
     717                return $color;
     718        }
     719        if ( preg_match( '/^[a-zA-Z]{3,30}$/', $color ) ) {
     720                return $color;
     721        }
     722        if ( preg_match( '/^(?:rgb|rgba|hsl|hsla)\([0-9.,%\s\/]+\)$/i', $color ) ) {
     723                return $color;
     724        }
     725        return '';
     726}
     727
     728/**
     729 * Keep stylesheet text from closing the surrounding style element.
     730 *
     731 * @param mixed $css CSS text.
     732 * @return string
     733 */
     734function themify_popup_escape_inline_style( $css ) {
     735        if ( ! is_string( $css ) ) {
     736                return '';
     737        }
     738        $css = str_replace( "\0", '', $css );
     739        $css = preg_replace( '/<[^>]*>/', '', $css );
     740        return str_replace( '<', '', $css );
     741}
  • themify-popup/trunk/includes/themify-metabox/includes/themify-metabox-core.php

    r3344185 r3749602  
    161161
    162162                if( !empty( $_POST['themify_proper_save'] )) {
     163                        if ( empty( $_POST['themify_metabox_nonce'] ) || ! wp_verify_nonce( sanitize_text_field( wp_unslash( $_POST['themify_metabox_nonce'] ) ), 'themify_metabox_save' ) ) {
     164                                return $post_id;
     165                        }
     166
    163167                        foreach( $this->get_meta_boxes() as $meta_box ) {
    164168                                $tabs = $this->get_meta_box_options( $meta_box['id'], $_POST['post_type'] );
    … …  
    214218                }
    215219
     220                $new_meta = $this->sanitize_field_value( $field, $new_meta );
     221
    216222                // remove empty meta fields from database
    217223                if ( '' === $new_meta ) {
    … …  
    221227                        return;
    222228                }
    223                
    224                 /* sanitization */
    225                 if (isset( $field['type'] ) && ($field['type'] === 'textbox' || $field['type'] === 'textarea' ) && ! current_user_can( 'unfiltered_html' )) {
    226                         $new_meta = wp_kses_data( $new_meta );
    227                 }
    228                
     229
    229230                $old_meta = get_post_meta( $post_id, $field['name'], true );
    230231                if ( $new_meta !== '' && $new_meta != $old_meta ) {
    231232                        update_post_meta( $post_id, $field['name'], $new_meta );
    232233                }
     234        }
     235
     236        /**
     237         * Restrict choice, color, and text meta to values that are safe to store.
     238         *
     239         * @param array $field Field config.
     240         * @param mixed $value Raw posted value.
     241         * @return mixed
     242         */
     243        protected function sanitize_field_value( $field, $value ) {
     244                $type = isset( $field['type'] ) ? $field['type'] : '';
     245
     246                if ( in_array( $type, array( 'dropdown', 'radio', 'image_radio', 'layout', 'dropdownbutton' ), true ) ) {
     247                        if ( ! is_scalar( $value ) ) {
     248                                return '';
     249                        }
     250                        $value = (string) wp_unslash( $value );
     251                        $allowed = $this->get_choice_values( $field );
     252                        if ( $allowed ) {
     253                                return in_array( $value, $allowed, true ) ? $value : '';
     254                        }
     255                        return sanitize_text_field( $value );
     256                }
     257
     258                if ( 'color' === $type ) {
     259                        if ( ! is_scalar( $value ) ) {
     260                                return '';
     261                        }
     262                        $color = (string) wp_unslash( $value );
     263                        if ( function_exists( 'themify_popup_sanitize_css_color' ) ) {
     264                                return themify_popup_sanitize_css_color( $color );
     265                        }
     266                        $color = trim( sanitize_text_field( $color ) );
     267                        return preg_match( '/^(#[0-9a-fA-F]{3,8}|[a-zA-Z]{3,30}|(rgb|rgba|hsl|hsla)\([0-9.,%\s\/]+\))$/', $color ) ? $color : '';
     268                }
     269
     270                if ( in_array( $type, array( 'textbox', 'textarea' ), true ) && ! current_user_can( 'unfiltered_html' ) ) {
     271                        if ( ! is_scalar( $value ) ) {
     272                                return '';
     273                        }
     274                        return wp_kses_data( $value );
     275                }
     276
     277                return $value;
     278        }
     279
     280        /**
     281         * Allowed values for a choice field.
     282         *
     283         * @param array $field Field config.
     284         * @return array
     285         */
     286        protected function get_choice_values( $field ) {
     287                $type = isset( $field['type'] ) ? $field['type'] : '';
     288                $options = array();
     289                if ( 'dropdownbutton' === $type && isset( $field['states'] ) ) {
     290                        $options = $field['states'];
     291                } elseif ( isset( $field['meta'] ) ) {
     292                        $options = $field['meta'];
     293                }
     294                $first = is_array( $options ) ? reset( $options ) : null;
     295                $is_option_list = is_array( $first ) && ( isset( $first['value'] ) || isset( $first['name'] ) );
     296                if ( ! $is_option_list && is_callable( $options ) ) {
     297                        $options = call_user_func( $options );
     298                }
     299                if ( ! is_array( $options ) ) {
     300                        return array();
     301                }
     302                $allowed = array();
     303                foreach ( $options as $option ) {
     304                        if ( is_array( $option ) && isset( $option['value'] ) && is_scalar( $option['value'] ) ) {
     305                                $allowed[] = (string) $option['value'];
     306                        }
     307                }
     308                return $allowed;
    233309        }
    234310
    … …  
    271347                        echo '</ul>';
    272348                        echo '<div class="ilc-btabs themify-tabs-body">';
     349                        static $themify_metabox_nonce_sent = false;
     350                        if ( ! $themify_metabox_nonce_sent ) {
     351                                wp_nonce_field( 'themify_metabox_save', 'themify_metabox_nonce', false );
     352                                $themify_metabox_nonce_sent = true;
     353                        }
    273354                        foreach( $tabs as $tab ) {
    274355                                if( isset( $tab['display_callback'] ) && is_callable( $tab['display_callback'] ) ) {
  • themify-popup/trunk/readme.txt

    r3730038 r3749602  
    55Requires at least: 4.5
    66Tested up to: 7.1.2
    7 Stable tag: 1.4.7
     7Stable tag: 1.4.8
    88License: GPL v2
    99License URI: https://www.gnu.org/licenses/old-licenses/gpl-2.0.html
  • themify-popup/trunk/themify-popup.php

    r3730038 r3749602  
    33Plugin Name:  Themify Popup
    44Plugin URI:   https://themify.me/popup
    5 Version:      1.4.7
     5Version:      1.4.8
    66Author:       Themify
    77Description:  A free plugin to display popups for ads, newsletter subscriptions, and general info. It comes with various popup styles (classic, slide-out, fullscreen, etc.). Popups can be scheduled and configured to show on certain posts, pages, categories, and user roles.
    … …  
    1313
    1414defined( 'ABSPATH' ) or die( '-1' );
    15 const THEMIFY_POPUP_VERSION='1.4.7';
     15const THEMIFY_POPUP_VERSION='1.4.8';
    1616/**
    1717 * Bootstrap Popup plugin
  • themify-popup/trunk/views/single-popup.php

    r3634821 r3749602  
    1212</head>
    1313
    14 <body <?php body_class(); ?> style="background:#fff;width:<?php echo esc_attr( themify_popup_get( 'popup_width' ) . themify_popup_get( 'popup_width_unit', 'px' ) ); ?>;min-height:<?php echo esc_attr( themify_popup_get( 'popup_height' ) . themify_popup_get( 'popup_height_unit', 'px' ) ); ?>;margin:auto" id="themify-popup-<?php the_id(); ?>">
     14<?php
     15        $popup_width = themify_popup_css_number( themify_popup_get( 'popup_width' ) );
     16        $popup_width_unit = themify_popup_css_unit( themify_popup_get( 'popup_width_unit', 'px' ), array( 'px', '%', 'em' ) );
     17        $popup_height = themify_popup_css_number( themify_popup_get( 'popup_height' ) );
     18        $popup_height_unit = themify_popup_css_unit( themify_popup_get( 'popup_height_unit', 'px' ), array( 'px', '%' ) );
     19?>
     20<body <?php body_class(); ?> style="background:#fff;width:<?php echo esc_attr( ( '' !== $popup_width ? $popup_width : '' ) . ( '' !== $popup_width_unit ? $popup_width_unit : 'px' ) ); ?>;min-height:<?php echo esc_attr( ( '' !== $popup_height ? $popup_height : '' ) . ( '' !== $popup_height_unit ? $popup_height_unit : 'px' ) ); ?>;margin:auto" id="themify-popup-<?php the_id(); ?>">
    1521
    1622        <?php if( have_posts() ) : while( have_posts() ) : the_post(); ?>
  • themify-popup/trunk/views/styles.php

    r2916598 r3749602  
    44 *
    55 * @var $id
     6 * @var $style
    67 */
    78
     9$id = isset( $id ) ? (int) $id : 0;
     10$style = ( isset( $style ) && is_string( $style ) ) ? $style : '';
    811$styles = array();
    9 if( themify_popup_check( 'popup_width' ) ) {
    10         if( $style === 'classic' ) {
    11                 $styles[] = "body.themify-popup-showing-{$id} .mfp-wrap .mfp-inline-holder .mfp-content{width:" . themify_popup_get( 'popup_width' ) . themify_popup_get( 'popup_width_unit', 'px' ) . '!important}';
    12         } elseif( $style === 'slide-out' ) {
    13                 $styles[] = "#themify-popup-{$id}{width:" . themify_popup_get( 'popup_width' ) . themify_popup_get( 'popup_width_unit', 'px' ) . '!important}';
     12
     13if ( themify_popup_check( 'popup_width' ) ) {
     14        $width = themify_popup_css_number( themify_popup_get( 'popup_width' ) );
     15        $width_unit = themify_popup_css_unit( themify_popup_get( 'popup_width_unit', 'px' ), array( 'px', '%', 'em' ) );
     16        if ( '' === $width_unit ) {
     17                $width_unit = 'px';
     18        }
     19        if ( '' !== $width ) {
     20                $size = $width . $width_unit;
     21                if ( 'classic' === $style ) {
     22                        $styles[] = "body.themify-popup-showing-{$id} .mfp-wrap .mfp-inline-holder .mfp-content{width:{$size}!important}";
     23                } elseif ( 'slide-out' === $style ) {
     24                        $styles[] = "#themify-popup-{$id}{width:{$size}!important}";
     25                }
    1426        }
    1527}
    16 if( themify_popup_check( 'popup_height' ) && ! themify_popup_check( 'popup_auto_height' ) ) {
    17         if( $style === 'classic' ) {
    18                 $styles[] = "body.themify-popup-showing-{$id} .mfp-wrap .mfp-inline-holder .mfp-content{height:" . themify_popup_get( 'popup_height' ) . themify_popup_get( 'popup_height_unit', 'px' ) . '!important}';
    19         } elseif( $style === 'slide-out' ) {
    20                 $styles[] = "#themify-popup-{$id}{height:" . themify_popup_get( 'popup_height' ) . themify_popup_get( 'popup_height_unit', 'px' ) . '!important}';
     28if ( themify_popup_check( 'popup_height' ) && ! themify_popup_check( 'popup_auto_height' ) ) {
     29        $height = themify_popup_css_number( themify_popup_get( 'popup_height' ) );
     30        $height_unit = themify_popup_css_unit( themify_popup_get( 'popup_height_unit', 'px' ), array( 'px', '%' ) );
     31        if ( '' === $height_unit ) {
     32                $height_unit = 'px';
     33        }
     34        if ( '' !== $height ) {
     35                $size = $height . $height_unit;
     36                if ( 'classic' === $style ) {
     37                        $styles[] = "body.themify-popup-showing-{$id} .mfp-wrap .mfp-inline-holder .mfp-content{height:{$size}!important}";
     38                } elseif ( 'slide-out' === $style ) {
     39                        $styles[] = "#themify-popup-{$id}{height:{$size}!important}";
     40                }
    2141        }
    2242}
    23 if( themify_popup_check( 'popup_overlay_color' ) ) {
    24         $styles[] = "body.themify-popup-showing-{$id} .mfp-bg{background-color: " . themify_popup_get( 'popup_overlay_color' ) . '}';
     43$overlay = themify_popup_sanitize_css_color( themify_popup_get( 'popup_overlay_color' ) );
     44if ( '' !== $overlay ) {
     45        $styles[] = "body.themify-popup-showing-{$id} .mfp-bg{background-color:{$overlay}}";
    2546}
    26 $styles[] = themify_popup_get_custom_css();
     47$custom_css = themify_popup_get_custom_css();
     48if ( '' !== $custom_css ) {
     49        $styles[] = $custom_css;
     50}
    2751
    28 if( ! empty( $styles ) ) {
    29         printf( '<style>%s</style>', implode( "\n", $styles ) );
     52if ( ! empty( $styles ) ) {
     53        printf( '<style>%s</style>', themify_popup_escape_inline_style( implode( "\n", $styles ) ) );
    3054}
Note: See TracChangeset for help on using the changeset viewer.