Files
once-campfire/test/controllers/users/push_subscriptions_controller_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

61 lines
2.2 KiB
Ruby

require "test_helper"
class Users::PushSubscriptionsControllerTest < ActionDispatch::IntegrationTest
setup do
sign_in :david
stub_web_push_dns_resolution
end
test "create new push subscription" do
subscription_params = { "endpoint" => "https://fcm.googleapis.com/fcm/send/abc123", "p256dh_key" => "123", "auth_key" => "456" }
post user_push_subscriptions_url,
params: { push_subscription: subscription_params }, headers: { "HTTP_USER_AGENT" => "Mozilla/5.0" }
assert_response :ok
assert_equal subscription_params, users(:david).push_subscriptions.last.attributes.slice("endpoint", "p256dh_key", "auth_key")
assert_equal "Mozilla/5.0", users(:david).push_subscriptions.last.user_agent
end
test "touch existing subscription" do
assert_no_difference -> { users(:david).push_subscriptions.count } do
assert_changes -> { push_subscriptions(:david_chrome).reload.updated_at } do
post user_push_subscriptions_url(params: {
push_subscription: push_subscriptions(:david_chrome).attributes.slice("endpoint", "p256dh_key", "auth_key")
})
end
end
assert_response :ok
end
test "rejects subscription with non-permitted endpoint" do
subscription_params = { "endpoint" => "https://attacker.example.com/steal", "p256dh_key" => "123", "auth_key" => "456" }
assert_no_difference -> { Push::Subscription.count } do
post user_push_subscriptions_url, params: { push_subscription: subscription_params }
end
assert_response :unprocessable_entity
end
test "rejects subscription with endpoint resolving to a private IP" do
stub_dns_resolution("169.254.169.254")
subscription_params = { "endpoint" => "https://fcm.googleapis.com/fcm/send/abc123", "p256dh_key" => "123", "auth_key" => "456" }
assert_no_difference -> { Push::Subscription.count } do
post user_push_subscriptions_url, params: { push_subscription: subscription_params }
end
assert_response :unprocessable_entity
end
test "destroy a push subscription via dev mode" do
assert_difference -> { Push::Subscription.count }, -1 do
delete user_push_subscription_url(push_subscriptions(:david_chrome))
assert_redirected_to user_push_subscriptions_url
end
end
end