1
0

netsafe_mutation_test.go 3.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102
  1. package netsafe
  2. import (
  3. "context"
  4. "strings"
  5. "testing"
  6. )
  7. // TestSSRFGuardedDialContext_LiteralIPSkipsResolver pins the netsafe.go:37
  8. // decision (`if ip := net.ParseIP(host); ip != nil`). The string "fe80::1%eth0"
  9. // is rejected by net.ParseIP (returns nil) but accepted by the resolver, which
  10. // yields the link-local address fe80::1. With the branch intact, ParseIP returns
  11. // nil so the host falls through to LookupIPAddr, resolves to fe80::1, and is
  12. // blocked by IsBlockedIP -> the error mentions the resolved blocked address.
  13. // If the condition is flipped to `ip == nil`, the nil-IP literal path is taken
  14. // instead: ips = [{IP: nil}], IsBlockedIP(nil) is false, the guard never fires
  15. // and the error would never say "blocked private/internal address fe80::1".
  16. func TestSSRFGuardedDialContext_LiteralIPSkipsResolver(t *testing.T) {
  17. _, err := SSRFGuardedDialContext(context.Background(), "tcp", "[fe80::1%eth0]:80")
  18. if err == nil {
  19. t.Fatal("expected error for link-local host with zone suffix")
  20. }
  21. if !strings.Contains(err.Error(), "blocked private/internal address fe80::1") {
  22. t.Fatalf("expected guard to block resolved link-local fe80::1, got: %v", err)
  23. }
  24. }
  25. // TestSSRFGuardedDialContext_LiteralPrivateIPv6Blocked complements the above by
  26. // confirming that a valid IP literal (parsed by the line 37 branch) is still run
  27. // through IsBlockedIP and rejected with the literal in the message.
  28. func TestSSRFGuardedDialContext_LiteralPrivateIPv6Blocked(t *testing.T) {
  29. _, err := SSRFGuardedDialContext(context.Background(), "tcp", "[::1]:80")
  30. if err == nil {
  31. t.Fatal("expected dial to ::1 to be blocked")
  32. }
  33. if !strings.Contains(err.Error(), "blocked private/internal address ::1") {
  34. t.Fatalf("expected '::1' literal in blocked error, got: %v", err)
  35. }
  36. }
  37. // TestNormalizeHost_LengthBoundary pins the netsafe.go:76 length check
  38. // (`len(addr) > 253`). A valid-pattern hostname of exactly 253 chars must be
  39. // accepted (kills `>` -> `>=` / off-by-one mutations of the bound), while the
  40. // same hostname at 254 chars must be rejected.
  41. func TestNormalizeHost_LengthBoundary(t *testing.T) {
  42. label := strings.Repeat("a", 61)
  43. base := label + "." + label + "." + label + "." // 186 chars, valid pattern
  44. h253 := base + strings.Repeat("a", 253-len(base))
  45. if len(h253) != 253 {
  46. t.Fatalf("test setup: expected 253-char host, got %d", len(h253))
  47. }
  48. h254 := h253 + "a"
  49. got, err := NormalizeHost(h253)
  50. if err != nil {
  51. t.Fatalf("NormalizeHost(253-char valid host) returned error: %v", err)
  52. }
  53. if got != h253 {
  54. t.Fatalf("NormalizeHost(253-char host) = %q, want unchanged input", got)
  55. }
  56. if _, err := NormalizeHost(h254); err == nil {
  57. t.Fatal("NormalizeHost(254-char host) expected error, got nil")
  58. }
  59. }
  60. // TestNormalizeHost_PatternClauseIndependentOfLength pins the OR in line 76:
  61. // a short hostname (well under the 253 limit) that violates the pattern must
  62. // still be rejected. If `||` were mutated to `&&`, this short-but-invalid host
  63. // would slip through because the length clause is false.
  64. func TestNormalizeHost_PatternClauseIndependentOfLength(t *testing.T) {
  65. cases := []string{
  66. "under_score.example.com",
  67. "bad host",
  68. "exa$mple.com",
  69. "-leadingdash.com",
  70. }
  71. for _, in := range cases {
  72. t.Run(in, func(t *testing.T) {
  73. if len(in) > 253 {
  74. t.Fatalf("test setup: %q should be short to isolate the pattern clause", in)
  75. }
  76. if _, err := NormalizeHost(in); err == nil {
  77. t.Fatalf("NormalizeHost(%q) expected error for invalid pattern, got nil", in)
  78. }
  79. })
  80. }
  81. }
  82. // TestNormalizeHost_ValidShortHostAccepted ensures a short valid-pattern host is
  83. // accepted, so a mutation dropping the `!` on the pattern match (rejecting valid
  84. // hosts) is caught alongside the rejection cases above.
  85. func TestNormalizeHost_ValidShortHostAccepted(t *testing.T) {
  86. const in = "node-1.example.com"
  87. got, err := NormalizeHost(in)
  88. if err != nil {
  89. t.Fatalf("NormalizeHost(%q) returned error: %v", in, err)
  90. }
  91. if got != in {
  92. t.Fatalf("NormalizeHost(%q) = %q, want %q", in, got, in)
  93. }
  94. }