diff --git a/README.md b/README.md index 96b4c1a..b129928 100644 --- a/README.md +++ b/README.md @@ -98,9 +98,7 @@ expiration 1704067200: **Whitelist patterns:** -- **Exact match**: `cdn.example.com` — matches only that host -- **Suffix match**: `.example.com` — matches `cdn.example.com`, - `images.example.com`, and `example.com` +- **Exact match only**: `cdn.example.com` — matches only that host ### Configuration diff --git a/config.example.yml b/config.example.yml index 1660cb4..44f9b94 100644 --- a/config.example.yml +++ b/config.example.yml @@ -13,8 +13,7 @@ state_dir: ./data # Generate with: openssl rand -base64 32 signing_key: "CHANGE_ME_generate_with_openssl_rand_base64_32" -# Hosts that don't require signatures -# Use "." prefix for wildcard subdomain matching (e.g., ".example.com" matches "cdn.example.com") +# Hosts that don't require signatures (exact match only) whitelist_hosts: - s3.sneak.cloud - static.sneak.cloud diff --git a/internal/imgcache/whitelist.go b/internal/imgcache/whitelist.go index df24be2..5baf101 100644 --- a/internal/imgcache/whitelist.go +++ b/internal/imgcache/whitelist.go @@ -6,22 +6,19 @@ import ( ) // HostWhitelist implements the Whitelist interface for checking allowed source hosts. +// Only exact host matches are supported. Leading dots in patterns are stripped +// (e.g. ".example.com" becomes an exact match for "example.com"). type HostWhitelist struct { - // exactHosts contains hosts that must match exactly (e.g., "cdn.example.com") - exactHosts map[string]struct{} - // suffixHosts contains domain suffixes to match (e.g., ".example.com" matches "cdn.example.com") - suffixHosts []string + // hosts contains hosts that must match exactly (e.g., "cdn.example.com") + hosts map[string]struct{} } // NewHostWhitelist creates a whitelist from a list of host patterns. -// Patterns starting with "." are treated as suffix matches. -// Examples: -// - "cdn.example.com" - exact match only -// - ".example.com" - matches cdn.example.com, images.example.com, etc. +// All patterns are treated as exact matches. Leading dots are stripped +// for backwards compatibility (e.g. ".example.com" matches "example.com" only). func NewHostWhitelist(patterns []string) *HostWhitelist { w := &HostWhitelist{ - exactHosts: make(map[string]struct{}), - suffixHosts: make([]string, 0), + hosts: make(map[string]struct{}), } for _, pattern := range patterns { @@ -30,17 +27,22 @@ func NewHostWhitelist(patterns []string) *HostWhitelist { continue } - if strings.HasPrefix(pattern, ".") { - w.suffixHosts = append(w.suffixHosts, pattern) - } else { - w.exactHosts[pattern] = struct{}{} + // Strip leading dot — suffix matching is not supported. + // ".example.com" is treated as exact match for "example.com". + pattern = strings.TrimPrefix(pattern, ".") + + if pattern == "" { + continue } + + w.hosts[pattern] = struct{}{} } return w } // IsWhitelisted checks if a URL's host is in the whitelist. +// Only exact host matches are supported. func (w *HostWhitelist) IsWhitelisted(u *url.URL) bool { if u == nil { return false @@ -51,32 +53,17 @@ func (w *HostWhitelist) IsWhitelisted(u *url.URL) bool { return false } - // Check exact match - if _, ok := w.exactHosts[host]; ok { - return true - } + _, ok := w.hosts[host] - // Check suffix match - 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 + return ok } // IsEmpty returns true if the whitelist has no entries. func (w *HostWhitelist) IsEmpty() bool { - return len(w.exactHosts) == 0 && len(w.suffixHosts) == 0 + return len(w.hosts) == 0 } // Count returns the total number of whitelist entries. func (w *HostWhitelist) Count() int { - return len(w.exactHosts) + len(w.suffixHosts) + return len(w.hosts) } diff --git a/internal/imgcache/whitelist_test.go b/internal/imgcache/whitelist_test.go index 3e33b66..b72c33c 100644 --- a/internal/imgcache/whitelist_test.go +++ b/internal/imgcache/whitelist_test.go @@ -31,41 +31,47 @@ func TestHostWhitelist_IsWhitelisted(t *testing.T) { want: false, }, { - name: "suffix match", + name: "dot prefix does not enable suffix matching", patterns: []string{".example.com"}, testURL: "https://cdn.example.com/image.jpg", - want: true, + want: false, }, { - name: "suffix match deep subdomain", + name: "dot prefix does not match deep subdomain", patterns: []string{".example.com"}, testURL: "https://cdn.images.example.com/image.jpg", - want: true, + want: false, }, { - name: "suffix match apex domain", + name: "dot prefix stripped matches apex domain exactly", patterns: []string{".example.com"}, testURL: "https://example.com/image.jpg", want: true, }, { - name: "suffix match not found", + name: "dot prefix does not match unrelated domain", patterns: []string{".example.com"}, testURL: "https://notexample.com/image.jpg", want: false, }, { - name: "suffix match partial not allowed", + name: "dot prefix does not match partial domain", patterns: []string{".example.com"}, testURL: "https://fakeexample.com/image.jpg", want: false, }, { - name: "multiple patterns", - patterns: []string{"cdn.example.com", ".images.org", "static.test.net"}, + name: "multiple patterns exact only", + patterns: []string{"cdn.example.com", "photos.images.org", "static.test.net"}, testURL: "https://photos.images.org/image.jpg", want: true, }, + { + name: "multiple patterns no suffix match", + patterns: []string{"cdn.example.com", ".images.org", "static.test.net"}, + testURL: "https://photos.images.org/image.jpg", + want: false, + }, { name: "empty whitelist", patterns: []string{}, @@ -90,6 +96,12 @@ func TestHostWhitelist_IsWhitelisted(t *testing.T) { testURL: "https://cdn.example.com/image.jpg", want: true, }, + { + name: "whitespace dot prefix stripped matches exactly", + patterns: []string{" .other.com "}, + testURL: "https://other.com/image.jpg", + want: true, + }, } for _, tt := range tests { @@ -139,6 +151,11 @@ func TestHostWhitelist_IsEmpty(t *testing.T) { patterns: []string{"example.com"}, want: false, }, + { + name: "dot prefix entry still counts", + patterns: []string{".example.com"}, + want: false, + }, } for _, tt := range tests { @@ -168,7 +185,7 @@ func TestHostWhitelist_Count(t *testing.T) { want: 3, }, { - name: "suffix hosts only", + name: "dot prefix hosts treated as exact", patterns: []string{".a.com", ".b.com"}, want: 2, }, @@ -177,6 +194,11 @@ func TestHostWhitelist_Count(t *testing.T) { patterns: []string{"exact.com", ".suffix.com"}, want: 2, }, + { + name: "dot prefix deduplicates with exact", + patterns: []string{"example.com", ".example.com"}, + want: 1, + }, } for _, tt := range tests {