remove suffix matching from host whitelist
All checks were successful
check / check (push) Successful in 1m50s

Signatures are per-URL, so the whitelist should only support exact host
matches. Remove the suffix/wildcard matching that allowed patterns like
'.example.com' to bypass signature requirements for entire domain trees.

Leading dots in existing config entries are now stripped, so '.example.com'
becomes 'example.com' as an exact match (backwards-compatible normalisation).
This commit is contained in:
user
2026-03-17 01:55:19 -07:00
parent 2e934c8894
commit e241b99d22
4 changed files with 48 additions and 61 deletions

View File

@@ -96,11 +96,10 @@ expiration 1704067200:
4. URL: 4. URL:
`/v1/image/cdn.example.com/photos/cat.jpg/800x600.webp?sig=<base64url>&exp=1704067200` `/v1/image/cdn.example.com/photos/cat.jpg/800x600.webp?sig=<base64url>&exp=1704067200`
**Whitelist patterns:** **Whitelist entries** are exact host matches only (e.g.
`cdn.example.com`). Suffix/wildcard matching is not supported —
- **Exact match**: `cdn.example.com` — matches only that host signatures are per-URL, so each allowed host must be listed
- **Suffix match**: `.example.com` — matches `cdn.example.com`, explicitly.
`images.example.com`, and `example.com`
### Configuration ### Configuration

View File

@@ -13,8 +13,7 @@ state_dir: ./data
# Generate with: openssl rand -base64 32 # Generate with: openssl rand -base64 32
signing_key: "CHANGE_ME_generate_with_openssl_rand_base64_32" signing_key: "CHANGE_ME_generate_with_openssl_rand_base64_32"
# Hosts that don't require signatures # Hosts that don't require signatures (exact match only)
# Use "." prefix for wildcard subdomain matching (e.g., ".example.com" matches "cdn.example.com")
whitelist_hosts: whitelist_hosts:
- s3.sneak.cloud - s3.sneak.cloud
- static.sneak.cloud - static.sneak.cloud

View File

