Release 4.4.0 - #1973
Merged
Merged
Conversation
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Bumps [squizlabs/php_codesniffer](https://github.com/PHPCSStandards/PHP_CodeSniffer) from 3.10.2 to 3.13.6. - [Release notes](https://github.com/PHPCSStandards/PHP_CodeSniffer/releases) - [Changelog](https://github.com/PHPCSStandards/PHP_CodeSniffer/blob/4.x/CHANGELOG-3.x.md) - [Commits](PHPCSStandards/PHP_CodeSniffer@3.10.2...3.13.6) --- updated-dependencies: - dependency-name: squizlabs/php_codesniffer dependency-version: 3.13.6 dependency-type: indirect ... Signed-off-by: dependabot[bot] <support@github.com>
…odesniffer-3.13.6 chore(deps-dev): bump squizlabs/php_codesniffer from 3.10.2 to 3.13.6
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
…ls (#1958) * fix: enforce network boundaries on multisite record reads and settings writes Addresses authorization findings from a security review of v4.3.0. Multisite record isolation (Network): Network::network_query_args() preserved any numeric blog_id coming from the request, but a numeric type check is not an authorization check and the Stream tables are shared across the network -- a site-level view_stream user could read another site's activity with ?blog_id=N. Requested blog IDs are now honoured only for users with manage_network_options; everyone else is pinned to the current blog. The read abilities (get-record, get-records, purge-records) already carried equivalent guards; this closes the legacy list-table and AJAX path. ajax_network_admin() also derived network-admin authority purely from an HTTP_REFERER prefix, which is caller-controlled. That let a site user lift the per-blog query restriction and, via blog_id_logged(), record their actions against blog_id 0 and corrupt site attribution. The Referer is now treated as a UI hint only and must be accompanied by a real network capability. WP-CLI is exempt since it has shell-level access and normally no logged-in user. Network-wide settings writes (Ability): update-settings and create-exclusion-rule inherited the default permission callback (WP_STREAM_SETTINGS_CAPABILITY, i.e. manage_options) while Settings::update_all_setting_values() routes to update_site_option() on network-activated installs. A site administrator could therefore change network-wide retention, role access, and audit exclusion rules. Both now use Ability::can_write_settings(), which additionally requires manage_network_options when the write will be network-scoped. GHCR publishing (docker-images.yml): The publish step was guarded by contains(github.ref_name, 'master'), which also matches unprotected branch names such as feature-master-publish. Replaced with an exact ref comparison. This guards against an accidental publish only. The condition is read from the pushed ref's own copy of the workflow, so someone with write access could still edit it on their own branch; closing that requires a deployment branch policy configured in repository settings, which is outside this change. * test: cover multisite record isolation and network settings authorization Adds regression coverage for the boundaries hardened in the previous commit. WP_Stream\Network previously had almost no direct coverage (4.76% of methods), which is how the can_write_settings() either/or bug below went unnoticed through two full green test runs. tests/phpunit/test-class-network.php (new): - A site administrator and a subscriber both have a caller-supplied blog_id discarded in favour of the current blog. - A super admin still gets cross-site filtering, so Network Admin is unaffected. - site_id keeps its existing default. - can_view_network_records() is false for a site administrator and true for a super admin, so a spoofed Referer alone cannot establish network context. - blog_id_logged() keeps site attribution for a site user. test-class-ability-update-settings.php: - A site administrator cannot write network-wide settings. - A super admin is denied when the Stream settings capability is revoked. This pins the AND semantics of can_write_settings(): the network capability is an additional requirement, not a substitute. Note WP_User::has_cap() returns early for super admins before the user_has_cap filter runs, so the test uses map_meta_cap/do_not_allow -- the one restriction that early return honours. Each test was verified to fail against the unfixed code and pass against the fix. Both suites: 401 tests, 0 failures; skipped/incomplete counts unchanged from the develop baseline. * fix: stop leaking integration credentials through Stream records and APIs Alert destination credentials (get-alerts): stream/get-alerts requires only view_stream, but alert destinations are configured behind the Stream settings capability, so returning alert_meta verbatim dropped credentials across a privilege boundary. A Slack incoming webhook URL and an IFTTT Maker key are both bearer credentials -- possession alone is enough to post into the channel or fire the account's applets. Ability::redact_alert_meta() now replaces those values with a `{key}_configured` boolean before output, so callers can still tell whether a destination is set up without receiving the secret. Non-secret configuration (channel, username, event_name) is untouched. The helper also absorbs the existing empty-meta-to-stdClass normalization the output schema requires. Payment gateway credentials (WooCommerce connector): callback_updated_option() serialized entire third-party gateway settings arrays into record metadata. Gateways routinely co-locate operational settings with live API secrets and webhook signing keys, so an enabled credential-bearing gateway persisted those secrets into stream_meta where any Stream viewer or record-detail API consumer could read them. Adds Connector::is_secret_key() / redact_secret_values(), applied to both old and new values before serialization. Matching is substring/suffix based rather than an allowlist because connectors log option arrays belonging to plugins we do not control, and an allowlist cannot anticipate their field names. Over-redacting costs a little audit detail; under-redacting persists a live credential. The helper lives on the Connector base class because the remaining unredacted-secret findings need the same logic. Note the pattern list was corrected while writing the tests: 'password' did not match mailserver_pass and no pattern matched rg_gforms_key, both of which are real targets. A bare 'key' suffix was also rejected as it matched harmless words such as monkey. Each redaction test was verified to fail against the unredacted code. Both suites: 408 tests, 0 failures. * fix: redact credential settings before they reach Stream record metadata Applies the Connector redaction helper added in the previous commit to the remaining settings that were persisted verbatim. Settings connector (mailserver_pass): callback_updated_option() passed values through sanitize_value(), which only flattens complex types to strings and has no notion of sensitivity, so a reusable mailbox password was stored as both old_value and value. Redaction is now applied per field at the two call sites where the setting name is known -- sanitize_value() itself receives no key and cannot make the decision. Gravity Forms connector (rg_gforms_key, rg_gforms_captcha_private_key): check() logged old/new values for every tracked option including the reCAPTCHA private key, and check_rg_gforms_key() explicitly logged both halves of a license-key change. Both now redact before logging. In the license case the update/delete status is derived before redaction, so the message still reports which happened -- covered by a test. The change also refines the pattern list from the previous commit, driven by test failures rather than assumption: - rg_gforms_captcha_public_key was being over-redacted by the '_key' suffix rule. Public halves of key pairs are meant to be published and redacting them removes audit detail for no security benefit, so PUBLIC_KEY_PATTERNS now exempts public_key / publishable_key / site_key ahead of the secret match. A test asserts the public key survives while the private one does not. - The earlier assertion that 'publishable_key' should be treated as secret was wrong and has been corrected. Each redaction test was verified to fail without the fix while the non-secret control tests continued to pass, confirming they discriminate rather than redacting everything. Both suites: 414 tests, 0 failures. * fix: address review feedback on secret redaction 1. Secret markers now beat the public-name exemption. PUBLIC_KEY_PATTERNS was checked first and returned early, so a name such as secret_site_key or webhook_public_key escaped redaction despite carrying an explicit secret marker -- inverting the over-redact-rather-than-under-redact preference the previous commit claimed. The exemption exists only to stop the broad "_key" suffix rule from catching published key halves, so it is now consulted after the secret substrings and before the suffix rule. 2. REDACTED_PLACEHOLDER is '[redacted]' rather than ''. The empty string made a withheld credential indistinguishable from a cleared field, contradicting the stated rationale of the empty-value test. Unset credentials still log as '' so "never set" and "set but withheld" remain distinguishable; the test now asserts both directions. 3. Extends redaction to EDD and Jetpack, which logged raw old/new option values. EDD is the notable one: it logs arbitrary settings fields including payment gateway API keys. BuddyPress was examined and left alone -- it only logs component activation booleans and page IDs, so there is no credential to redact and a call would be dead code. 4. Adds the wp_stream_secret_alert_meta_keys filter so third-party alert types registered via wp_stream_alert_types can have their own destination secrets redacted from get-alerts output. Tests assert against Connector::REDACTED_PLACEHOLDER rather than a literal so they track the constant. The precedence fix and the filter both have tests verified to fail against the previous behaviour. Both suites: 416 tests, 0 failures. * fix: close nested redaction gap and trim Jetpack option payloads Secret parents that key credentials by opaque IDs, such as Jetpack user_tokens, kept their values because recursion looked only at child names. Carry the parent name down so all descendants redact. Jetpack attached the whole jetpack_options blob to a record about one changed field, so a single heuristic miss exposed all tokens. Log only the changed field, and withhold Publicize connection payloads, which hold no non-credential data. Also add signature to the secret patterns for PayPal NVP gateways, and make ajax_network_admin use wp_doing_ajax so the Referer plus capability rule can be tested without a permanent DOING_AJAX constant.
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
The user_has_cap/role_has_cap filters are registered in the Admin constructor, but the Settings object they dereference is only constructed in Plugin::init() on init priority 9. Any capability check for view_stream between plugins_loaded and init 9 — security/firewall plugins evaluating rules on plugins_loaded do exactly this — reaches role_can_view() with a null options chain, and in_array() with a null haystack is an uncaught TypeError on PHP 8+, fataling wp-admin and admin-ajax for every logged-in user. Null-coalesce the options access and deny access during the pre-init window instead of fataling. Adds regression tests for both the early (denied, no fatal) and normal (granted) paths. Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
WordPress core passes the user_login (not email) to the retrieve_password action. Looking up by email first returned false for any login that happens to be email-shaped but does not match user_email, which then triggered a PHP 8 warning for reading property on bool. Look up by login first, fall back to email only for valid email strings, and bail out early if no WP_User is found. Fixes #1838 Co-authored-by: Faisal Ahammad <faisal@xwp.co>
* Fix false "SITE IS DISCONNECTED" error on empty query results wp stream query treated any empty result set as a disconnected site, since db->query() returns array() for both "no matching records" and implicitly for failures. This misfired on any site with no logged activity yet (e.g. a fresh install or an untouched multisite subsite), as reported in #1829. Check $wpdb->last_error instead, which WordPress only populates on an actual database error, so a legitimately empty result no longer trips the disconnected check. * Add PHPUnit coverage for CLI::connection() Covers both branches of the fix from the previous commit: a query that legitimately matches zero records should not trip WP_CLI::error(), while a genuine database failure still should. Uses WP_CLI's capture_exit mechanism (via reflection, since it's private) to catch the resulting ExitException instead of terminating the test process.
bartoszgadomski
approved these changes
Aug 31, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Cuts the 4.4.0 release from
develop. Minor bump: the diff adds backwards-compatible public surface (thewp_stream_secret_alert_meta_keysfilter, theConnectorredaction helpers,Network::can_view_network_records()) and removes nothing. No schema change and no PHP or WordPress version floor change.The diff against
mastershows all changes since 4.3.0. The release branch itself adds only 3 commits on top ofdevelop, touching 4 files:changelog.md,readme.txt,stream.php, andclasses/class-plugin.php. All code changes were reviewed and merged intodevelopbefore the branch was cut.Release Changelog
[redacted](Harden multisite authorization and stop logging integration credentials #1958).stream/get-alertsability (Harden multisite authorization and stop logging integration credentials #1958).wp stream queryon a site with no records (Error: SITE IS DISCONNECTED on one multisite install. Works on another on same server. #1829, Fix false "SITE IS DISCONNECTED" error on empty query results #1966).role_can_view()when the capability check runs beforeinit(Fix fatal in role_can_view() when cap check fires before init #1963).wp_stream_secret_alert_meta_keysfilter for the destination secrets of a custom alert type (Harden multisite authorization and stop logging integration credentials #1958).Connector::is_secret_key()andConnector::redact_secret_values()as reusable redaction helpers (Harden multisite authorization and stop logging integration credentials #1958).Release Checklist
masterbranch.== Upgrade Notice ==entry covers it.readme.txt.stream.php.Stable taginreadme.txt.classes/class-plugin.php.v4.4.0pending.Validation
RC tested in WordPress Playground on WP 7.1 / PHP 8.3, single site and multisite (2 sites, Stream network-activated).
v4.4.0-rc.1pre-release published; WP.org dry-run SVN log reviewed. Log confirms "No files will be committed to Subversion". SVN payload holds notests/,local/,docs/,node_modules/, or.github/content.stream.php,readme.txt, andPlugin::VERSION.php -lclean on every shipped PHP file outsidevendor/.?blog_id=2. Site administrator withoutmanage_network_options:can_view_network_records=false, requestedblog_id2 discarded, effectiveblog_id1. Super admin: effectiveblog_id2, so Network Admin still reads across sites. Forged network-admin Referer withwp_doing_ajaxtrue is rejected for the site administrator (ajax_network_admin=false,blog_id_logged(1)=1) and honoured for the super admin (true/0).mailserver_passrecord stores[redacted]for both old and new values;blogdescriptionkeeps its full values, so redaction discriminates. Zerostream_metarows hold the cleartext secret. Key classification correct on the hard cases:secret_site_keyredacted (secret marker beats the public-name exemption),rg_gforms_captcha_public_keyandpublishable_keykept,monkeyunaffected by the_keysuffix rule.wp stream queryon a site with no records prints an empty table, noSITE IS DISCONNECTED. With records present it lists them normally.Not covered by the manual test: #1963 (
role_can_view()beforeinit). It is a private method onAdmin, which is null outside wp-admin, so the pre-initstate cannot be forced without patching shipped code. Covered bytests/phpunit/test-class-admin.php.Do not merge until the stable
v4.4.0release is published and the WP.org deploy is confirmed.