Decide on SVG: it is accepted and rendered by libvips, and format=orig silently returns JPEG #68

Closed
opened 2026-08-09 03:44:54 +02:00 by clawbot · 4 comments
Collaborator

Verified against main at 61f42e6.

image/svg+xml is allow-listed by the fetcher (internal/httpfetcher/httpfetcher.go:100) and validated as acceptable by internal/magic/magic.go:114-116,180, so SVG bytes reach vips.NewImageFromBuffer (internal/imageprocessor/imageprocessor.go:154).

Two problems:

1. Security surface. SVG rendering in libvips goes through librsvg, and SVG is an XML format: external entity references, remote resource loading, and billion-laughs style expansion are the standard attack surface, on input fetched from an arbitrary upstream. Notably, SupportedInputFormats() (imageprocessor.go:221-229) deliberately does not list SVG — so the fetcher accepts a format the processor does not claim to support.

2. Silent format substitution, which is a repo-rule violation. detectFormat (imageprocessor.go:261-277) has no SVG case, so it returns "unknown", and formatFromString("unknown") returns FormatJPEG (:396). A request for orig on an SVG source therefore silently returns a JPEG. Repo rules are explicit: never silently fall back to a different setting when a parameter specifies a value — format=orig producing JPEG is exactly that defect.

Uncertain and worth checking first: whether the libvips build produced by script/bootstrap's environment actually has librsvg support compiled in. That determines whether problem 1 is live or latent, but problem 2 is real either way.

Definition of done

Pick one direction and implement it fully:

  • (a) Drop SVG — remove image/svg+xml from the fetcher's allowed content types and from magic, and return a clear 415 for SVG sources. Simplest and safest; SVG does not need rasterizing to be served by a CDN.
  • (b) Support SVG properly — add it to SupportedInputFormats(), add an SVG case to detectFormat, confirm librsvg is present and configured with external entity/resource loading disabled, and bound rasterization dimensions.

Either way:

  1. format=orig on an SVG source must never silently return a different format — it either works and returns SVG, or returns an explicit error. No JPEG fallback.
  2. Audit formatFromString's default-to-JPEG behavior (imageprocessor.go:396) for any other input that can reach it as "unknown"; a silent default there is the same class of bug.
  3. Failing tests first: an SVG source with format=orig does not return image/jpeg; and whichever of (a)/(b) is chosen is asserted directly.
  4. make check green.

Recommendation: (a). Rasterizing untrusted XML is a large attack surface for a feature nothing in the README promises.

Verified against `main` at `61f42e6`. `image/svg+xml` is allow-listed by the fetcher (`internal/httpfetcher/httpfetcher.go:100`) and validated as acceptable by `internal/magic/magic.go:114-116,180`, so SVG bytes reach `vips.NewImageFromBuffer` (`internal/imageprocessor/imageprocessor.go:154`). Two problems: **1. Security surface.** SVG rendering in libvips goes through librsvg, and SVG is an XML format: external entity references, remote resource loading, and billion-laughs style expansion are the standard attack surface, on input fetched from an arbitrary upstream. Notably, `SupportedInputFormats()` (`imageprocessor.go:221-229`) deliberately does **not** list SVG — so the fetcher accepts a format the processor does not claim to support. **2. Silent format substitution, which is a repo-rule violation.** `detectFormat` (`imageprocessor.go:261-277`) has no SVG case, so it returns `"unknown"`, and `formatFromString("unknown")` returns `FormatJPEG` (`:396`). A request for `orig` on an SVG source therefore silently returns a JPEG. Repo rules are explicit: never silently fall back to a different setting when a parameter specifies a value — `format=orig` producing JPEG is exactly that defect. **Uncertain and worth checking first:** whether the libvips build produced by `script/bootstrap`'s environment actually has librsvg support compiled in. That determines whether problem 1 is live or latent, but problem 2 is real either way. ## Definition of done Pick one direction and implement it fully: - **(a) Drop SVG** — remove `image/svg+xml` from the fetcher's allowed content types and from `magic`, and return a clear 415 for SVG sources. Simplest and safest; SVG does not need rasterizing to be served by a CDN. - **(b) Support SVG properly** — add it to `SupportedInputFormats()`, add an SVG case to `detectFormat`, confirm librsvg is present and configured with external entity/resource loading disabled, and bound rasterization dimensions. Either way: 1. `format=orig` on an SVG source must never silently return a different format — it either works and returns SVG, or returns an explicit error. No JPEG fallback. 2. Audit `formatFromString`'s default-to-JPEG behavior (`imageprocessor.go:396`) for any other input that can reach it as `"unknown"`; a silent default there is the same class of bug. 3. Failing tests first: an SVG source with `format=orig` does not return `image/jpeg`; and whichever of (a)/(b) is chosen is asserted directly. 4. `make check` green. **Recommendation: (a).** Rasterizing untrusted XML is a large attack surface for a feature nothing in the README promises.
clawbot added this to the 1.0.0 milestone 2026-08-09 03:44:54 +02:00
Author
Collaborator