@@ -5,23 +5,20 @@ import (
"strings" "strings"
) )
// HostWhitelist implements the Whitelist interface for checking allowed source hosts. // HostWhitelist checks whether a source host is allowed without a signature.
// Only exact host matches are supported. Signatures are per-URL, so
// wildcard/suffix matching is intentionally not provided.
type HostWhitelist struct { type HostWhitelist struct {
// exactHosts contains hosts that must match exactly (e.g., "cdn.example.com") // exactHosts contains hosts that must match exactly (e.g., "cdn.example.com")
exactHosts map[string]struct{} exactHosts map[string]struct{}
// suffixHosts contains domain suffixes to match (e.g., ".example.com" matches "cdn.example.com")
suffixHosts []string
} }
// NewHostWhitelist creates a whitelist from a list of host patterns. // NewHostWhitelist creates a whitelist from a list of hostnames.
// Patterns starting with "." are treated as suffix matches. // Each entry is treated as an exact host match. Leading dots are
// Examples: // stripped so that legacy ".example.com" entries become "example.com".
// - "cdn.example.com" - exact match only
// - ".example.com" - matches cdn.example.com, images.example.com, etc.
func NewHostWhitelist(patterns []string) *HostWhitelist { func NewHostWhitelist(patterns []string) *HostWhitelist {
w := &HostWhitelist{ w := &HostWhitelist{
exactHosts: make(map[string]struct{}), exactHosts: make(map[string]struct{}),
suffixHosts: make([]string, 0),
} }
for _, pattern := range patterns { for _, pattern := range patterns {
@@ -30,9 +27,11 @@ func NewHostWhitelist(patterns []string) *HostWhitelist {
continue continue
} }
if strings.HasPrefix(pattern, ".") { // Strip leading dot — suffix matching is no longer supported;
w.suffixHosts = append(w.suffixHosts, pattern) // ".example.com" is normalised to "example.com" as an exact entry.
} else { pattern = strings.TrimPrefix(pattern, ".")
if pattern != "" {
w.exactHosts[pattern] = struct{}{} w.exactHosts[pattern] = struct{}{}
} }
} }
@@ -40,7 +39,7 @@ func NewHostWhitelist(patterns []string) *HostWhitelist {
return w return w
} }
// IsWhitelisted checks if a URL's host is in the whitelist. // IsWhitelisted checks if a URL's host is in the whitelist (exact match only).
func (w *HostWhitelist) IsWhitelisted(u *url.URL) bool { func (w *HostWhitelist) IsWhitelisted(u *url.URL) bool {
if u == nil { if u == nil {
return false return false
@@ -51,32 +50,17 @@ func (w *HostWhitelist) IsWhitelisted(u *url.URL) bool {
return false return false
} }
// Check exact match _, ok := w.exactHosts[host]
if _, ok := w.exactHosts[host]; ok {
return true
}
// Check suffix match return ok
for _, suffix := range w.suffixHosts {
if strings.HasSuffix(host, suffix) {
return true
}
// Also match if host equals the suffix without the leading dot
// e.g., pattern ".example.com" should match "example.com"
if host == strings.TrimPrefix(suffix, ".") {
return true
}
}
return false
} }
// IsEmpty returns true if the whitelist has no entries. // IsEmpty returns true if the whitelist has no entries.
func (w *HostWhitelist) IsEmpty() bool { func (w *HostWhitelist) IsEmpty() bool {
return len(w.exactHosts) == 0 && len(w.suffixHosts) == 0 return len(w.exactHosts) == 0
} }
// Count returns the total number of whitelist entries. // Count returns the total number of whitelist entries.
func (w *HostWhitelist) Count() int { func (w *HostWhitelist) Count() int {
return len(w.exactHosts) + len(w.suffixHosts) return len(w.exactHosts)
} }

View File

@@ -31,41 +31,41 @@ func TestHostWhitelist_IsWhitelisted(t *testing.T) {
want: false, want: false,
}, },
{ {
name: "suffix match", name: "no suffix matching for subdomains",
patterns: []string{".example.com"}, patterns: []string{"example.com"},
testURL: "https://cdn.example.com/image.jpg", testURL: "https://cdn.example.com/image.jpg",
want: true, want: false,
}, },
{ {
name: "suffix match deep subdomain", name: "leading dot stripped to exact match",
patterns: []string{".example.com"},
testURL: "https://cdn.images.example.com/image.jpg",
want: true,
},
{
name: "suffix match apex domain",
patterns: []string{".example.com"}, patterns: []string{".example.com"},
testURL: "https://example.com/image.jpg", testURL: "https://example.com/image.jpg",
want: true, want: true,
}, },
{ {
name: "suffix match not found", name: "leading dot does not enable suffix matching",
patterns: []string{".example.com"}, patterns: []string{".example.com"},
testURL: "https://notexample.com/image.jpg", testURL: "https://cdn.example.com/image.jpg",
want: false, want: false,
}, },
{ {
name: "suffix match partial not allowed", name: "leading dot does not match deep subdomain",
patterns: []string{".example.com"}, patterns: []string{".example.com"},
testURL: "https://fakeexample.com/image.jpg", testURL: "https://cdn.images.example.com/image.jpg",
want: false, want: false,
}, },
{ {
name: "multiple patterns", name: "multiple patterns exact only",
patterns: []string{"cdn.example.com", ".images.org", "static.test.net"}, patterns: []string{"cdn.example.com", "photos.images.org", "static.test.net"},
testURL: "https://photos.images.org/image.jpg", testURL: "https://photos.images.org/image.jpg",
want: true, want: true,
}, },
{
name: "multiple patterns no suffix leak",
patterns: []string{"cdn.example.com", "images.org"},
testURL: "https://photos.images.org/image.jpg",
want: false,
},
{ {
name: "empty whitelist", name: "empty whitelist",
patterns: []string{}, patterns: []string{},
@@ -86,7 +86,7 @@ func TestHostWhitelist_IsWhitelisted(t *testing.T) {
}, },
{ {
name: "whitespace in patterns", name: "whitespace in patterns",
patterns: []string{" cdn.example.com ", " .other.com "}, patterns: []string{" cdn.example.com ", " other.com "},
testURL: "https://cdn.example.com/image.jpg", testURL: "https://cdn.example.com/image.jpg",
want: true, want: true,
}, },
@@ -139,6 +139,11 @@ func TestHostWhitelist_IsEmpty(t *testing.T) {
patterns: []string{"example.com"}, patterns: []string{"example.com"},
want: false, want: false,
}, },
{
name: "leading dot normalised to entry",
patterns: []string{".example.com"},
want: false,
},
} }
for _, tt := range tests { for _, tt := range tests {
@@ -168,14 +173,14 @@ func TestHostWhitelist_Count(t *testing.T) {
want: 3, want: 3,
}, },
{ {
name: "suffix hosts only", name: "leading dots normalised to exact",
patterns: []string{".a.com", ".b.com"}, patterns: []string{".a.com", ".b.com"},
want: 2, want: 2,
}, },
{ {
name: "mixed", name: "mixed deduplication",
patterns: []string{"exact.com", ".suffix.com"}, patterns: []string{"example.com", ".example.com"},
want: 2, want: 1,
}, },
} }