fix: SQL injection in bulk format change, block editor preview, and PHP 8 deprecations (10.47) - #31
fix: SQL injection in bulk format change, block editor preview, and PHP 8 deprecations (10.47)#31iftakharul-islam wants to merge 2 commits into
Conversation
Security: - format_change() interpolated the submitted $format value directly into an UPDATE statement. Only sanitize_text_field() was applied, which does not escape SQL quotes, so a crafted value could inject arbitrary SQL. Confirmed exploitable. Now passed through $wpdb->prepare(), matching the prepare_in_data() treatment the same method already gives $useremails. $ids is integer-cast and an empty result now returns early instead of producing "IN ()". Correctness: - S2_Block_Editor::preview() read subscribe2_options from an undefined property on itself, so the check was always true and preview never reached publish(). Now uses the $mysubscribe2 global the method already declares, matching setting() below it. This also removes the "Undefined array key never" warning in s2-core. - S2_Frontend read $_GET['s2'] unguarded and unsanitised in two places. Now isset-guarded and sanitised. The wp_hash() verification that protects the confirmation flow is unchanged. PHP 8 compatibility: - Declared $script_debug on S2_Ajax and $email/$ip/$message on S2_Core. These were assigned dynamically, which is deprecated in 8.2 and fatal in 9. - Defaulted the substitute() properties to '' and post_count to 0, and returned '' rather than null from get_tracking_link(), which removes the stripslashes()/str_replace() null deprecations on 8.1+. - Corrected post_cat_names/post_tag_names docblocks to string; both are assigned via implode() and consumed as strings. Bumps version to 10.47. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Closes the Patchstack report tracked in #25. All three s2/v1 routes gated on edit_posts, which every role down to Contributor holds. Confirmed by testing as a real Contributor: - /resend/{id} called publish() with no preview argument, mailing the live subscriber list. A Contributor triggered a send to every active subscriber on a post they did not own. - /preview/{id} sent mail for any post id, regardless of ownership. - /settings/{setting} exposed plugin configuration (sender, tracking, subject templates) to any Contributor. Now: - resend requires the s2_capability 'send' filter (manage_options by default, matching the Send Email screen) AND edit_post on the target. - preview requires edit_post on the specific post rather than the generic edit_posts. - settings requires the s2_capability 'settings' filter. Verified: Contributor, Author and Editor all receive 403 with zero mail sent; administrator retains full access on all three routes. Also: - Declared $preview_email on S2_Core. It was assigned dynamically and surfaced as a PHP 8.2 deprecation on the preview path. - Corrected the settings route args key from 'id' to 'setting'; the validator was targeting a parameter that route does not receive. - readme: Tested up to 7.0 (#29), verified against WordPress 7.0.2. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Added: broken access control fix (closes #25)Investigated the open issues on this repo. #25 is real and I reproduced it, so the fix is now in this PR. #25 — Broken Access Control (Patchstack / chokri hammedi)All three
The Fixed:
Verified after the fix: Contributor, Author, and Editor all get Also picked up Other open issues — no code change needed
Suggest closing #17 and #16 as already-resolved, and #25 when this merges. |
Follow-ups from the 10.46 security release. Tracked in weDevsOfficial/plugin-internal-tasks#5.
Security — SQL injection in
format_change()classes/class-s2-admin.php:1207$formatcame from$_POST['format']filtered only bysanitize_text_field(), which strips tags but does not escape SQL quotes, and was then interpolated straight into anUPDATE.Confirmed exploitable before the fix — this payload executed with no DB error and wrote attacker-controlled SQL:
After the fix the same payload is inert, stored as a literal string:
Requires
manage_optionsand passes a nonce, so this is privilege escalation from a compromised or lower-trust admin rather than an unauthenticated hole — but it is a real injection. Worth noting the same method already protected$useremailsand$idsviaprepare_in_data(); only$formatskipped it, and aphpcs:ignoreon the line hid it from tooling.Also added an early return when no user IDs match, which fixes a pre-existing
IN ()syntax error on that path.Correctness — block editor preview
classes/class-s2-block-editor.php:129preview()read$this->subscribe2_options['email_freq'], butS2_Block_Editordeclares no properties, so this was alwaysnull.'never' !== nullis always true, meaning preview always took the digest branch and never reachedpublish(). It also cascaded intoUndefined array key "never"atclass-s2-core.php:2070.Now uses the
$mysubscribe2global that the method already declared but never used — matchingsetting()twenty lines below.Robustness — confirmation links
classes/class-s2-frontend.phpread$_GET['s2']unguarded and unsanitised in two places, emitting warnings on malformed or absent codes. Nowisset-guarded and sanitised. Thewp_hash()verification that actually protects the confirm/unsubscribe flow is unchanged.PHP 8 compatibility
$script_debugonS2_Ajax, and$email/$ip/$messageonS2_Core(used 11× in core and 9× inShortcodeTrait, declared nowhere). Dynamic property creation is deprecated in 8.2 and fatal in PHP 9.substitute()properties to'',post_countto0, and returned''instead ofnullfromget_tracking_link()— clearing thestripslashes()/str_replace()null deprecations on 8.1+.post_cat_names/post_tag_namesdocblocks fromarray|nulltostring; both are assigned viaimplode()and consumed as strings.Testing
Verified on a live install (WP 7.0.2 / PHP 8.2):
substitute()on a fresh object went from 9 deprecation notices to 0.new S2_Ajax()no longer warns.on*attributes); subscribe flow returns the confirmation message and writesactive=0; all four admin screens load with no fatals; confirmation links handle absent, empty, junk, and XSS-shapeds2values without warnings.php -lclean across every file in the plugin.debug.logstayed empty throughout.All test data removed; subscribers and user meta restored to their original values.
Not included
The
nojsshortcode argument deprecation from issue #5 is intentionally left out — it is public API used by the widget and Gutenberg block, so it deserves a planned deprecation cycle rather than being bundled into a fix release.