mirror of
https://github.com/basecamp/once-campfire.git
synced 2026-08-28 17:42:50 +09:00
262ac6be06
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.
37 lines
1.4 KiB
Ruby
37 lines
1.4 KiB
Ruby
class WebPush::Notification
|
|
def initialize(title:, body:, path:, badge:, endpoint:, endpoint_ip:, p256dh_key:, auth_key:)
|
|
@title, @body, @path, @badge = title, body, path, badge
|
|
@endpoint, @endpoint_ip, @p256dh_key, @auth_key = endpoint, endpoint_ip, p256dh_key, auth_key
|
|
end
|
|
|
|
# @endpoint_ip is the public address Push::Subscription resolved and guarded for
|
|
# this delivery. When it is nil the host resolved to nothing or to a blocked
|
|
# (private) address, so we skip delivery rather than let the request fall back
|
|
# to re-resolving the raw host -- which is what would reopen the SSRF for a
|
|
# subscription that slipped in before endpoint validation existed.
|
|
def deliver(connection: nil)
|
|
if @endpoint_ip
|
|
WebPush.payload_send \
|
|
message: encoded_message,
|
|
endpoint: @endpoint, endpoint_ip: @endpoint_ip, p256dh: @p256dh_key, auth: @auth_key,
|
|
vapid: vapid_identification,
|
|
connection: connection,
|
|
urgency: "high"
|
|
end
|
|
end
|
|
|
|
private
|
|
def vapid_identification
|
|
{ subject: "mailto:support@37signals.com" }.merge \
|
|
Rails.configuration.x.vapid.symbolize_keys
|
|
end
|
|
|
|
def encoded_message
|
|
JSON.generate title: @title, options: { body: @body, icon: icon_path, data: { path: @path, badge: @badge } }
|
|
end
|
|
|
|
def icon_path
|
|
Rails.application.routes.url_helpers.account_logo_path
|
|
end
|
|
end
|