Repository navigation
Description
Affected version: confirmed on commit 4bed89d8934147fa2db5f4ff4b935930ebe1cc1e (branch main).
Summary
picfit is an image-resizing server. Its /display, /get, and /redirect endpoints accept
a url query parameter and fetch that URL directly to use as the source image
(middleware/parsers.go's URLParser, backed by image.FromURL in processor.go), with no
restriction on the target host. Access to these endpoints is only gated by an optional HMAC
signature (middleware.Security), which is enforced solely when the operator sets a non-empty
secret_key in the server config; the sample config shipped in the repository
(provisioning/roles/picfit/files/etc/picfit/config.json) does not set one, and the README
does not document that secret_key needs to be set to avoid this. With no secret_key
configured, which is the server's effective default, any network-reachable, unauthenticated
caller can make the server issue outbound HTTP GET and HEAD requests to any host it specifies,
including loopback and RFC1918 private-network addresses.
Details
middleware/parsers.go:
// URLParser extracts the url query string and add a url.URL to the context
func URLParser(mimetypeDetectorType string, processor *picfit.Processor) gin.HandlerFunc {
mimetypeDetector := image.GetMimetypeDetector(mimetypeDetectorType)
return func(c *gin.Context) {
...
if value := c.Query("url"); value != "" {
url, err := url.Parse(value)
...
c.Set("url", url)
c.Set("mimetype", mimetype)
}
c.Next()
}
}No private-IP, loopback, or hostname check of any kind runs on the parsed url here.
processor.go's processImage then fetches it unconditionally via image.FromURL.
middleware/auth.go's Security middleware is the only access control on this path, and it
is a no-op unless secret_key is set:
func Security(secretKey string) gin.HandlerFunc {
return func(c *gin.Context) {
if secretKey != "" {
if !signature.VerifyParameters(secretKey, c.MustGet("parameters").(map[string]any)) {
c.String(http.StatusUnauthorized, "Invalid signature")
c.Abort()
return
}
}
c.Next()
}
}server/http.go wires middleware.Security(s.config.SecretKey) ahead of URLParser on the
/display, /get, and /redirect routes, but s.config.SecretKey is empty unless set in
config, and the shipped sample config does not set it.
Steps To Reproduce
(available upon request)
Impact
An unauthenticated remote attacker who can reach a default-configured picfit instance can
force it to make HTTP requests to internal-only services (localhost-bound admin APIs,
RFC1918-range services such as internal caches, databases, or metadata endpoints), and, since
the fetched content is decoded and processed as an image before being returned to the
caller, a reachable internal endpoint that happens to serve image bytes has its content
returned directly to the attacker through the resize/display response. Even where the
fetched content is not a valid image, the distinct "reached but undecodable" vs.
"unreachable" error behavior still gives an attacker a reachability/port-scanning oracle into
the internal network reachable from the server (CWE-918, server-side request forgery).
A fix should require secret_key (or an equivalent allowlist/signature mechanism) to be set
before accepting url requests at all, reject loopback and RFC1918 targets by default the way
many other image-proxy projects do (imgproxy, thumbor), and resolve hostnames before the
check to avoid DNS-rebinding bypasses.
Activity