3 Commits

Author SHA1 Message Date
user
55bb620de0 docs: update README and config to reflect exact-match-only whitelist
All checks were successful
check / check (push) Successful in 5s
Remove suffix match documentation and config comments since whitelist
now only supports exact host matches.
2026-03-15 11:18:44 -07:00
user
215ddb7f72 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
2026-03-15 11:18:25 -07:00
user
27739da046 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.
2026-03-15 11:18:01 -07:00
3 changed files with 53 additions and 34 deletions

View File

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

View File

@@ -5,20 +5,20 @@ import (
"strings"
)
// 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.
// 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{}
// hosts contains hosts that must match exactly (e.g., "cdn.example.com")
hosts map[string]struct{}
}
// NewHostWhitelist creates a whitelist from a list of hostnames.
// Each entry is treated as an exact host match. Leading dots are
// stripped so that legacy ".example.com" entries become "example.com".
// NewHostWhitelist creates a whitelist from a list of host patterns.
// 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{}),
hosts: make(map[string]struct{}),
}
for _, pattern := range patterns {
@@ -27,19 +27,22 @@ func NewHostWhitelist(patterns []string) *HostWhitelist {
continue
}
// Strip leading dot — suffix matching is no longer supported;
// ".example.com" is normalised to "example.com" as an exact entry.
// Strip leading dot — suffix matching is not supported.
// ".example.com" is treated as exact match for "example.com".
pattern = strings.TrimPrefix(pattern, ".")
if pattern != "" {
w.exactHosts[pattern] = struct{}{}
if pattern == "" {
continue
}
w.hosts[pattern] = struct{}{}
}
return w
}
// IsWhitelisted checks if a URL's host is in the whitelist (exact match only).
// 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
@@ -50,17 +53,17 @@ func (w *HostWhitelist) IsWhitelisted(u *url.URL) bool {
return false
}
_, ok := w.exactHosts[host]
_, ok := w.hosts[host]
return ok
}
// IsEmpty returns true if the whitelist has no entries.
func (w *HostWhitelist) IsEmpty() bool {
return len(w.exactHosts) == 0
return len(w.hosts) == 0
}
// Count returns the total number of whitelist entries.
func (w *HostWhitelist) Count() int {
return len(w.exactHosts)
return len(w.hosts)
}

View File

@@ -31,27 +31,33 @@ func TestHostWhitelist_IsWhitelisted(t *testing.T) {
want: false,
},
{
name: "no suffix matching for subdomains",
patterns: []string{"example.com"},
name: "dot prefix does not enable suffix matching",
patterns: []string{".example.com"},
testURL: "https://cdn.example.com/image.jpg",
want: false,
},
{
name: "leading dot stripped to exact match",
name: "dot prefix does not match deep subdomain",
patterns: []string{".example.com"},
testURL: "https://cdn.images.example.com/image.jpg",
want: false,
},
{
name: "dot prefix stripped matches apex domain exactly",
patterns: []string{".example.com"},
testURL: "https://example.com/image.jpg",
want: true,
},
{
name: "leading dot does not enable suffix matching",
name: "dot prefix does not match unrelated domain",
patterns: []string{".example.com"},
testURL: "https://cdn.example.com/image.jpg",
testURL: "https://notexample.com/image.jpg",
want: false,
},
{
name: "leading dot does not match deep subdomain",
name: "dot prefix does not match partial domain",
patterns: []string{".example.com"},
testURL: "https://cdn.images.example.com/image.jpg",
testURL: "https://fakeexample.com/image.jpg",
want: false,
},
{
@@ -61,8 +67,8 @@ func TestHostWhitelist_IsWhitelisted(t *testing.T) {
want: true,
},
{
name: "multiple patterns no suffix leak",
patterns: []string{"cdn.example.com", "images.org"},
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,
},
@@ -86,10 +92,16 @@ func TestHostWhitelist_IsWhitelisted(t *testing.T) {
},
{
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",
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 {
@@ -140,7 +152,7 @@ func TestHostWhitelist_IsEmpty(t *testing.T) {
want: false,
},
{
name: "leading dot normalised to entry",
name: "dot prefix entry still counts",
patterns: []string{".example.com"},
want: false,
},
@@ -173,12 +185,17 @@ func TestHostWhitelist_Count(t *testing.T) {
want: 3,
},
{
name: "leading dots normalised to exact",
name: "dot prefix hosts treated as exact",
patterns: []string{".a.com", ".b.com"},
want: 2,
},
{
name: "mixed deduplication",
name: "mixed",
patterns: []string{"exact.com", ".suffix.com"},
want: 2,
},
{
name: "dot prefix deduplicates with exact",
patterns: []string{"example.com", ".example.com"},
want: 1,
},