Lower-case the image URL's host and require a signature for a port (closes #246)

With cdn.example.com allowlisted, CDN.Example.com and
cdn.example.com:8443 passed the allowlist too, and each spelling had its
own cache entries and its own connections under
upstream_connections_per_host. The image URL parser now lower-cases the
host and refuses one holding "@" with ErrInvalidHost (400).
ValidateRequest counts a host followed by ":", with or without a port,
as not allowlisted, so it needs a signature. That check is not in
allowlist.IsAllowed, which also matches referer_blocklist. A URL is now
signed over the lower-cased host; README.md says so.

Model: opus-5-5
This commit is contained in:
2026-10-15 04:27:04 +00:00
parent ee974f67bf
commit 1dcf277b01
4 changed files with 38 additions and 2 deletions
+9 -1
View File
@@ -7,6 +7,7 @@ import (
"fmt"
"io"
"log/slog"
"net"
"net/url"
"runtime/debug"
"time"
@@ -218,7 +219,14 @@ func (s *Service) ValidateRequest(req *ImageRequest) error {
return fmt.Errorf("invalid source URL: %w", err)
}
if s.allowlist.IsAllowed(parsedURL) {
// An allowlist entry names no port, so a host followed by ":", with or
// without a port, needs a signature. This is checked here and not in
// allowlist.IsAllowed, which also matches referer_blocklist, where a
// Referer must be blocked whatever its port.
_, _, splitErr := net.SplitHostPort(parsedURL.Host)
hostHasPort := splitErr == nil
if !hostHasPort && s.allowlist.IsAllowed(parsedURL) {
return nil
}
+10
View File
@@ -144,6 +144,16 @@ func parseImageComponents(remainder string) (*ParsedURL, error) {
return nil, ErrMissingHost
}
// A URL reads what comes before "@" as a user name and password, and the
// rest as the host
if strings.Contains(host, "@") {
return nil, fmt.Errorf("%w: %q holds @", ErrInvalidHost, host)
}
// Host names do not depend on letter case, so the allowlist, the upstream
// connection limits and the cache see one spelling of each host
host = strings.ToLower(host)
// Extract query string if present in path
if qIndex := strings.Index(path, "?"); qIndex != -1 {
query = path[qIndex+1:]