The fetcher accepts image/svg+xml; internal/magic detects SVG. orig of an SVG at the orig size is the SVG unchanged, cached, with a Content-Security-Policy allowing only its styles and data: images; at another size, ErrSVGNotOriginalSize (400). Any other format renders it at the least size covering the output, at most 8192 on a side. Rendering refuses an SVG with a side over 8192 times the other (ErrSVGTooThin, 415), and the orig size of one above 8192 on a side (ErrSVGTooLargeForOrigSize, 400). Child processes of pixad read an SVG's size and render it, killed after 20 seconds and limited to 1 GiB; any failure there is ErrSVGRenderFailed (415). Tests pinning the SVG refusal now expect SVG. Model: opus-5-5
This commit was merged in pull request #351.
This commit is contained in:
1 parent
20874ba2af
commit
c2e32d987b
22 files changed
+1525
-131
No files matched your search
@@ -234,12 +234,8 @@ func (s *Handlers) HandleTooManyRequests() http.HandlerFunc {
|
||||
func (s *Handlers) respondImageError(
|
||||
w http.ResponseWriter, req *imagerequest.Request, err error,
|
||||
) {
|
||||
// An upstream body whose first bytes are not those of the format its
|
||||
// Content-Type names is the upstream's error, not pixa's: the image
|
||||
// service has logged it as a refused source.
|
||||
if errors.Is(err, magic.ErrMagicByteMismatch) ||
|
||||
errors.Is(err, magic.ErrUnknownFormat) ||
|
||||
errors.Is(err, magic.ErrNotEnoughData) {
|
||||
// The image service has logged this as a refused source
|
||||
if isFirstBytesMismatch(err) {
|
||||
s.respondError(w, "unsupported source format",
|
||||
http.StatusUnsupportedMediaType)
|
||||
|
||||
@@ -269,11 +265,29 @@ func (s *Handlers) respondImageError(
|
||||
errors.Is(err, imageprocessor.ErrUnsupportedInputFormat):
|
||||
status = http.StatusUnsupportedMediaType
|
||||
message = "unsupported source format"
|
||||
case errors.Is(err, imageprocessor.ErrSVGTooThin):
|
||||
status = http.StatusUnsupportedMediaType
|
||||
message = fmt.Sprintf("SVG source too thin to render: one side is "+
|
||||
"more than %d times the other", imageprocessor.MaxInputDimension)
|
||||
case errors.Is(err, imageprocessor.ErrSVGRenderFailed):
|
||||
status = http.StatusUnsupportedMediaType
|
||||
message = fmt.Sprintf("SVG source could not be rendered: it is not "+
|
||||
"valid, or it takes more than %d seconds or %d GiB of memory to "+
|
||||
"render", int(imageprocessor.SVGRenderTimeout.Seconds()),
|
||||
imageprocessor.MaxSVGRenderMemory/bytesPerGiB)
|
||||
case errors.Is(err, imageprocessor.ErrOutputTooLarge):
|
||||
status = http.StatusBadRequest
|
||||
message = fmt.Sprintf("size too large for the source: the side "+
|
||||
"computed from its proportions is above %d",
|
||||
imageprocessor.MaxOutputDimension)
|
||||
case errors.Is(err, imageprocessor.ErrSVGNotOriginalSize):
|
||||
status = http.StatusBadRequest
|
||||
message = "an SVG source is returned as SVG only at the orig size; " +
|
||||
"ask for another format to resize it"
|
||||
case errors.Is(err, imageprocessor.ErrSVGTooLargeForOrigSize):
|
||||
status = http.StatusBadRequest
|
||||
message = fmt.Sprintf("SVG source above %d on a side: give a size "+
|
||||
"to render it at", imageprocessor.MaxOutputDimension)
|
||||
case errors.Is(err, imageprocessor.ErrAVIFOutputTooLarge):
|
||||
status = http.StatusBadRequest
|
||||
message = fmt.Sprintf("size too large for AVIF: the output "+
|
||||
@@ -297,6 +311,18 @@ func (s *Handlers) respondImageError(
|
||||
s.respondError(w, message, status)
|
||||
}
|
||||
|
||||
// bytesPerGiB is the number of bytes in a GiB.
|
||||
const bytesPerGiB = 1 << 30
|
||||
|
||||
// isFirstBytesMismatch reports whether err refuses an upstream body whose
|
||||
// first bytes are not those of the format its Content-Type names. That is the
|
||||
// upstream's error, not pixa's.
|
||||
func isFirstBytesMismatch(err error) bool {
|
||||
return errors.Is(err, magic.ErrMagicByteMismatch) ||
|
||||
errors.Is(err, magic.ErrUnknownFormat) ||
|
||||
errors.Is(err, magic.ErrNotEnoughData)
|
||||
}
|
||||
|
||||
// cacheControl returns the Cache-Control header for an image served through a
|
||||
// URL that expires at expires, or never when expires is the zero time. A cache
|
||||
// may keep the image for a year, but not past the URL's expiry, after which
|
||||
@@ -315,6 +341,26 @@ func cacheControl(expires time.Time) string {
|
||||
return fmt.Sprintf("public, max-age=%d, immutable", int64(maxAge/time.Second))
|
||||
}
|
||||
|
||||
// svgContentSecurityPolicyValue is the Content-Security-Policy header value
|
||||
// of an SVG response, in place of the middleware's. A browser that opens the
|
||||
// SVG applies its styles and draws its embedded data: images, but runs none
|
||||
// of its scripts and loads nothing else. The styles need unsafe-inline, as an
|
||||
// SVG keeps them inline, in style elements and attributes.
|
||||
const svgContentSecurityPolicyValue = "default-src 'none'; " +
|
||||
"style-src 'unsafe-inline'; " +
|
||||
"img-src data:; " +
|
||||
"frame-ancestors 'none'; " +
|
||||
"sandbox"
|
||||
|
||||
// setSVGContentSecurityPolicy sets the Content-Security-Policy in header to
|
||||
// svgContentSecurityPolicyValue when contentType is that of SVG. Any other
|
||||
// response keeps the middleware's policy.
|
||||
func setSVGContentSecurityPolicy(header http.Header, contentType string) {
|
||||
if contentType == imagerequest.FormatSVG.MIMEType() {
|
||||
header.Set("Content-Security-Policy", svgContentSecurityPolicyValue)
|
||||
}
|
||||
}
|
||||
|
||||
// refuseBlockedReferer answers 403 with a JSON error when the request's Referer
|
||||
// names a host on referer_blocklist, and reports whether it answered. A request
|
||||
// with no Referer, or one that does not parse as a URL with a host, is not
|
||||
@@ -397,6 +443,7 @@ func (s *Handlers) serveImage(
|
||||
|
||||
// Set response headers
|
||||
w.Header().Set("Content-Type", resp.ContentType)
|
||||
setSVGContentSecurityPolicy(w.Header(), resp.ContentType)
|
||||
|
||||
if resp.ContentLength > 0 {
|
||||
w.Header().Set("Content-Length", strconv.FormatInt(resp.ContentLength, 10))
|
||||
|
||||
Reference in new issue
Block a user