Files
once-campfire/test/lib/web_push/persistent_request_test.rb
T
Jeremy Daer 262ac6be06 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.
2026-08-25 21:04:11 -07:00

27 lines
1.1 KiB
Ruby

require "test_helper"
class WebPush::PersistentRequestTest < ActiveSupport::TestCase
ENDPOINT = "https://fcm.googleapis.com/fcm/send/test123"
# The delivery must connect to the public IP resolved and guarded by
# Push::Subscription, never re-resolve the raw endpoint host at connect time --
# otherwise a rebind between resolution and delivery reopens the SSRF. An empty
# message keeps the request past encryption and onto the socket we assert on.
test "pins delivery to endpoint_ip instead of re-resolving the host" do
host = URI(ENDPOINT).host
WebMock.disable_net_connect! allow: [ host ]
TCPSocket.expects(:open).with { |*args, **| args.first == host }.never
TCPSocket.expects(:open).with { |*args, **| args.first == DnsTestHelper::WEB_PUSH_PUBLIC_TEST_IP && args[1] == 443 }.throws(:pinned_to_ip)
assert_throws :pinned_to_ip do
WebPush.payload_send \
message: "",
endpoint: ENDPOINT,
endpoint_ip: DnsTestHelper::WEB_PUSH_PUBLIC_TEST_IP,
p256dh: "", auth: "", vapid: {},
urgency: "high"
end
end
end