Refuse an image URL whose source path or query holds a # (closes #286)
check / check (push) Waiting to run

A # written as %23 reached the parser decoded and stayed in the source
path or query. The source URL built from it ended at the # when it was
fetched, so each text after it fetched the same upstream image, without
a signature for an allowlisted host, and cached it under keys of its
own. Such a URL is now refused with 400, and README.md says neither the
source path nor the source query may hold a #.

Model: opus-5-5
This commit is contained in:
2026-10-15 23:21:57 +00:00
parent 9119667c31
commit ca96d8da37
3 changed files with 16 additions and 2 deletions
+3 -2
View File
@@ -323,8 +323,9 @@ first `/`, the size and format after the last `/`, and between them the source
path up to the first `?` and the source query after it. So the `?` before the path up to the first `?` and the source query after it. So the `?` before the
query, any `?` in the query, and every `%` in the source path or query must be query, any `?` in the query, and every `%` in the source path or query must be
percent-encoded, as `%3F` and `%25`. Any other character may be written as percent-encoded, as `%3F` and `%25`. Any other character may be written as
itself or percent-encoded: `/`, `;`, `,`, `=` and `&` work either way. For itself or percent-encoded: `/`, `;`, `,`, `=` and `&` work either way. Neither
example, the source path nor the source query may hold a `#`, and an image URL that
writes one as `%23` is refused with 400. For example,
`https://cdn.example.com/img?url=https://origin.example.com/my%20cat.jpg` at `https://cdn.example.com/img?url=https://origin.example.com/my%20cat.jpg` at
800x600 WebP is 800x600 WebP is
`/v1/image/cdn.example.com/img%3Furl=https://origin.example.com/my%2520cat.jpg/800x600.webp`, `/v1/image/cdn.example.com/img%3Furl=https://origin.example.com/my%2520cat.jpg/800x600.webp`,
+5
View File
@@ -30,6 +30,11 @@ P2: security: per-origin rate limiting
# Completed Steps # Completed Steps
- 2026-10-15 a `/v1/image/` URL whose source path or source query holds a `#`,
written as `%23`, is refused with 400 (closes #286): the source URL built from
it ended at the `#` when fetched, so each text after it fetched the same
upstream image, without a signature for an allowlisted host, and cached it
under keys of its own. `README.md` says neither may hold a `#`.
- 2026-10-15 the host of a `/v1/image/` URL must be a host name or an IPv4 - 2026-10-15 the host of a `/v1/image/` URL must be a host name or an IPv4
address (letters, digits, hyphens, underscores and dots) or an IPv6 address in address (letters, digits, hyphens, underscores and dots) or an IPv6 address in
brackets, either one optionally followed by `:` and a port, and any other host brackets, either one optionally followed by `:` and a port, and any other host
+8
View File
@@ -171,6 +171,14 @@ func parseImageComponents(remainder string) (*ParsedURL, error) {
// host // host
host = strings.ToLower(host) host = strings.ToLower(host)
// Go's URL parser, which the fetcher uses, reads a "#" as the start of a
// fragment and drops the rest, so each text after it would be another
// spelling, cached under its own keys, of one upstream request
if strings.Contains(path, "#") {
return nil, fmt.Errorf(
`%w: the source path or query holds a "#"`, ErrInvalidPath)
}
// Extract query string if present in path // Extract query string if present in path
if qIndex := strings.Index(path, "?"); qIndex != -1 { if qIndex := strings.Index(path, "?"); qIndex != -1 {
query = path[qIndex+1:] query = path[qIndex+1:]