Decision requested. Two options are in the body; the record has no ruling.

  • (a) Drop SVG: reject image/svg+xml at the fetcher with a 415, remove it from magic, and make format=orig on any unrecognized input an explicit error instead of a JPEG.
  • (b) Support SVG: add it to the processor's supported inputs, confirm librsvg is present with external resource loading disabled, bound rasterization dimensions.

Recommendation: (a). Rasterizing untrusted XML is a large attack surface for a feature the README does not promise, and (a) is a small change that also fixes the silent JPEG fallback. Not dispatched until you rule; everything else proceeds.

model: claude-fable-5

Decision requested. Two options are in the body; the record has no ruling. - (a) Drop SVG: reject `image/svg+xml` at the fetcher with a 415, remove it from `magic`, and make `format=orig` on any unrecognized input an explicit error instead of a JPEG. - (b) Support SVG: add it to the processor's supported inputs, confirm librsvg is present with external resource loading disabled, bound rasterization dimensions. Recommendation: (a). Rasterizing untrusted XML is a large attack surface for a feature the README does not promise, and (a) is a small change that also fixes the silent JPEG fallback. Not dispatched until you rule; everything else proceeds. model: claude-fable-5
sneak was assigned by clawbot 2026-09-21 09:20:43 +02:00
Author
Collaborator

Still current on next at 162a2a3: the fetcher and magic accept SVG, orig of an input the processor cannot name is still saved as JPEG (formatFromString), and README.md now lists SVG among the accepted sources. Decided as the recommended option (a), by the standing rule to build the recommendation: an SVG source is refused with 415, SVG leaves magic, the fetcher and README.md, and orig of an input the processor cannot name is an explicit error. Supporting SVG would be a new feature for the owner to approve.

Model: opus-5-5

Still current on `next` at `162a2a3`: the fetcher and `magic` accept SVG, `orig` of an input the processor cannot name is still saved as JPEG (`formatFromString`), and `README.md` now lists SVG among the accepted sources. Decided as the recommended option (a), by the standing rule to build the recommendation: an SVG source is refused with 415, SVG leaves `magic`, the fetcher and `README.md`, and `orig` of an input the processor cannot name is an explicit error. Supporting SVG would be a new feature for the owner to approve. Model: opus-5-5
sneak was unassigned by clawbot 2026-10-13 03:16:52 +02:00
clawbot self-assigned this 2026-10-13 03:16:52 +02:00
Author
Collaborator

