Files
clinch/test/lib/private_address_check_test.rb
Dan Milne 406a79d9eb Block SSRF via backchannel_logout_uri
backchannel_logout_uri was validated only for scheme/HTTPS, so an admin (or a
compromised admin account) could point it at internal infrastructure — cloud
metadata (169.254.169.254), loopback, or RFC1918 hosts — and every user logout
would fire a server-side POST there.

Add PrivateAddressCheck (app/lib) and apply it as defense-in-depth:
- Application validation rejects URIs whose host is, or is a literal, internal
  address (loopback / private / link-local / 0.0.0.0 / localhost / metadata
  hostnames). Fast, DNS-free, immediate admin feedback.
- BackchannelLogoutJob re-checks at request time WITH DNS resolution and aborts
  (no retry) if the host resolves to a non-public address — covering URIs that
  predate the validation and public hostnames pointed at internal IPs.

Tests cover the address classification, the model validation, and updates an
existing test that used a localhost logout URI.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-06-11 08:14:45 +10:00

39 lines
1.3 KiB
Ruby

require "test_helper"
class PrivateAddressCheckTest < ActiveSupport::TestCase
# internal_host? — DNS-free checks on IP literals and known hostnames
test "flags loopback, private, and link-local IP literals as internal" do
%w[
127.0.0.1
10.0.0.1
172.16.5.5
192.168.1.1
169.254.169.254
0.0.0.0
::1
].each do |host|
assert PrivateAddressCheck.internal_host?(host), "expected #{host} to be internal"
end
end
test "flags localhost-style hostnames as internal" do
assert PrivateAddressCheck.internal_host?("localhost")
assert PrivateAddressCheck.internal_host?("foo.localhost")
assert PrivateAddressCheck.internal_host?("metadata.google.internal")
assert PrivateAddressCheck.internal_host?("")
end
test "does not flag public IP literals as internal" do
refute PrivateAddressCheck.internal_host?("8.8.8.8")
refute PrivateAddressCheck.internal_host?("1.1.1.1")
end
# resolves_to_internal? on IP literals (no DNS needed) exercises the same
# address classification used after resolution.
test "resolves_to_internal? classifies IP literals" do
assert PrivateAddressCheck.resolves_to_internal?("169.254.169.254")
assert PrivateAddressCheck.resolves_to_internal?("127.0.0.1")
refute PrivateAddressCheck.resolves_to_internal?("8.8.8.8")
end
end