diff --git a/core/includes/class-orbit-fox.php b/core/includes/class-orbit-fox.php index 0dbdb56f..c9ab36e8 100644 --- a/core/includes/class-orbit-fox.php +++ b/core/includes/class-orbit-fox.php @@ -175,6 +175,8 @@ public function get_version() { */ private function define_hooks() { + $this->loader->add_action( 'obfx_sanitize_html_tag', $this, 'sanitize_html_tag', 10, 3 ); + $plugin_admin = new Orbit_Fox_Admin( $this->get_plugin_name(), $this->get_version() ); $this->loader->add_action( 'admin_init', $plugin_admin, 'load_modules' ); @@ -232,4 +234,17 @@ public function run() { $this->loader->run(); } + /** + * Sanitize html tags. + * + * @param string $tag HTML tag name. + * @param array $allowed Allowed HTML tags. + * @param string $default Default HTML tag. + * + * @return string + */ + public function sanitize_html_tag( $tag, $allowed = array( 'h1', 'h2', 'h3', 'h4', 'h5', 'h6', 'p' ), $default = 'h2' ) { + return in_array( $tag, $allowed, true ) ? $tag : $default; + } + } diff --git a/obfx_modules/beaver-widgets/modules/post-grid/includes/frontend.php b/obfx_modules/beaver-widgets/modules/post-grid/includes/frontend.php index 67aac133..e96d5669 100644 --- a/obfx_modules/beaver-widgets/modules/post-grid/includes/frontend.php +++ b/obfx_modules/beaver-widgets/modules/post-grid/includes/frontend.php @@ -60,7 +60,7 @@ function obfx_show_post_grid_title( $settings ) { if ( ! empty( $settings->show_title_link ) && $settings->show_title_link === 'yes' ) { echo ''; } - $tag = ! empty( $settings->title_tag ) ? $settings->title_tag : 'h4'; + $tag = ! empty( $settings->title_tag ) ? apply_filters( 'obfx_sanitize_html_tag', $settings->title_tag, array( 'h1', 'h2', 'h3', 'h4', 'h5', 'h6', 'p', 'span', 'div' ), 'h5' ) : 'h5'; the_title( '<' . $tag . ' class="obfx-post-grid-title">', '' ); if ( ! empty( $settings->show_title_link ) && $settings->show_title_link === 'yes' ) { echo ''; diff --git a/obfx_modules/beaver-widgets/modules/pricing-table/includes/frontend.php b/obfx_modules/beaver-widgets/modules/pricing-table/includes/frontend.php index 8a823c19..e3abc1e9 100644 --- a/obfx_modules/beaver-widgets/modules/pricing-table/includes/frontend.php +++ b/obfx_modules/beaver-widgets/modules/pricing-table/includes/frontend.php @@ -9,10 +9,13 @@ $class_to_add = $settings->card_layout === 'yes' ? 'obfx-card' : ''; +$plan_title_tag = ! empty( $settings->plan_title_tag ) ? apply_filters( 'obfx_sanitize_html_tag', $settings->plan_title_tag ) : 'h2'; +$plan_subtitle_tag = ! empty( $settings->plan_subtitle_tag ) ? apply_filters( 'obfx_sanitize_html_tag', $settings->plan_subtitle_tag, array( 'h1', 'h2', 'h3', 'h4', 'h5', 'h6', 'p' ), 'p' ) : 'p'; + echo '
'; echo '
'; - echo '<' . esc_attr( $settings->plan_title_tag ) . ' class="obfx-plan-title text-center">' . wp_kses_post( $settings->plan_title ) . 'plan_title_tag ) . '>'; - echo '<' . esc_attr( $settings->plan_subtitle_tag ) . ' class="obfx-plan-subtitle text-center">' . wp_kses_post( $settings->plan_subtitle ) . 'plan_subtitle_tag ) . '>'; + echo '<' . esc_html( $plan_title_tag ) . ' class="obfx-plan-title text-center">' . wp_kses_post( $settings->plan_title ) . ''; + echo '<' . esc_html( $plan_subtitle_tag ) . ' class="obfx-plan-subtitle text-center">' . wp_kses_post( $settings->plan_subtitle ) . ''; echo '
'; echo '
'; switch ( $settings->currency_position ) { diff --git a/obfx_modules/elementor-extra-widgets/widgets/elementor/pricing-table.php b/obfx_modules/elementor-extra-widgets/widgets/elementor/pricing-table.php index 113b558a..81ade2ee 100644 --- a/obfx_modules/elementor-extra-widgets/widgets/elementor/pricing-table.php +++ b/obfx_modules/elementor-extra-widgets/widgets/elementor/pricing-table.php @@ -1048,7 +1048,7 @@ protected function render() { $output .= '
'; if ( ! empty( $settings['title'] ) ) { // Start of title tag. - $title_tag = $this->sanitize_tag( $settings['title_tag'] ); + $title_tag = apply_filters( 'obfx_sanitize_html_tag', $settings['title_tag'], array( 'h1', 'h2', 'h3', 'h4', 'h5', 'h6', 'p' ), 'h3' ); $output .= '<' . esc_html( $title_tag ) . ' ' . $this->get_render_attribute_string( 'title' ) . '>'; // Title string. @@ -1059,7 +1059,7 @@ protected function render() { } if ( ! empty( $settings['subtitle'] ) ) { // Start of subtitle tag. - $subtitle_tag = $this->sanitize_tag( $settings['subtitle_tag'] ); + $subtitle_tag = apply_filters( 'obfx_sanitize_html_tag', $settings['subtitle_tag'], array( 'h1', 'h2', 'h3', 'h4', 'h5', 'h6', 'p' ), 'p' ); $output .= '<' . esc_html( $subtitle_tag ) . ' ' . $this->get_render_attribute_string( 'subtitle' ) . '>'; // Subtitle string. @@ -1168,16 +1168,5 @@ private function display_button_icon( $settings ) { } return $output; } - - /** - * Sanitize html tags. - * - * @param string $tag HTML tagname. - * - * @return string - */ - private function sanitize_tag( $tag ) { - return in_array( $tag, array( 'h1', 'h2', 'h3', 'h4', 'h5', 'h6', 'p' ), true ) ? $tag : 'h1'; - } } diff --git a/tests/test-module-beaver-widgets.php b/tests/test-module-beaver-widgets.php new file mode 100644 index 00000000..e48b4ee5 --- /dev/null +++ b/tests/test-module-beaver-widgets.php @@ -0,0 +1,132 @@ +pricing_table_template = dirname( dirname( __FILE__ ) ) . '/obfx_modules/beaver-widgets/modules/pricing-table/includes/frontend.php'; + } + + /** + * Render the pricing table frontend template. + * + * @param array $args Settings to overwrite the defaults with. + * + * @return string + */ + protected function render_pricing_table( $args = array() ) { + $settings = (object) array_merge( + array( + 'card_layout' => 'yes', + 'plan_title' => 'Plan title', + 'plan_title_tag' => 'h2', + 'plan_subtitle' => 'Plan subtitle', + 'plan_subtitle_tag' => 'p', + 'price' => '50', + 'currency' => '$', + 'currency_position' => 'after', + 'period' => '/mo', + 'features' => array(), + 'text' => 'Buy now', + 'link' => 'https://example.com', + ), + $args + ); + + ob_start(); + include $this->pricing_table_template; + + return ob_get_clean(); + } + + /** + * Supported tags should be returned unchanged. + * + * @covers Orbit_Fox::sanitize_html_tag + */ + public function test_sanitize_html_tag_keeps_supported_tags() { + foreach ( array( 'h1', 'h2', 'h3', 'h4', 'h5', 'h6', 'p' ) as $tag ) { + $this->assertEquals( $tag, apply_filters( 'obfx_sanitize_html_tag', $tag ) ); + } + } + + /** + * Anything that is not a supported tag should fall back to a safe default. + * + * @covers Orbit_Fox::sanitize_html_tag + */ + public function test_sanitize_html_tag_falls_back_for_unsupported_tags() { + $unsupported = array( + 'h1 onmouseover=alert(document.cookie)', + 'p onclick=alert(1)', + 'h2 class="evil"', + 'script', + 'div', + 'H1', + '', + ); + + foreach ( $unsupported as $tag ) { + $this->assertEquals( 'h2', apply_filters( 'obfx_sanitize_html_tag', $tag ), 'Unsupported tag: ' . $tag ); + } + } + + /** + * Callers can narrow the allowed tags and pick their own fallback. + * + * @covers Orbit_Fox::sanitize_html_tag + */ + public function test_sanitize_html_tag_honours_custom_allowed_list_and_default() { + $allowed = array( 'h3', 'h4' ); + + $this->assertEquals( 'h3', apply_filters( 'obfx_sanitize_html_tag', 'h3', $allowed, 'p' ) ); + $this->assertEquals( 'p', apply_filters( 'obfx_sanitize_html_tag', 'h1', $allowed, 'p' ) ); + $this->assertEquals( 'p', apply_filters( 'obfx_sanitize_html_tag', 'script', $allowed, 'p' ) ); + } + + /** + * The pricing table should render the configured heading tags. + */ + public function test_pricing_table_renders_configured_tags() { + $output = $this->render_pricing_table( + array( + 'plan_title_tag' => 'h3', + 'plan_subtitle_tag' => 'h4', + ) + ); + + $this->assertStringContainsString( '

Plan title

', $output ); + $this->assertStringContainsString( '

Plan subtitle

', $output ); + } + + /** + * The pricing table should reject unsafe heading tags and fall back to its defaults. + */ + public function test_pricing_table_sanitizes_unsupported_tags() { + $output = $this->render_pricing_table( + array( + 'plan_title_tag' => 'h3 onmouseover=alert(1)', + 'plan_subtitle_tag' => 'script', + ) + ); + + $this->assertStringContainsString( '

Plan title

', $output ); + $this->assertStringContainsString( '

Plan subtitle

', $output ); + $this->assertStringNotContainsString( 'onmouseover', $output ); + $this->assertStringNotContainsString( 'loader = $this->createMock('Orbit_Fox_Loader');