Files
mealie/mealie/services/event_bus_service/publisher.py
Hayden ce88ed6841 fix: harden server-initiated HTTP against SSRF and DNS rebinding
Route all server-initiated outbound HTTP through one SSRF-protected layer:

- Resolve each host once, validate every resolved address (private,
  loopback, link-local, CGNAT 100.64/10, multicast, reserved, unspecified,
  IPv4-mapped IPv6), and reject if any is unsafe.
- Pin the connection to the validated address via curl's RESOLVE option so
  curl cannot re-resolve to a rebound IP (closes the validate-then-connect
  TOCTOU). Uses getaddrinfo so IPv6 records are checked too.
- Add a sync SafeTransport and safehttp.post, and move the webhook and
  recipe-action paths off raw requests.post so they are guarded like the
  scraper.
- Add HTTP_ALLOW_LIST / HTTP_DISALLOW_LIST settings (deny > allow > default)
  so operators can reach an internal server or block extra hosts.

Apprise notifications egress through the third-party library and are not
covered here.
2026-07-19 21:41:30 -05:00

58 lines
1.9 KiB
Python

from typing import Protocol
import apprise
from fastapi.encoders import jsonable_encoder
from mealie.core.config import get_app_settings
from mealie.pkgs import safehttp
from mealie.services.event_bus_service.event_types import Event
class PublisherLike(Protocol):
def publish(self, event: Event, notification_urls: list[str]): ...
class ApprisePublisher:
def __init__(self, hard_fail=False) -> None:
asset = apprise.AppriseAsset(
async_mode=True,
image_url_mask="https://raw.githubusercontent.com/mealie-recipes/mealie/9571816ac4eed5beacfc0abf6c03eff1427fd0eb/frontend/static/icons/android-chrome-maskable-512x512.png",
)
self.apprise = apprise.Apprise(asset=asset)
self.hard_fail = hard_fail
def publish(self, event: Event, notification_urls: list[str]):
"""Publishses a list of notification URLs"""
tags = []
for dest in notification_urls:
# we tag the url so it only sends each notification once
tag = str(event.event_id)
tags.append(tag)
status = self.apprise.add(dest, tag=tag)
if not status and self.hard_fail:
raise Exception("Apprise URL Add Failed")
self.apprise.notify(title=event.message.title, body=event.message.body, tag=tags)
class WebhookPublisher:
def __init__(self, hard_fail=False) -> None:
self.hard_fail = hard_fail
def publish(self, event: Event, notification_urls: list[str]):
event_payload = jsonable_encoder(event)
settings = get_app_settings()
for url in notification_urls:
r = safehttp.post(
url,
json=event_payload,
timeout=15,
allow_hosts=settings.http_allow_list,
deny_hosts=settings.http_disallow_list,
)
if self.hard_fail:
r.raise_for_status()