Compare commits

..

5 Commits

Author SHA1 Message Date
alexei.dolgolyov 83215473c7 chore: release v0.2.2
Release / release (push) Successful in 1m0s
2026-04-22 02:51:10 +03:00
alexei.dolgolyov 4e23d2b054 chore(compose): hardcode NOTIFY_BRIDGE_ALLOW_PRIVATE_URLS=1 in compose
This project ships for homelab use; downstream targets (Immich, Gitea,
...) sit on RFC1918 addresses which the SSRF guard blocks by default.
Setting the flag directly in compose — not via ${...} substitution —
avoids the Portainer gotcha where the stack-level "Environment variables"
panel is for compose-file substitutions only, not runtime container env.
Operators who want to run this on a public-facing box can drop the line.
2026-04-22 02:49:19 +03:00
alexei.dolgolyov f7d51b27d2 Revert "chore(compose): default NOTIFY_BRIDGE_ALLOW_PRIVATE_URLS=1 for homelab"
This reverts commit 3bb0585e43.
2026-04-22 02:47:09 +03:00
alexei.dolgolyov 3bb0585e43 chore(compose): default NOTIFY_BRIDGE_ALLOW_PRIVATE_URLS=1 for homelab
Homelab targets (Immich, Gitea, ...) are almost always on RFC1918
addresses, which the SSRF guard rejects by default.  Exporting the flag
to 1 in the compose file — overridable via the host environment —
matches how this project is actually deployed (TrueNAS / unraid / etc.)
without weakening the defense for anyone who sets it to 0 on a
public-facing box.
2026-04-22 02:46:10 +03:00
alexei.dolgolyov 58cba88c92 docs(immich-ssrf): surface NOTIFY_BRIDGE_ALLOW_PRIVATE_URLS hint in error
Homelab/LAN Immich instances trip the SSRF guard (Host 192.168.x resolves
to blocked address).  The fix is to set NOTIFY_BRIDGE_ALLOW_PRIVATE_URLS=1
in the runtime env — call that out directly in the error message so
operators don't have to dig through source to find it.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-22 02:42:22 +03:00
6 changed files with 27 additions and 37 deletions
+14 -28
View File
@@ -1,41 +1,27 @@
## v0.2.1 (2026-04-22)
## v0.2.2 (2026-04-22)
Security-focused release on top of v0.2.0. Hardens the restore/backup flow,
CSRF/SSRF surfaces, JWT revocation on role change, and template-context
leakage; adds a new **per-tracking-config quiet hours** feature with
app-level IANA timezone support; plus a handful of performance fixes.
Patch release — homelab usability fixes on top of v0.2.1. The SSRF hardening
introduced in v0.2.1 blocks outbound requests to RFC1918 / link-local hosts,
which breaks tracking of Immich / Gitea / etc. running on the same LAN.
This release makes the workaround discoverable and enables it by default
in the shipped `docker-compose.yml`.
### Features
### Bug Fixes
- **Per-tracking-config quiet hours with app-level IANA timezone** — new `Timezone` app setting (defaults to `UTC`) and a `Quiet Hours` section on the Immich tracking-config form. HH:MM windows (including overnight, e.g. `22:0007:00`) are interpreted in the configured timezone and suppress all notifications for that tracker. ([6c3dd67](https://git.dolgolyov-family.by/alexei.dolgolyov/notify-bridge/commit/6c3dd67))
- **Default `NOTIFY_BRIDGE_ALLOW_PRIVATE_URLS=1` in `docker-compose.yml`** — the shipped compose is intended for homelab use. The flag is now hardcoded in the `environment:` block (not a `${...}` substitution) so it works correctly with Portainer's per-stack env panel, which only does compose-file substitution and not runtime container env. Operators running on a public-facing host can drop the line. ([4e23d2b](https://git.dolgolyov-family.by/alexei.dolgolyov/notify-bridge/commit/4e23d2b))
### Security
### Documentation
- **Signed & verified pending-restore bundles** — SHA256 stored in `AppSetting` and checked on startup apply; files outside `data_dir` are refused and permissions tightened to `0600`. ([56993d2](https://git.dolgolyov-family.by/alexei.dolgolyov/notify-bridge/commit/56993d2))
- **Same-origin check on `POST /api/backup/apply-restart`** — Bearer-in-localStorage was CSRF-reachable from any XSS'd admin tab; require matching `Origin`/`Referer`. ([56993d2](https://git.dolgolyov-family.by/alexei.dolgolyov/notify-bridge/commit/56993d2))
- **JWT `token_version` bumps on demotion** — role/username change and admin password reset now bump `token_version` so already-issued tokens lose admin. Last-admin TOCTOU guarded by `COUNT` + post-commit recheck that rolls back on race. ([56993d2](https://git.dolgolyov-family.by/alexei.dolgolyov/notify-bridge/commit/56993d2))
- **SSRF guard extended** to `ImmichClient.__init__` and the `external_domain` setter — admin-mutable URLs were bypassing the check that webhook / Slack / Discord paths already used. Dev `scripts/restart-backend.sh` now sets `NOTIFY_BRIDGE_ALLOW_PRIVATE_URLS=1` so homelab Immich instances still work. ([56993d2](https://git.dolgolyov-family.by/alexei.dolgolyov/notify-bridge/commit/56993d2))
- **Redact & cap Immich error bodies** (~120 chars) before they flow into `ActionExecution.error` / `EventLog.details` (both UI-visible). ([56993d2](https://git.dolgolyov-family.by/alexei.dolgolyov/notify-bridge/commit/56993d2))
- **Deny-list sensitive keys** (`api_key`, `token`, `secret`, `password`, `authorization`, `cookie`, …) in template-context merges so a rogue template cannot exfiltrate provider creds via `{{ api_key }}`. ([56993d2](https://git.dolgolyov-family.by/alexei.dolgolyov/notify-bridge/commit/56993d2))
- **Cap user-controlled Immich search params** — `query` ≤ 256, `person_ids` ≤ 50, `size` ≤ 100 — so a Telegram listener cannot DoS upstream. ([56993d2](https://git.dolgolyov-family.by/alexei.dolgolyov/notify-bridge/commit/56993d2))
- **Stream upload reads** with a running byte counter + `Content-Length` precheck instead of buffering the full body and then rejecting. ([56993d2](https://git.dolgolyov-family.by/alexei.dolgolyov/notify-bridge/commit/56993d2))
- **Log Telegram `parse_mode` fallbacks** instead of swallowing silently — template escape bugs now surface in server logs. ([56993d2](https://git.dolgolyov-family.by/alexei.dolgolyov/notify-bridge/commit/56993d2))
- **Rollback partial imports** on pending-restore failure (error recorded on a fresh session). ([56993d2](https://git.dolgolyov-family.by/alexei.dolgolyov/notify-bridge/commit/56993d2))
### Performance
- **Fix N+1** in `_refresh_telegram_chat_titles` — single `IN` query instead of `session.get` per chat. ([56993d2](https://git.dolgolyov-family.by/alexei.dolgolyov/notify-bridge/commit/56993d2))
- **Parallelize album & shared-link fetches** in `test_dispatch` via `asyncio.gather`, and per-receiver Telegram test sends in the notifier with a semaphore of 5. ([56993d2](https://git.dolgolyov-family.by/alexei.dolgolyov/notify-bridge/commit/56993d2))
- **Early-exit `collect_scheduled_assets(limit=0)`** so the periodic-summary test path skips the full per-album filter/sample (was O(album_assets)). ([56993d2](https://git.dolgolyov-family.by/alexei.dolgolyov/notify-bridge/commit/56993d2))
- **Explicit `CREATE INDEX IF NOT EXISTS`** for `event_log` (`user_id` / `action_id` / `provider_id`) so the first boot after upgrade isn't left unindexed for the dashboard query. ([56993d2](https://git.dolgolyov-family.by/alexei.dolgolyov/notify-bridge/commit/56993d2))
- **Add `AbortController` timeout (120s)** to `fetchAuth` so uploads/downloads don't hang indefinitely. ([56993d2](https://git.dolgolyov-family.by/alexei.dolgolyov/notify-bridge/commit/56993d2))
- **Surface `NOTIFY_BRIDGE_ALLOW_PRIVATE_URLS` hint in SSRF rejection errors** — the `UnsafeURLError` raised by `ImmichClient` now tells operators how to allow LAN targets, instead of leaving them to dig through source. ([58cba88](https://git.dolgolyov-family.by/alexei.dolgolyov/notify-bridge/commit/58cba88))
---
<details>
<summary>All Commits</summary>
- [6c3dd67](https://git.dolgolyov-family.by/alexei.dolgolyov/notify-bridge/commit/6c3dd67) — feat(tracking): per-config quiet hours with app-level IANA timezone _(alexei.dolgolyov)_
- [56993d2](https://git.dolgolyov-family.by/alexei.dolgolyov/notify-bridge/commit/56993d2) — fix(security,perf): harden restore, CSRF, token_version + perf pass _(alexei.dolgolyov)_
- [4e23d2b](https://git.dolgolyov-family.by/alexei.dolgolyov/notify-bridge/commit/4e23d2b) — chore(compose): hardcode NOTIFY_BRIDGE_ALLOW_PRIVATE_URLS=1 in compose _(alexei.dolgolyov)_
- [f7d51b2](https://git.dolgolyov-family.by/alexei.dolgolyov/notify-bridge/commit/f7d51b2) — Revert "chore(compose): default NOTIFY_BRIDGE_ALLOW_PRIVATE_URLS=1 for homelab" _(alexei.dolgolyov)_
- [3bb0585](https://git.dolgolyov-family.by/alexei.dolgolyov/notify-bridge/commit/3bb0585) — chore(compose): default NOTIFY_BRIDGE_ALLOW_PRIVATE_URLS=1 for homelab _(alexei.dolgolyov)_
- [58cba88](https://git.dolgolyov-family.by/alexei.dolgolyov/notify-bridge/commit/58cba88) — docs(immich-ssrf): surface NOTIFY_BRIDGE_ALLOW_PRIVATE_URLS hint in error _(alexei.dolgolyov)_
</details>
+4
View File
@@ -12,6 +12,10 @@ services:
environment:
- NOTIFY_BRIDGE_SECRET_KEY=${NOTIFY_BRIDGE_SECRET_KEY:?Set NOTIFY_BRIDGE_SECRET_KEY (min 32 chars)}
- NOTIFY_BRIDGE_CORS_ALLOWED_ORIGINS=${NOTIFY_BRIDGE_CORS_ALLOWED_ORIGINS:-*}
# Homelab target: allow outbound requests to RFC1918 / link-local addresses.
# The SSRF guard otherwise rejects 10.*/172.16.*/192.168.*/169.254.* hosts,
# which breaks tracking of Immich / Gitea / etc. running on the same LAN.
- NOTIFY_BRIDGE_ALLOW_PRIVATE_URLS=1
healthcheck:
test: ["CMD", "python", "-c", "import urllib.request; urllib.request.urlopen('http://localhost:8420/api/health')"]
interval: 30s
+1 -1
View File
@@ -1,7 +1,7 @@
{
"name": "notify-bridge-frontend",
"private": true,
"version": "0.2.1",
"version": "0.2.2",
"type": "module",
"scripts": {
"dev": "vite dev",
+1 -1
View File
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
[project]
name = "notify-bridge-core"
version = "0.2.1"
version = "0.2.2"
description = "Core library for Notify Bridge — service provider abstractions, models, notifications, and templates"
requires-python = ">=3.12"
dependencies = [
@@ -61,14 +61,15 @@ class ImmichClient:
# SSRF guard — admin-set Immich URLs are loaded from provider config
# which can be mutated via PATCH /api/providers or imported via
# prepare-restore, so we revalidate at construction time rather than
# trusting DB state. ``NOTIFY_BRIDGE_ALLOW_PRIVATE_URLS=1`` bypasses
# for dev against localhost Immich.
# trusting DB state. Homelab deployments pointing at RFC1918 targets
# must set ``NOTIFY_BRIDGE_ALLOW_PRIVATE_URLS=1`` in the runtime env.
if self._url:
try:
validate_outbound_url(self._url)
except UnsafeURLError as err:
raise UnsafeURLError(
f"Refusing to build ImmichClient for unsafe URL {self._url!r}: {err}"
f"Refusing to build ImmichClient for unsafe URL {self._url!r}: {err}. "
"If this is a LAN/homelab Immich, set NOTIFY_BRIDGE_ALLOW_PRIVATE_URLS=1."
) from err
@property
@@ -81,9 +82,8 @@ class ImmichClient:
@external_domain.setter
def external_domain(self, value: str | None) -> None:
# Mirror the constructor's SSRF guard — external_domain is used to
# build URLs that leak into rendered notifications, but any code path
# that eventually fetches this URL would otherwise bypass the check.
# Mirror the constructor's SSRF guard. Set
# ``NOTIFY_BRIDGE_ALLOW_PRIVATE_URLS=1`` for LAN/homelab targets.
if value:
try:
validate_outbound_url(value)
+1 -1
View File
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
[project]
name = "notify-bridge-server"
version = "0.2.1"
version = "0.2.2"
description = "Standalone Notify Bridge server — FastAPI REST API with SQLite database"
requires-python = ">=3.12"
dependencies = [