• 2.7.0 4221d4b19d

    2.7.0
    All checks were successful
    CI / Lint + Static Analysis (push) Successful in 1m56s
    CI / Unit Tests (PHP 8.3) (push) Successful in 1m49s
    CI / Unit Tests (PHP 8.4) (push) Successful in 1m48s
    CI / Unit Tests (PHP 8.5) (push) Successful in 1m52s
    Release / Verify tag, version and changelog (push) Successful in 3s
    Release / Lint + Static Analysis (push) Successful in 1m53s
    Release / Unit Tests (PHP 8.5) (push) Successful in 1m38s
    Release / Build zip and publish release (push) Successful in 3s
    Release / Unit Tests (PHP 8.3) (push) Successful in 1m37s
    Release / Unit Tests (PHP 8.4) (push) Successful in 1m37s
    Stable

    eric released this 2026-10-16 22:36:07 -07:00 | 1 commits to main since this release

    Security

    • DNS pinning now works with the cURL transport. The pinned IP was passed as $args['curl'], which WordPress never reads, so cURL looked the hostname up again when connecting, and a DNS rebind could get past the private-IP check. CURLOPT_RESOLVE is now set on the cURL handle from the http_api_curl action, for this request only (#3).
    • Loopback is now checked on the resolved IP as well as on the literal URL host. Before, [::], 0177.0.0.1, 2130706433, 127.1 or a name resolving to 127.x fell through to the private-range check, so the Allow Private IPs setting opened localhost. Only the apprise_notifications_allow_localhost filter can open loopback now. 0.0.0.0/8 and :: also count as loopback (#4).
    • The private-IP check now blocks non-global ranges that PHP's FILTER_FLAG_NO_PRIV_RANGE / FILTER_FLAG_NO_RES_RANGE let through: 100.64.0.0/10 (CGNAT, Tailscale, and Alibaba Cloud metadata at 100.100.100.200), 192.0.0.0/24, 198.18.0.0/15, the documentation ranges, 224.0.0.0/4, and in IPv6 64:ff9b:1::/48, 100::/64, Teredo 2001::/32, 2001:db8::/32, fec0::/10 and ff00::/8. NAT64 (64:ff9b::/96) and 6to4 (2002::/16) addresses are checked by the IPv4 address they embed, so 64:ff9b::a9fe:a9fe is treated as 169.254.169.254. Allow Private IPs and the apprise_notifications_allow_private_ip filter still open these ranges (#5).
    • On multisite, only a super admin can change Allow Private IPs. A subsite administrator has manage_options but not network-level trust, and could use the setting with Test Connection to probe hosts and ports on the server's internal network. For other users the checkbox is now disabled, and anything they submit for it is ignored. Values already saved are not changed: if a subsite admin turned the setting on before this release, it stays on until a super admin turns it off (#6).
    • Allow Private IPs no longer opens link-local and cloud metadata addresses: 169.254.0.0/16 (metadata at 169.254.169.254 on AWS, GCP, Azure and others), fe80::/10, AWS IMDS over IPv6 at fd00:ec2::254, and Alibaba Cloud metadata at 100.100.100.200. No Apprise server runs there. The apprise_notifications_allow_private_ip filter can still open them, but it gets false as the default for these addresses whatever the setting, and only a strict true lets one through (#7).

    Fixed

    • Email suppression → Matching events only with user_register on no longer blocks the new user's own set-password email. Only the admin's new-user email is suppressed, which is the one the Apprise notification replaces. Before, a newly registered user never got their login link (#8).
    • Email suppression now needs an Apprise API URL as well as the plugin being enabled. With no URL set, All emails used to drop every site email, password resets included, and Matching events only dropped the emails it covers, with no Apprise notification sent in their place. The settings page now warns when a suppression mode is chosen but no API URL is set. A URL that is set but unreachable still can't be detected before WordPress sends mail (#9).
    • The Apprise notice about a new plugin version now respects the plugin's Enable Notifications switch. Before, a site with notifications turned off but Check for new versions on kept sending update notices to the Apprise URL. With notifications off, the daily notice job is unscheduled; new versions still show on the Plugins and Updates screens with one-click update. On multisite the main site's switch applies, since only the main site sends the notice (#10).

    Changed

    • The Theme Auto-Updated event is now labelled Theme Updated. It has always fired for manual theme updates as well as automatic ones. The event key (theme_update) is unchanged.
    • The Enable Notifications description no longer says it doesn't affect the update checker, which stopped being true with the fix above.
    • README and readme.txt reviewed against the code: the README's event list now has all 17 events and their defaults, its custom-event example uses the dispatcher instead of the apprise_notifications_event_types filter removed in 2.4.1, it lists every public hook, and it no longer describes delivery as synchronous. readme.txt drops a Screenshots section with no screenshots (#12).

    Removed

    • Raw cURL options returned in $args['curl'] by apprise_notifications_request_args are no longer filtered. WordPress never applied them, so the filtering did nothing.
    Downloads
  • 2.6.0 06d630dad8

    2.6.0
    All checks were successful
    CI / Unit Tests (PHP 8.3) (push) Successful in 1m41s
    CI / Unit Tests (PHP 8.4) (push) Successful in 1m44s
    CI / Unit Tests (PHP 8.5) (push) Successful in 1m47s
    Release / Verify tag, version and changelog (push) Successful in 3s
    Release / Lint + Static Analysis (push) Successful in 1m49s
    CI / Lint + Static Analysis (push) Successful in 1m59s
    Release / Unit Tests (PHP 8.4) (push) Successful in 1m41s
    Release / Unit Tests (PHP 8.3) (push) Successful in 2m2s
    Release / Unit Tests (PHP 8.5) (push) Successful in 1m36s
    Release / Build zip and publish release (push) Successful in 3s
    Stable

    eric released this 2026-10-16 16:58:55 -07:00 | 13 commits to main since this release

    Added

    • Available updates now appear on Plugins and Dashboard → Updates with one-click update from the release zip. The plugin header declares Update URI: https://git.pickysysadmin.ca/eric/apprise-wordpress-notifications/releases, and the opt-in update check answers core's update_plugins_git.pickysysadmin.ca filter with the highest release in the repo's releases.rss. Results are cached for 12 hours (1 hour after a failed fetch) and dropped when the setting is toggled. The Update URI also stops wordpress.org from offering a same-slug plugin as an update.

    Changed

    • Minimum PHP version raised from 8.0 to 8.3 (Requires PHP: 8.3). PHP 8.0 and 8.1 are end of life, and 8.2 reaches end of life in December 2026. Sites still on an older PHP should stay on 2.5.4 until they upgrade PHP.
    • The update check replaces its custom admin notice, dismiss button and AJAX handler with core's update UI. The Apprise notification (once per new version, from a daily WP-Cron job) is unchanged, except that it now links to Dashboard → Updates.
    • On multisite, the main site's Check for new versions setting applies to the whole network, and only the main site sends the Apprise notification. Previously each site checked and notified separately.
    • Release zips are now built by CI from tagged commits and attached to each release as apprise-wordpress-notifications.X.Y.Z.zip, with apprise-wordpress-notifications/ as the root folder. Install from that zip, not the Source Code archive.

    Security

    • The update check sends an Apprise-Notifications/<version> user agent instead of core's default, which includes the site's URL. A cached version that isn't a release-style version number is discarded and refetched rather than used in the download URL.
    Downloads
  • 2.5.4 bee61ab047

    2.5.4 Stable

    eric released this 2026-06-28 09:52:08 -07:00 | 19 commits to main since this release

    Forgejo migration and update checker hardening

    Fixed update checker compatibility with Forgejo. Forgejo's /releases.rss feed uses the same URL path as Gitea, but item titles are plain version numbers (e.g. 2.5.3) rather than v-prefixed. The checker now extracts the version from basename(link) instead of the item title, and filters feed items by /releases/tag/ in the link URL — making it robust against any future title format changes regardless of forge software.

    Updated internal docstrings from "Gitea" to "Forgejo".

    Full changelog: 2.5.3...2.5.4

    Downloads
  • 2.5.3 0beb9b9c3d

    2.5.3 Stable

    eric released this 2026-06-28 09:42:33 -07:00 | 21 commits to main since this release

    Security

    • Block CURLOPT_URL in sanitize_curl_options() — CURLOPT_URL (integer 10002) was absent from the cURL option blocklist. A rogue plugin hooking apprise_notifications_request_args could inject this option to redirect any Apprise API request to an arbitrary URL after SSRF validation had already passed. The CURLOPT_RESOLVE DNS pin applied to the original Apprise hostname has no effect on a substituted target, so loopback services, RFC 1918 hosts, and cloud metadata endpoints (e.g. 169.254.169.254) were reachable. Exploitation requires server-side code execution via a malicious or compromised plugin.

    Full changelog: 2.5.2...2.5.3

    Downloads
  • 2.5.2 de4c16e54e

    2.5.2 Stable

    eric released this 2026-06-27 22:48:18 -07:00 | 22 commits to main since this release

    Security fixes and hardening

    • Fixed IPv4-mapped IPv6 SSRF bypass (HIGH) — Addresses in the
      ::ffff:0:0/96 range (e.g. ::ffff:169.254.169.254, ::ffff:7f00:1)
      bypassed both the loopback check and private/reserved range check because
      PHP's FILTER_FLAG_NO_PRIV_RANGE / FILTER_FLAG_NO_RES_RANGE do not
      cover that range. Both resolve_and_validate_url() and
      is_loopback_address() now unwrap IPv4-mapped addresses before range
      checks, mirroring WordPress core's wp_http_validate_url().

    • Added CURLOPT_CAINFO/CAPATH to sanitize_curl_options() blocklist (LOW)
      — Both options substitute the CA bundle used for SSL verification and were
      absent despite all other SSL-bypass cURL options being blocked.

    • Fixed email suppression 'matching' mode not suppressing pingback emails
      when pingback_new is enabled but comment_new is disabled — the
      notify_post_author / notify_moderator filters were only registered
      when comment_new was on.

    • Updated email suppression 'all' mode description to explicitly warn that
      password reset emails have no Apprise notification substitute.

    • Updated Stateless URLs field description to clarify that Apprise makes
      outbound HTTP requests to those URLs, including HTTP-dispatching schemes
      (json://, xml://, form://).

    Downloads