Building option (a) needs existing tests changed, which CLAUDE.md leaves to you. I recommend this change in internal/magic/magic_internal_test.go, so that SVG leaves internal/magic (MIMETypeSVG, detectSVG, and skipBOM, which only detectSVG uses):

  • TestIsSupportedMIMEType: {"image/svg+xml", true} becomes false.
  • TestDetectFormat: its four SVG cases are removed; SVG bytes then get ErrUnknownFormat.
  • TestDetectSVG, TestSkipBOM and TestRealWorldSVGPatterns are removed with the code they test.

Without it, internal/magic still lists SVG as a supported input, but SVG is refused anyway: nothing outside the tests calls IsSupportedMIMEType, and the fetcher no longer accepts image/svg+xml.

The rest is on the branch issue-68-refuse-svg, with no PR yet: the fetcher refuses SVG, both image routes answer 415 for it, orig of an input the processor cannot name is an error (also 415) instead of a JPEG, and README.md drops SVG.

  • Finding: the libvips that script/bootstrap installs in the test phase renders SVG; on next, an SVG source asked for as orig came back as image/jpeg.
  • Judgement call: every upstream Content-Type outside the accepted list now answers 415, where it answered 500.
  • formatFromString has one caller, Process; once SVG is refused, no source the routes accept reaches it unnamed, so the new error is a guard.
  • Unchanged: an SVG sent under another image type fails the first-bytes check and still answers 500.

Model: opus-5-5

Building option (a) needs existing tests changed, which `CLAUDE.md` leaves to you. I recommend this change in `internal/magic/magic_internal_test.go`, so that SVG leaves `internal/magic` (`MIMETypeSVG`, `detectSVG`, and `skipBOM`, which only `detectSVG` uses): - `TestIsSupportedMIMEType`: `{"image/svg+xml", true}` becomes `false`. - `TestDetectFormat`: its four SVG cases are removed; SVG bytes then get `ErrUnknownFormat`. - `TestDetectSVG`, `TestSkipBOM` and `TestRealWorldSVGPatterns` are removed with the code they test. Without it, `internal/magic` still lists SVG as a supported input, but SVG is refused anyway: nothing outside the tests calls `IsSupportedMIMEType`, and the fetcher no longer accepts `image/svg+xml`. The rest is on the branch [`issue-68-refuse-svg`](https://git.eeqj.de/sneak/pixa/src/branch/issue-68-refuse-svg), with no PR yet: the fetcher refuses SVG, both image routes answer 415 for it, `orig` of an input the processor cannot name is an error (also 415) instead of a JPEG, and `README.md` drops SVG. - Finding: the libvips that `script/bootstrap` installs in the test phase renders SVG; on `next`, an SVG source asked for as `orig` came back as `image/jpeg`. - Judgement call: every upstream `Content-Type` outside the accepted list now answers 415, where it answered 500. - `formatFromString` has one caller, `Process`; once SVG is refused, no source the routes accept reaches it unnamed, so the new error is a guard. - Unchanged: an SVG sent under another image type fails the first-bytes check and still answers 500. Model: opus-5-5
clawbot removed their assignment 2026-10-13 04:16:33 +02:00
sneak was assigned by clawbot 2026-10-13 04:16:33 +02:00
Author
Collaborator

Not needed now: the refusal of SVG, the 415 and the end of the silent JPEG are on the branch without changing an existing test, and they close this issue. The SVG detection left in internal/magic is not called on the request path; it goes with the unused code listed in #168, whose approval covers its tests. The PR opens from the branch with that as a disclosure.

Model: opus-5-5

Not needed now: the refusal of SVG, the 415 and the end of the silent JPEG are on the branch without changing an existing test, and they close this issue. The SVG detection left in `internal/magic` is not called on the request path; it goes with the unused code listed in https://git.eeqj.de/sneak/pixa/issues/168, whose approval covers its tests. The PR opens from the branch with that as a disclosure. Model: opus-5-5
sneak was unassigned by clawbot 2026-10-13 04:18:13 +02:00
clawbot self-assigned this 2026-10-13 04:18:13 +02:00
Sign in to join this conversation.
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: sneak/pixa#68