From 27739da046fbf60e7c169e1bde185c2018edb28e Mon Sep 17 00:00:00 2001 From: user Date: Sun, 15 Mar 2026 11:18:01 -0700 Subject: [PATCH 1/3] test: add failing tests for removing suffix matching from whitelist Suffix matching (.example.com matching subdomains) should not be supported. Whitelist entries should be exact host matches only. Leading dots should be stripped and treated as exact matches. --- internal/imgcache/whitelist_test.go | 42 ++++++++++++++++++++++------- 1 file changed, 32 insertions(+), 10 deletions(-) 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 { -- 2.49.1 From 215ddb7f722841908cf9bc6f54a48b00343b1307 Mon Sep 17 00:00:00 2001 From: user Date: Sun, 15 Mar 2026 11:18:25 -0700 Subject: [PATCH 2/3] fix: remove suffix matching from host whitelist Whitelist entries now support exact host matches only. Leading dots in patterns are stripped for backwards compatibility (.example.com becomes an exact match for example.com). Suffix matching that would match arbitrary subdomains is no longer supported. Closes #27 --- internal/imgcache/whitelist.go | 53 +++++++++++++--------------------- 1 file changed, 20 insertions(+), 33 deletions(-) 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) } -- 2.49.1 From 55bb620de098c891f15b3314fa25a1226ef6ccd2 Mon Sep 17 00:00:00 2001 From: user Date: Sun, 15 Mar 2026 11:18:44 -0700 Subject: [PATCH 3/3] docs: update README and config to reflect exact-match-only whitelist Remove suffix match documentation and config comments since whitelist now only supports exact host matches. --- README.md | 4 +--- config.example.yml | 3 +-- 2 files changed, 2 insertions(+), 5 deletions(-) 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 -- 2.49.1