Destination check: Apprise follows HTTP redirects itself, so a checked public webhook can lead the server to a private address #15
Labels
No labels
bug
documentation
enhancement
security
No project
No assignees
1 participant
Notifications
Due date
No due date set.
Dependencies
No dependencies set
Reference
Postulo/postulo-apprise#15
Loading…
Reference in a new issue
No description provided.
Delete branch "%!s()"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Found on 2026-09-29 in a read-only review of postulo-apprise at
7637d899d, reading how Apprise 1.13.1 (the locked version) sends once the plugin's check has passed, and inspecting the server objectsbuild()returns.The plugin checks the host written in the line and nothing after it. Apprise then sends with the
requestslibrary and follows 3xx answers by default, so the address finally contacted is chosen by whoever answers at the checked host. The core's own client re-checks every redirect precisely "so a public hostname cannot bounce a plugin onto the router's administration page" (in the core,plugins/http.py:10-12); the Apprise path has no equivalent. The module docstring names one residual gap, the second name lookup; redirects are a second one, simpler to use, and are not mentioned.src/postulo_apprise/notifier.py:66(build:client = apprise.Apprise(), with the default asset);:205and:236(client.notify(...)intestandsend);:26-30(the docstring's one stated gap). In Apprise 1.13.1:url.py:136(redirects = True),asset.py:198(http_redirects = True),url.py:252-254(self.redirects = parse_bool(kwargs.get("redirect", self.asset.http_redirects))), andallow_redirects=self.redirectsat 167 call sites in 140 plugin files, among themplugins/custom_json.py:325,plugins/custom_xml.py:416,plugins/custom_form.py:410,plugins/ntfy.py:664andplugins/gotify.py:241.build()makes forjson://,ntfy://andgotify://lines all reportredirects=True, and nothing innotifier.pypasses anAppriseAssetor reads aredirectargument. With private destinations refused, a line naming a public host passes the form. If that host answers 307 or 308 with aLocationon a loopback, link-local or LAN address,requestsrepeats the same method, headers and body there. For the custom webhook schemes (json,xml,form) the method, extra headers and extra payload fields are all set in the line.client().apprise.Apprise(asset=apprise.AppriseAsset(http_redirects=False)).add(), refuse a server whoseredirectsis true: aredirect=argument in the line overrides the asset default (checked: with the asset set toFalse, a line carrying the argument still reportsTrue).pyproject.toml:20allowsapprise>=1.9; raise the floor to a release that hashttp_redirects, since without it the option is silently ignored and redirects are followed.httpwith a redirect tohttpsneeds the…s://scheme.json,xmlandformoverpostulo.plugins.api.client(), closes this and the lookup gap together.Related to #1 (closed), which named the lookup gap only.
Done when
tests/test_apprise.pyshows that every server built from a saved line hasredirects is False, and that a line carryingredirect=yesis refused byvalidate()with a readable sentence.Fixed by pull request #23.