Strip metadata from processed images (closes #82) #145

Merged
clawbot merged 2 commits from issue-82-strip-metadata into next 2026-09-29 02:18:26 +02:00
3 changed files with 43 additions and 1 deletions
Showing only changes of commit f5902f4241 - Show all commits
+15
View File
@@ -111,6 +111,21 @@ with a private address can choose the address it is counted by through its own
its own address is trusted too. Setting `trusted_proxies` to the proxy's own its own address is trusted too. Setting `trusted_proxies` to the proxy's own
address closes this. address closes this.
### Image Metadata
pixa decodes and re-encodes every image it serves, and removes all metadata from
the output: EXIF (GPS position, camera make, model and serial number, capture
time, embedded thumbnail), XMP, IPTC and the ICC colour profile. This cannot be
turned off.
- The `orig` format means the source's own format, not the source's bytes: an
`orig` image is re-encoded and stripped like any other.
- An image with an EXIF orientation is turned upright first, so it displays the
same without the tag; a requested size applies to the upright image.
- An image with an ICC profile is converted to sRGB first, since clients show an
image with no profile as sRGB. Colours outside sRGB, such as the most
saturated ones in a Display P3 photo, are clipped.
### Source Hosts ### Source Hosts
Source hosts may be allowlisted in the configuration. Non-allowlisted Source hosts may be allowlisted in the configuration. Non-allowlisted
+6 -1
View File
@@ -30,6 +30,12 @@ exhaustion
# Completed Steps # Completed Steps
- 2026-09-28 strip metadata from processed images (closes #82): every output is
exported with govips' `StripMetadata`, so it carries no EXIF, XMP, IPTC or ICC
profile; the image is first turned upright with `AutoRotate` (before sizes are
worked out) and, when it has an ICC profile, converted to sRGB; the `orig`
format is re-encoded and stripped like any other, as pixa never serves the
source bytes; there is no setting to keep metadata; documented in `README.md`.
- 2026-09-28 rate limit the login form (closes #66): `POST /` is limited to 5 - 2026-09-28 rate limit the login form (closes #66): `POST /` is limited to 5
attempts per minute per client address, and an attempt over the limit is attempts per minute per client address, and an attempt over the limit is
refused with 429 and a `Retry-After` header; the address is the one refused with 429 and a `Retry-After` header; the address is the one
@@ -251,7 +257,6 @@ exhaustion
# Future Steps # Future Steps
- P1: strip EXIF and other metadata from processed images (privacy)
- P2: security - P2: security
- referer blacklist - referer blacklist
- per-IP rate limiting on the image routes - per-IP rate limiting on the image routes
+22
View File
@@ -161,6 +161,13 @@ func (p *ImageProcessor) Process(
} }
defer img.Close() defer img.Close()
// Turn the image upright now: encode strips the EXIF orientation tag,
// and sizes below must be worked out on the upright image.
err = img.AutoRotate()
if err != nil {
return nil, fmt.Errorf("failed to auto-rotate: %w", err)
}
// Get original dimensions // Get original dimensions
origWidth := img.Width() origWidth := img.Width()
origHeight := img.Height() origHeight := img.Height()
@@ -404,6 +411,21 @@ func (p *ImageProcessor) encode(
return nil, fmt.Errorf("%w: %s", ErrUnsupportedOutputFormat, format) return nil, fmt.Errorf("%w: %s", ErrUnsupportedOutputFormat, format)
} }
// Stripping drops the ICC profile as well, and clients show an image
// with no profile as sRGB, so convert to sRGB first. "srgb" names
// libvips' built-in profile; govips' own sRGB path variable is set on
// first use but read without a lock, so concurrent requests race on it.
if img.HasICCProfile() {
err := img.TransformICCProfileWithFallback("srgb", "srgb")
if err != nil {
return nil, fmt.Errorf("failed to convert to sRGB: %w", err)
}
}
// Drop EXIF, XMP, IPTC and the ICC profile. govips ignores this for
// GIF, which carries none of them.
params.StripMetadata = true
output, _, err := img.Export(&params) output, _, err := img.Export(&params)
if err != nil { if err != nil {
return nil, err return nil, err