Commit Graph

3 Commits

Author SHA1 Message Date
Jeremy Daer 6e312c6028 Stop sending push notifications to banned users (#338)
Banning a user deletes their sessions and closes their connections but
keeps their push subscriptions, and Room::MessagePusher chose recipients
by membership alone. A banned user's browser or phone therefore went on
receiving the room name, sender and text of new direct messages,
mentions, and messages in rooms they had set to everything.

Choose subscriptions from active users only. The subscriptions are kept,
so unbanning brings notifications back without the user having to
subscribe again (the client doesn't resubscribe while the browser still
holds a subscription).

Co-authored-by: Marcello Costagliola <176920116+namespaceMarcello@users.noreply.github.com>
2026-10-07 11:25:16 -07:00
Jeremy Daer 94a48aacb6 Guard push-subscription endpoints against SSRF (#265)
* Guard push-subscription endpoints against SSRF

Web push delivery POSTed to the endpoint URL a user supplied when
registering a subscription, with no scheme, host, or private-network
check -- unlike the OpenGraph unfurl path, which already routes through
the shared SSRF address policy (surfguard). Any authenticated user could
register a subscription whose endpoint pointed at an internal address and
have the server fetch it on every chat message: blind SSRF for internal
recon and reachability probing, plus a thread-pool DoS on the delivery
pool.

Validate the endpoint when the subscription is saved: it must be HTTPS,
its host must belong to a known browser push service (allowlist), and it
must resolve to a public IP. On every delivery, re-resolve the host and
pin the connection to that public IP so a later DNS rebind can't redirect
the request to an internal address. If no public IP resolves at delivery
time -- a rebind, or a subscription that predates this validation --
delivery is skipped rather than falling back to re-resolving the raw
host.

Adds model, controller, and delivery-pinning tests, plus a DNS stub
helper for deterministic resolution in tests.

* Route push endpoint guarding through RestrictedHTTP::PrivateNetworkGuard

The endpoint SSRF check already delegated its private-network classification
to surfguard, but called Surfguard.resolve_public_ips directly rather than
through RestrictedHTTP::PrivateNetworkGuard -- the hostname-in, address-out
shim #241 established as the app's single guarded-outbound entry point and
that Opengraph::Fetch resolves through. Route push resolution through the same
guard so once-campfire keeps one place that resolves and classifies outbound
addresses. Behavior is unchanged: the guard raises Violation when a permitted
host resolves only to blocked addresses (previously an empty list -> nil) and
propagates Surfguard::Unresolvable for a host that resolves to nothing; both
map to a nil endpoint IP, which fails validation and skips delivery. The
allowlist, HTTPS/443 constraints, and per-delivery IP pinning are unchanged.

* Address push-SSRF review: defer DNS off enqueue path, disable proxy on pinned path, revalidate on re-registration

- Resolve the guarded endpoint IP lazily inside WebPush::Notification#deliver
  (on the bounded delivery worker) instead of eagerly when the notification is
  built on the serial enqueue path, so a slow resolver can't stall the push job
  before any delivery starts. resolved_endpoint_ip only reads the already-loaded
  endpoint attribute, so it is safe off the AR connection.
- Pin the delivery socket with an explicit nil proxy address so http_proxy/
  https_proxy can't route the request through a proxy that re-resolves the host
  and defeats the ipaddr pin.
- Revalidate an existing subscription on re-registration so a row predating
  endpoint validation gets the same 422 as a fresh create instead of being kept
  alive by touch.
- Regression tests: resolution deferred to delivery, pin survives proxy env,
  legacy invalid row rejected with 422.

* Bound the web-push delivery queue (max_queue, not the ignored queue_size)

Concurrent::ThreadPoolExecutor takes :max_queue; :queue_size was silently
ignored, leaving the delivery backlog unbounded (max_queue: 0). A flood of
valid push subscriptions could accumulate queued deliveries without limit --
more acute now that each delivery task also resolves DNS. Using max_queue: 10000
activates the intended cap; overflow raises RejectedExecutionError under the
default :abort policy, which deliver_later already rescues (push is best-effort,
retried on the next message).

* Trim push-SSRF guard comments to match house style

Apply the review suggestions on the push-subscription SSRF guard: replace
the verbose rationale comments with terse one-liners (or drop them where
the code speaks for itself). No behavior change -- the Surfguard-shim
routing, per-delivery public-IP pin, and bounded delivery queue are
untouched.
2026-08-28 11:44:39 -07:00
Kevin McConnell df76a227dc Hello world
First open source release of Campfire 🎉
2025-08-21 09:31:59 +01:00