Plugin Directory

Changeset 3362049


Ignore:
Timestamp:
09/15/2025 08:08:20 PM (7 months ago)
Author:
museai
Message:

Release version 0.5. Prevent XSS vulnerabilities and enhance parameter flexibility.

Location:
muse-ai
Files:
3 added
2 edited

Legend:

Unmodified
Added
Removed
  • muse-ai/trunk/muse-ai.php

    r2987496 r3362049  
    33 * Plugin Name: muse.ai
    44 * Description: Enable oEmbed and shortcode support for muse.ai video embedding.
    5  * Version: 0.4
     5 * Version: 0.5
    66 * Author: muse.ai
    77 * Author URI: https://muse.ai
     
    1111add_action('elementor/widgets/register', 'museai_elementor');
    1212add_shortcode('muse-ai', 'museai_shortcode_video');
     13
     14function museai_sanitize_css($css_input) {
     15    $css_input = trim($css_input);
     16    if (empty($css_input)) {
     17        return '';
     18    }
     19
     20    // Whitelist of safe CSS properties.
     21    $allowed_properties = [
     22        'width', 'height', 'max-width', 'max-height', 'min-width', 'min-height',
     23        'margin', 'margin-top', 'margin-bottom', 'margin-left', 'margin-right',
     24        'padding', 'padding-top', 'padding-bottom', 'padding-left', 'padding-right',
     25        'border', 'border-width', 'border-style', 'border-color', 'border-radius',
     26        'background', 'background-color', 'background-image', 'background-size', 'background-position',
     27        'color', 'font-size', 'font-family', 'font-weight', 'font-style',
     28        'text-align', 'text-decoration', 'line-height', 'letter-spacing',
     29        'opacity', 'visibility', 'display', 'position', 'top', 'bottom', 'left', 'right',
     30        'z-index', 'overflow', 'box-shadow', 'text-shadow'
     31    ];
     32
     33    // Remove dangerous content.
     34    $dangerous_patterns = [
     35        '/javascript:/i',
     36        '/expression\s*\(/i',
     37        '/url\s*\(\s*["\']?\s*data:/i',
     38        '/import/i',
     39        '/@import/i',
     40        '/behavior:/i',
     41        '/binding:/i',
     42        '/mozbinding:/i',
     43        '/vbscript:/i',
     44        '/livescript:/i'
     45    ];
     46
     47    foreach ($dangerous_patterns as $pattern) {
     48        $css_input = preg_replace($pattern, '', $css_input);
     49    }
     50
     51    // Parse CSS declarations.
     52    $declarations = explode(';', $css_input);
     53    $safe_css = [];
     54
     55    foreach ($declarations as $declaration) {
     56        $declaration = trim($declaration);
     57        if (empty($declaration)) continue;
     58
     59        // Split property and value
     60        $parts = explode(':', $declaration, 2);
     61        if (count($parts) !== 2) continue;
     62
     63        $property = trim(strtolower($parts[0]));
     64        $value = trim($parts[1]);
     65
     66        // Check if property is allowed
     67        if (!in_array($property, $allowed_properties)) continue;
     68
     69        // Sanitize value
     70        $value = museai_sanitize_css_value($property, $value);
     71        if ($value === false) continue;
     72
     73        $safe_css[] = $property . ': ' . $value;
     74    }
     75
     76    return implode('; ', $safe_css);
     77}
     78
     79function museai_sanitize_css_value($property, $value) {
     80    // Remove quotes and normalize.
     81    $value = trim($value, '"\'');
     82    $value = trim($value);
     83
     84    // Block dangerous patterns in values.
     85    $dangerous_patterns = [
     86        '/javascript:/i',
     87        '/expression/i',
     88        '/url\s*\(\s*["\']?\s*data:/i',
     89        '/url\s*\(\s*["\']?\s*javascript:/i',
     90        '/vbscript:/i'
     91    ];
     92
     93    foreach ($dangerous_patterns as $pattern) {
     94        if (preg_match($pattern, $value)) {
     95            return false;
     96        }
     97    }
     98
     99    // Property-specific validation.
     100    switch ($property) {
     101        case 'width':
     102        case 'height':
     103        case 'max-width':
     104        case 'max-height':
     105        case 'min-width':
     106        case 'min-height':
     107            // Allow px, %, em, rem, vh, vw
     108            if (!preg_match('/^(auto|inherit|\d+(\.\d+)?(px|%|em|rem|vh|vw))$/', $value)) {
     109                return false;
     110            }
     111            break;
     112
     113        case 'color':
     114        case 'background-color':
     115        case 'border-color':
     116            // Allow hex colors, rgb, rgba, named colors
     117            if (!preg_match('/^(#[0-9a-f]{3,6}|rgb\(\s*\d+\s*,\s*\d+\s*,\s*\d+\s*\)|rgba\(\s*\d+\s*,\s*\d+\s*,\s*\d+\s*,\s*[\d.]+\s*\)|transparent|inherit|[a-z]+)$/i', $value)) {
     118                return false;
     119            }
     120            break;
     121
     122        case 'font-size':
     123            if (!preg_match('/^(\d+(\.\d+)?(px|pt|em|rem|%)|small|medium|large|x-large|xx-large)$/i', $value)) {
     124                return false;
     125            }
     126            break;
     127
     128        case 'z-index':
     129            if (!preg_match('/^(\d+|auto|inherit)$/', $value)) {
     130                return false;
     131            }
     132            break;
     133
     134        case 'opacity':
     135            if (!preg_match('/^(0(\.\d+)?|1(\.0+)?|\.\d+)$/', $value)) {
     136                return false;
     137            }
     138            break;
     139
     140        default:
     141            // General sanitization - remove suspicious content
     142            if (preg_match('/[<>{}\\\\]/', $value)) {
     143                return false;
     144            }
     145            // Limit length
     146            if (strlen($value) > 100) {
     147                return false;
     148            }
     149    }
     150
     151    return $value;
     152}
    13153
    14154function museai_init() {
     
    19159function museai_shortcode_video( $atts = [] ) {
    20160    $embed_id = bin2hex(random_bytes(16));
     161
     162    // Sanitize and validate all inputs with proper whitelisting.
    21163    $video_id = preg_replace('/[^a-z0-9]/i', '', $atts['id'] ?? '');
    22164    $width = preg_replace('/[^0-9%]/', '', $atts['width'] ?? '100%');
    23     $volume = (int) preg_replace('/[^0-9]/', '', $atts['volume'] ?? '50');
     165    $volume = max(0, min(100, (int) preg_replace('/[^0-9]/', '', $atts['volume'] ?? '50')));
    24166    $autoplay = (int) preg_replace('/[^01]/', '', $atts['autoplay'] ?? '0');
    25     $title = (int) preg_replace('/[^01]/', '', $atts['title'] ?? '1');
    26     $style = preg_replace('/"/', '', $atts['style'] ?? 'full');
    27     $start = (float) preg_replace('/[^0-9.]/', '', $atts['start'] ?? '0');
     167
     168    // Handle title parameter: can be 0, 1, or custom string
     169    $title_input = trim($atts['title'] ?? '1');
     170    $title_lower = strtolower($title_input);
     171    if ($title_lower === '0' || $title_lower === 'false') {
     172        $title = 0;
     173    } elseif ($title_lower === '1' || $title_lower === 'true') {
     174        $title = 1;
     175    } else {
     176        // Custom title string - sanitize it.
     177        $title = substr(sanitize_text_field($title_input), 0, 200);
     178    }
     179
     180    // Whitelist allowed styles.
     181    $allowed_styles = ['full', 'minimal', 'audio', 'cover'];
     182    $style = in_array($atts['style'] ?? 'full', $allowed_styles) ? ($atts['style'] ?? 'full') : 'full';
     183
     184    $start = max(0, (float) preg_replace('/[^0-9.]/', '', $atts['start'] ?? '0'));
    28185    $loop = (int) preg_replace('/[^01]/', '', $atts['loop'] ?? '0');
    29186    $resume = (int) preg_replace('/[^01]/', '', $atts['resume'] ?? '0');
    30     $align = preg_replace('/"/', '', $atts['align'] ?? '');
    31     $playpos = preg_replace('/"/', '', $atts['cover_play_position'] ?? 'bottom-left');
    32     $cta = addslashes(($atts['cta'] ?? '') ? $atts['cta'] : '');
    33     $css = preg_replace('/"/', '\'', $atts['css'] ?? '');
    34     $subtitles = preg_replace('/"/', '', $atts['subtitles'] ?? '');
    35     $locale = preg_replace('/"/', '', $atts['locale'] ?? '');
     187
     188    // Whitelist allowed align values.
     189    $allowed_aligns = ['left', 'center', 'right', ''];
     190    $align = in_array($atts['align'] ?? '', $allowed_aligns) ? ($atts['align'] ?? '') : '';
     191
     192    // Whitelist allowed cover play positions.
     193    $allowed_positions = ['top-left', 'top-right', 'bottom-left', 'bottom-right', 'center'];
     194    $playpos = in_array($atts['cover_play_position'] ?? 'bottom-left', $allowed_positions) ? ($atts['cover_play_position'] ?? 'bottom-left') : 'bottom-left';
     195
     196    // Handle CTA parameter - can be JSON object or empty.
     197    $cta_input = trim($atts['cta'] ?? '');
     198    $cta = '';
     199
     200    if (!empty($cta_input)) {
     201        // Try to decode as JSON.
     202        $cta_decoded = json_decode($cta_input, true);
     203
     204        if (json_last_error() === JSON_ERROR_NONE && is_array($cta_decoded)) {
     205            // Sanitize each allowed field.
     206            $sanitized_cta = array();
     207
     208            // Sanitize time field - must be 'end' or positive number.
     209            if (isset($cta_decoded['time'])) {
     210                if ($cta_decoded['time'] === 'end') {
     211                    $sanitized_cta['time'] = 'end';
     212                } elseif (is_numeric($cta_decoded['time']) && $cta_decoded['time'] >= 0) {
     213                    $sanitized_cta['time'] = (float) $cta_decoded['time'];
     214                }
     215            }
     216
     217            // Sanitize text fields - strip dangerous characters.
     218            if (isset($cta_decoded['title']) && is_string($cta_decoded['title'])) {
     219                $sanitized_cta['title'] = substr(sanitize_text_field($cta_decoded['title']), 0, 200);
     220            }
     221
     222            if (isset($cta_decoded['text']) && is_string($cta_decoded['text'])) {
     223                $sanitized_cta['text'] = substr(sanitize_textarea_field($cta_decoded['text']), 0, 500);
     224            }
     225
     226            if (isset($cta_decoded['button']) && is_string($cta_decoded['button'])) {
     227                $sanitized_cta['button'] = substr(sanitize_text_field($cta_decoded['button']), 0, 100);
     228            }
     229
     230            // Sanitize link field - must be valid URL.
     231            if (isset($cta_decoded['link']) && is_string($cta_decoded['link'])) {
     232                $link_trimmed = trim($cta_decoded['link']);
     233                if (filter_var($link_trimmed, FILTER_VALIDATE_URL) &&
     234                    (strpos($link_trimmed, 'http://') === 0 || strpos($link_trimmed, 'https://') === 0)) {
     235                    $sanitized_cta['link'] = esc_url_raw($link_trimmed);
     236                }
     237            }
     238
     239            // Only use CTA if we have at least some valid data.
     240            if (!empty($sanitized_cta)) {
     241                $cta = $sanitized_cta;
     242            }
     243        }
     244    }
     245
     246    // Sanitize CSS with strict whitelist approach.
     247    $css = museai_sanitize_css($atts['css'] ?? '');
     248
     249    // Whitelist language codes for subtitles and locale.
     250    $subtitles = preg_replace('/[^a-z\-]/', '', strtolower($atts['subtitles'] ?? ''));
     251    $locale = preg_replace('/[^a-z\-]/', '', strtolower($atts['locale'] ?? ''));
     252
    36253    $download = (int) preg_replace('/[^01]/', '', $atts['download'] ?? '0');
    37254    $playlist = preg_replace('/[^a-z0-9,]/i', '', $atts['playlist'] ?? '');
    38255
    39     $links = preg_replace('/"/', '', $atts['links'] ?? '1');
    40     $links = $links == '0' || $links == '1' ? (int) $links : sprintf('"%s"', $links);
    41 
    42     $logo = preg_replace('/"/', '', $atts['logo'] ?? '1');
    43     $logo = $logo == '0' || $logo == '1' ? (int) $logo : sprintf('"%s"', $logo);
    44 
    45     $search = preg_replace('/"/', '', $atts['search'] ?? '1');
    46     $search = $search == '0' || $search == '1' ? (int) $search : sprintf('"%s"', $search);
     256    // Handle links parameter: can be 0, 1, true, false, or URL.
     257    $links_input = trim($atts['links'] ?? '1');
     258    $links_lower = strtolower($links_input);
     259    if ($links_lower === '0' || $links_lower === 'false') {
     260        $links = 0;
     261    } elseif ($links_lower === '1' || $links_lower === 'true') {
     262        $links = 1;
     263    } elseif (filter_var($links_input, FILTER_VALIDATE_URL) && (strpos($links_input, 'http://') === 0 || strpos($links_input, 'https://') === 0)) {
     264        $links = esc_url_raw($links_input);
     265    } else {
     266        $links = 1; // Default fallback.
     267    }
     268
     269    // Handle logo parameter: can be 0, 1, true, false, or URL.
     270    $logo_input = trim($atts['logo'] ?? '1');
     271    $logo_lower = strtolower($logo_input);
     272    if ($logo_lower === '0' || $logo_lower === 'false') {
     273        $logo = 0;
     274    } elseif ($logo_lower === '1' || $logo_lower === 'true') {
     275        $logo = 1;
     276    } elseif (filter_var($logo_input, FILTER_VALIDATE_URL) && (strpos($logo_input, 'http://') === 0 || strpos($logo_input, 'https://') === 0)) {
     277        $logo = esc_url_raw($logo_input);
     278    } else {
     279        $logo = 1; // Default fallback.
     280    }
     281
     282    // Handle search parameter: only boolean.
     283    $search_raw = preg_replace('/[^a-z0-9]/', '', strtolower($atts['search'] ?? '1'));
     284    $search = ($search_raw == '0' || $search_raw == '1') ? (int) $search_raw : 1;
     285
     286    // Build JavaScript configuration array with proper escaping
     287    $config = array(
     288        'container' => '#museai-player-' . $embed_id,
     289        'video' => $video_id,
     290        'width' => $width,
     291        'links' => $links,
     292        'logo' => $logo,
     293        'search' => $search,
     294        'autoplay' => $autoplay,
     295        'volume' => $volume,
     296        'title' => $title,
     297        'style' => $style,
     298        'start' => $start,
     299        'loop' => $loop,
     300        'resume' => $resume,
     301        'align' => $align,
     302        'coverPlayPosition' => $playpos,
     303        'cta' => $cta,
     304        'subtitles' => $subtitles,
     305        'locale' => $locale,
     306        'download' => $download,
     307        'playlist' => $playlist
     308    );
     309
     310    // Use wp_json_encode for safe JavaScript output
     311    $config_json = wp_json_encode($config, JSON_HEX_TAG | JSON_HEX_AMP | JSON_HEX_APOS | JSON_HEX_QUOT);
    47312
    48313    $out = sprintf(
    49         '<div id="museai-player-%s"></div>'.
    50         '<script>'.
    51         'MusePlayer({'.
    52         'container: "#museai-player-%1$s", video: "%s", '.
    53         'width: "%s", links: %s, logo: %s, search: %s, autoplay: %s, '.
    54         'volume: %s, title: %s, style: "%s", start: %s, loop: %s, '.
    55         'resume: %s, align: "%s", coverPlayPosition: "%s", cta: "%s", '.
    56         'css: "%s", subtitles: "%s", locale: "%s", download: %s, '.
    57         'playlist: "%s"'.
    58         '})'.
    59         '</script>',
    60         $embed_id,
    61         $video_id,
    62         $width,
    63         $links,
    64         $logo,
    65         $search,
    66         $autoplay,
    67         $volume,
    68         $title,
    69         $style,
    70         $start,
    71         $loop,
    72         $resume,
    73         $align,
    74         $playpos,
    75         $cta,
    76         $css,
    77         $subtitles,
    78         $locale,
    79         $download,
    80         $playlist
     314        '<div id="museai-player-%s"></div><script>MusePlayer(%s);</script>',
     315        esc_attr($embed_id),
     316        $config_json
    81317    );
    82318    return $out;
     
    121357            );
    122358            $this->add_control(
     359                'logo',
     360                [
     361                    'label' => esc_html__( 'Logo', 'elementor-museai-widget' ),
     362                    'type' => \Elementor\Controls_Manager::TEXT,
     363                    'input_type' => 'text',
     364                    'placeholder' => esc_html__( 'Custom logo or yes/no', 'elementor-museai-widget' ),
     365                    'default' => 'yes',
     366                    'description' => esc_html__( 'Enter "yes" to show default logo, "no" to hide, or custom url to png', 'elementor-museai-widget' ),
     367                ]
     368            );
     369            $this->add_control(
    123370                'title',
    124371                [
    125372                    'label' => esc_html__( 'Title', 'elementor-museai-widget' ),
    126373                    'type' => \Elementor\Controls_Manager::SWITCHER,
    127                     'label_on' => 'show',
    128                     'label_off' => 'hide',
    129                     'default' => 'yes',
    130                 ]
    131             );
    132             $this->add_control(
    133                 'logo',
    134                 [
    135                     'label' => esc_html__( 'Logo', 'elementor-museai-widget' ),
    136                     'type' => \Elementor\Controls_Manager::SWITCHER,
    137                     'label_on' => 'show',
    138                     'label_off' => 'hide',
     374                    'label_on' => 'yes',
     375                    'label_off' => 'no',
    139376                    'default' => 'yes',
    140377                ]
     
    185422            $settings = $this->get_settings_for_display();
    186423            $embed_id = bin2hex(random_bytes(16));
    187             $html = wp_oembed_get( $settings['video_id'] );
    188 
    189             $video_id = $settings['video_id'];
    190             $links = $settings['links'] == 'yes' ? 'true' : 'false';
    191             $logo = $settings['logo'] == 'yes' ? '1' : '0';
    192             $search = $settings['search'] == 'yes' ? '1' : '0';
    193             $title = $settings['title'] == 'yes' ? '1' : '0';
    194             $autoplay = $settings['autoplay'] == 'yes' ? '1' : '0';
    195             $volume = $settings['mute'] == 'yes' ? '0' : '100';
     424
     425            // Sanitize video ID - only allow alphanumeric characters.
     426            $video_id = preg_replace('/[^a-z0-9]/i', '', $settings['video_id'] ?? '');
     427
     428            // Handle links parameter: can be boolean or URL in Elementor.
     429            $links_setting = $settings['links'] ?? 'yes';
     430            if ($links_setting === 'yes') {
     431                $links = 1;
     432            } elseif ($links_setting === 'no') {
     433                $links = 0;
     434            } elseif (filter_var($links_setting, FILTER_VALIDATE_URL) && (strpos($links_setting, 'http://') === 0 || strpos($links_setting, 'https://') === 0)) {
     435                $links = esc_url_raw($links_setting);
     436            } else {
     437                $links = 1; // Default fallback
     438            }
     439
     440            // Handle logo parameter: can be boolean or URL in Elementor.
     441            $logo_setting = $settings['logo'] ?? 'yes';
     442            if ($logo_setting === 'yes') {
     443                $logo = 1;
     444            } elseif ($logo_setting === 'no') {
     445                $logo = 0;
     446            } elseif (filter_var($logo_setting, FILTER_VALIDATE_URL) && (strpos($logo_setting, 'http://') === 0 || strpos($logo_setting, 'https://') === 0)) {
     447                $logo = esc_url_raw($logo_setting);
     448            } else {
     449                $logo = 1; // Default fallback
     450            }
     451            $search = ($settings['search'] ?? 'yes') == 'yes' ? 1 : 0;
     452
     453            // Handle title parameter: can be boolean or custom string in Elementor.
     454            $title_setting = $settings['title'] ?? 'yes';
     455            if ($title_setting === 'yes') {
     456                $title = 1;
     457            } elseif ($title_setting === 'no') {
     458                $title = 0;
     459            } else {
     460                // Custom title string - sanitize it
     461                $title = substr(sanitize_text_field($title_setting), 0, 200);
     462            }
     463
     464            $autoplay = ($settings['autoplay'] ?? 'no') == 'yes' ? 1 : 0;
     465            $volume = ($settings['mute'] ?? 'no') == 'yes' ? 0 : 100;
     466
     467            // Build secure configuration array.
     468            $config = array(
     469                'container' => '#museai-player-' . $embed_id,
     470                'video' => $video_id,
     471                'links' => $links,
     472                'logo' => $logo,
     473                'search' => $search,
     474                'autoplay' => $autoplay,
     475                'volume' => $volume,
     476                'title' => $title
     477            );
     478
     479            // Use wp_json_encode for safe JavaScript output.
     480            $config_json = wp_json_encode($config, JSON_HEX_TAG | JSON_HEX_AMP | JSON_HEX_APOS | JSON_HEX_QUOT);
    196481
    197482            $out = sprintf(
    198                 '<div id="museai-player-%s"></div>'.
    199                 '<script>'.
    200                 'MusePlayer({'.
    201                 'container: "#museai-player-%1$s", video: "%s", '.
    202                 'links: %s, logo: %s, search: %s, autoplay: %s, volume: %s, title: %s'.
    203                 '})'.
    204                 '</script>',
    205                 $embed_id,
    206                 $video_id,
    207                 $links,
    208                 $logo,
    209                 $search,
    210                 $autoplay,
    211                 $volume,
    212                 $title,
     483                '<div id="museai-player-%s"></div><script>MusePlayer(%s);</script>',
     484                esc_attr($embed_id),
     485                $config_json
    213486            );
    214487            echo $out;
  • muse-ai/trunk/readme.txt

    r2987496 r3362049  
    2727If you would like more control, you can use shortcodes. For example:
    2828
    29 `[muse-ai id="VBdrD8v" width="100%" title="0"]`
     29`[muse-ai id="VBdrD8v" width="100%" title="0" logo="https://tinyurl.com/yourLogoPNG"]`
    3030
    3131== Changelog ==
     32
     33= 0.5 =
     34* Sanitize and escape inputs to prevent CVE-2025-6262.
     35* Improvements to Elementor editor to allow custom logo.
     36* Added experimental extensions fo custom css and cta.
    3237
    3338= 0.4.1 =
Note: See TracChangeset for help on using the changeset viewer.