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.
This commit is contained in:
Jeremy Daer
2026-08-26 00:03:15 -07:00
parent fc91855d62
commit 7d5bb50b9b
7 changed files with 93 additions and 15 deletions
@@ -23,4 +23,31 @@ class WebPush::PersistentRequestTest < ActiveSupport::TestCase
urgency: "high"
end
end
# An egress proxy would open the TCP connection itself and re-resolve the
# endpoint host, defeating the ipaddr pin. The pinned path must ignore
# http_proxy/https_proxy and connect straight to the resolved public IP.
test "ignores proxy env so the pin can't be routed through a re-resolving proxy" do
host = URI(ENDPOINT).host
saved = ENV.slice("http_proxy", "https_proxy", "HTTP_PROXY", "HTTPS_PROXY")
%w[ http_proxy https_proxy HTTP_PROXY HTTPS_PROXY ].each { |k| ENV[k] = "http://proxy.internal:3128" }
WebMock.disable_net_connect! allow: [ host ]
TCPSocket.expects(:open).with { |*args, **| args.first == "proxy.internal" }.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
ensure
%w[ http_proxy https_proxy HTTP_PROXY HTTPS_PROXY ].each { |k| ENV.delete(k) }
saved.each { |k, v| ENV[k] = v }
end
end