Refuse an image URL whose source path or query holds a # (closes #286)
check / check (push) Waiting to run
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:
@@ -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`,
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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:]
|
||||||
|
|||||||
Reference in New Issue
Block a user