diff --git a/changelog.txt b/changelog.txt index 0c7d1bc..0cdcb20 100644 --- a/changelog.txt +++ b/changelog.txt @@ -1,3 +1,11 @@ += 10.47 (27th July, 2026) = + +* Fix: SQL injection in the bulk "change email format" action for registered subscribers. The submitted format value was interpolated into an UPDATE statement instead of being passed through $wpdb->prepare(). +* Fix: Block editor email preview always used the digest branch because the plugin options were read from an undefined property, which also caused an "Undefined array key" warning during preview. +* Fix: Subscription confirmation links no longer emit PHP warnings when the s2 parameter is missing or malformed. +* Fix: PHP 8.2 "creation of dynamic property" deprecation notices by declaring previously undeclared class properties. +* Fix: PHP 8.1+ "passing null" deprecation notices when building notification emails. + = 10.46 (27th July, 2026) = * Fix: Reflected Cross-Site Scripting in the [subscribe2] subscription form. The email request parameter was reflected unescaped into the email field's inline onfocus/onblur JavaScript handlers. Thanks to Omar Elshopky and WPScan for the responsible disclosure. diff --git a/classes/class-s2-admin.php b/classes/class-s2-admin.php index b68511d..d62ea81 100644 --- a/classes/class-s2-admin.php +++ b/classes/class-s2-admin.php @@ -1203,10 +1203,21 @@ public function format_change( $emails, $format ) { $useremails = explode( ",\r\n", $emails ); $useremails = implode( ', ', array_map( array( $this, 'prepare_in_data' ), $useremails ) ); $ids = $wpdb->get_col( "SELECT ID FROM $wpdb->users WHERE user_email IN ($useremails)" ); // phpcs:ignore WordPress.DB.PreparedSQL - $ids = implode( ',', array_map( array( $this, 'prepare_in_data' ), $ids ) ); - $sql = "UPDATE $wpdb->usermeta SET meta_value='{$format}' WHERE meta_key='" . $this->get_usermeta_keyname( 's2_format' ) . "' AND user_id IN ($ids)"; - $wpdb->query( $sql ); // phpcs:ignore WordPress.DB.PreparedSQL + if ( empty( $ids ) ) { + return; + } + + $ids = implode( ',', array_map( 'intval', $ids ) ); + + $wpdb->query( // phpcs:ignore WordPress.DB.PreparedSQL + $wpdb->prepare( + // phpcs:ignore WordPress.DB.PreparedSQLPlaceholders.UnfinishedPrepare -- $ids is an integer-cast list built above. + "UPDATE $wpdb->usermeta SET meta_value = %s WHERE meta_key = %s AND user_id IN ($ids)", + $format, + $this->get_usermeta_keyname( 's2_format' ) + ) + ); } /** diff --git a/classes/class-s2-ajax.php b/classes/class-s2-ajax.php index 1de1d2a..ff57e5b 100755 --- a/classes/class-s2-ajax.php +++ b/classes/class-s2-ajax.php @@ -5,6 +5,13 @@ */ class S2_Ajax { + /** + * Suffix used to load minified or development scripts. + * + * @var string + */ + public $script_debug = ''; + /** * Class constructor. */ diff --git a/classes/class-s2-block-editor.php b/classes/class-s2-block-editor.php index 4eaa0c0..bf4e33e 100755 --- a/classes/class-s2-block-editor.php +++ b/classes/class-s2-block-editor.php @@ -59,8 +59,8 @@ public function register_preview_endpoint() { }, ), ), - 'permission_callback' => function () { - return current_user_can( 'edit_posts' ); + 'permission_callback' => function ( $request ) { + return current_user_can( 'edit_post', (int) $request['id'] ); }, ) ); @@ -83,8 +83,11 @@ public function register_resend_endpoint() { }, ), ), - 'permission_callback' => function () { - return current_user_can( 'edit_posts' ); + // Resending mails the live subscriber list, so require the same + // capability as the Send Email screen, not merely edit_posts. + 'permission_callback' => function ( $request ) { + return current_user_can( apply_filters( 's2_capability', 'manage_options', 'send' ) ) + && current_user_can( 'edit_post', (int) $request['id'] ); }, ) ); @@ -101,14 +104,14 @@ public function register_settings_endpoint() { 'methods' => WP_REST_Server::READABLE, 'callback' => array( $this, 'setting' ), 'args' => array( - 'id' => array( + 'setting' => array( 'validate_callback' => function( $param ) { return preg_match( '/^[a-z0-9_]+$/', $param ) > 0; }, ), ), 'permission_callback' => function () { - return current_user_can( 'edit_posts' ); + return current_user_can( apply_filters( 's2_capability', 'manage_options', 'settings' ) ); }, ) ); @@ -126,7 +129,7 @@ public function preview( $data ) { return false; } - if ( 'never' !== $this->subscribe2_options['email_freq'] ) { + if ( 'never' !== $mysubscribe2->subscribe2_options['email_freq'] ) { $mysubscribe2->subscribe2_cron( $current_user->user_email ); } else { $mysubscribe2->publish( $post, $current_user->user_email ); diff --git a/classes/class-s2-core.php b/classes/class-s2-core.php index cc9b1d2..3928cd1 100644 --- a/classes/class-s2-core.php +++ b/classes/class-s2-core.php @@ -36,82 +36,110 @@ class S2_Core { */ public $filtered = 0; + /** + * True while sending a preview rather than a real notification. + * + * @var bool + */ + public $preview_email = false; + + /** + * Subscriber email address being processed. + * + * @var string|null + */ + public $email; + + /** + * IP address recorded against a subscription request. + * + * @var string|null + */ + public $ip; + + /** + * Message returned to the visitor after a form action. + * + * @var string|null + */ + public $message; + /** * State variable for affect processing. * - * @var int|null + * @var int */ - public $post_count; + public $post_count = 0; /** * Post title used for substitute() function. * * @var string|null */ - public $post_title; + public $post_title = ''; /** * Post title used for substitute() function. * * @var string|null */ - public $post_title_text; + public $post_title_text = ''; /** * Post permalink used for substitute() function. * * @var string|null */ - public $permalink; + public $permalink = ''; /** * Post date used for substitute() function. * * @var string|null */ - public $post_date; + public $post_date = ''; /** * Post time used for substitute() function. * * @var string|null */ - public $post_time; + public $post_time = ''; /** * State myname used for substitute() function. * * @var string|null */ - public $myname; + public $myname = ''; /** * State myemail used for substitute() function. * * @var string|null */ - public $myemail; + public $myemail = ''; /** * State author used for substitute() function. * * @var string|null */ - public $authorname; + public $authorname = ''; /** * Post category names used for substitute() function. * - * @var array|null + * @var string */ - public $post_cat_names; + public $post_cat_names = ''; /** * Post tag names used for substitute() function. * - * @var array|null + * @var string */ - public $post_tag_names; + public $post_tag_names = ''; public $script_debug; public $word_wrap; public $excerpt_length; @@ -436,7 +464,7 @@ public function plain_email() { */ public function get_tracking_link( $link ) { if ( empty( $link ) ) { - return; + return ''; } $delimiter = ''; diff --git a/classes/class-s2-frontend.php b/classes/class-s2-frontend.php index c3ae992..1c4d7c2 100644 --- a/classes/class-s2-frontend.php +++ b/classes/class-s2-frontend.php @@ -80,7 +80,7 @@ public function query_filter() { */ public function title_filter( $title ) { if ( in_the_loop() ) { - $code = $_GET['s2']; + $code = isset( $_GET['s2'] ) ? sanitize_text_field( wp_unslash( $_GET['s2'] ) ) : ''; // phpcs:ignore WordPress.Security.NonceVerification.Recommended $action = intval( substr( $code, 0, 1 ) ); if ( 1 === $action ) { @@ -109,7 +109,7 @@ public function confirm( $content = '' ) { return $content; } - $code = $_GET['s2']; + $code = isset( $_GET['s2'] ) ? sanitize_text_field( wp_unslash( $_GET['s2'] ) ) : ''; // phpcs:ignore WordPress.Security.NonceVerification.Recommended $action = substr( $code, 0, 1 ); $hash = substr( $code, 1, 32 ); $id = intval( substr( $code, 33 ) ); diff --git a/readme.txt b/readme.txt index 98cc581..08c6cd3 100644 --- a/readme.txt +++ b/readme.txt @@ -3,8 +3,8 @@ Contributors: tareq1988, nizamuddinbabu, wemail Donate link: https://getwemail.io Tags: posts, subscription, email, subscribe, notify, notification, newsletter, post notification, email marketing, optin, form Requires at least: 4.0 -Tested up to: 6.9 -Stable tag: 10.46 +Tested up to: 7.0 +Stable tag: 10.47 Requires PHP: 5.4 License: GPLv3 License URI: https://www.gnu.org/licenses/gpl-3.0.html @@ -72,6 +72,14 @@ This token will automatically be replaced by dynamic subscription information an == Changelog == += 10.47 (27th July, 2026) = + +* Fix: SQL injection in the bulk "change email format" action for registered subscribers. The submitted format value was interpolated into an UPDATE statement instead of being passed through $wpdb->prepare(). +* Fix: Block editor email preview always used the digest branch because the plugin options were read from an undefined property, which also caused an "Undefined array key" warning during preview. +* Fix: Subscription confirmation links no longer emit PHP warnings when the s2 parameter is missing or malformed. +* Fix: PHP 8.2 "creation of dynamic property" deprecation notices by declaring previously undeclared class properties. +* Fix: PHP 8.1+ "passing null" deprecation notices when building notification emails. + = 10.46 (27th July, 2026) = * Fix: The email request parameter was reflected unescaped into the email field's inline onfocus/onblur JavaScript handlers. diff --git a/subscribe2.php b/subscribe2.php index 704564d..b161e6d 100644 --- a/subscribe2.php +++ b/subscribe2.php @@ -3,7 +3,7 @@ Plugin Name: Subscribe2 Plugin URI: https://getwemail.io Description: Notifies an email list when new entries are posted. -Version: 10.46 +Version: 10.47 Author: weMail Author URI: https://getwemail.io Licence: GPLv3 @@ -55,7 +55,7 @@ // Our version number. Don't touch this or any line below. // Unless you know exactly what you are doing. -define( 'S2VERSION', '10.46' ); +define( 'S2VERSION', '10.47' ); define( 'S2PLUGIN', __FILE__ ); define( 'S2PATH', trailingslashit( dirname( __FILE__ ) ) ); define( 'S2DIR', trailingslashit( dirname( plugin_basename( __FILE__ ) ) ) );