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.
27 lines
1.1 KiB
Ruby
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
|