An image is checked before it is decoded
An upload went straight to libvips: whatever loader recognised the bytes ran (an SVG sent as image/png was rasterised), nothing bounded how many pixels it would decode to (a small PNG could decode to gigabytes, three times over), a GIF was loaded frame by frame and never resized, and all of it ran inside the request with nothing limiting how many at once. A GIF was typed gifv but stayed a .gif, which a gifv player can't play; its metadata was kept; colours lost their ICC profile without being converted; HEIC was advertised but the bundled libvips can't decode it. Now: - only libvips' JPEG, PNG, GIF, WebP and HEIF loaders ever run on an upload (every other loader is blocked); - the header alone says how big an image would decode, refused above Media:MaxPixels (40 MP) or MaxFrames; - a still image is shrunk on load, turned by its orientation and brought into sRGB (thumbnail), then written without metadata, a profile picture the same way; - an animated GIF becomes a looping silent H.264 mp4 typed gifv, as on Mastodon (PostMedia.Kind keeps it a gifv), and a remote GIF is an image; - processing runs Media:Concurrency at a time, and uploads have their own rate limit per credential; - HEIC and HEIF are no longer offered. Tests: only the upload formats load, the header tells the size, an SVG posing as a PNG and an image too large are refused before decoding, an animated GIF becomes a gifv and a still one an image, HEIC isn't advertised. The media scenarios against the pasture (GoToSocial, Mastodon, Misskey, Akkoma, Pixelfed, Smithereen, Vernissage, Castopod, PeerTube) pass: 329 checks. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01LsXgEaXee4GCU1hwYgPJXw
This commit is contained in:
1 parent
65938bb2a0
commit
e4f9d0b61e
14 files changed
+284
-55
No files matched your search
@@ -170,6 +170,40 @@ namespace PrivaPub.Tests.Http
|
||||
Assert.Equal(HttpStatusCode.UnprocessableEntity, (await alice.Client.Post("/api/v1/media")).Status);
|
||||
}
|
||||
|
||||
// refused before anything is decoded: an SVG claiming to be a PNG, and an image that would decode to too many pixels
|
||||
[Fact]
|
||||
public async Task An_image_that_is_not_one_or_too_large_is_refused_before_decoding()
|
||||
{
|
||||
var alice = await _host.Mastodon("alice");
|
||||
var svg = "<svg xmlns='http://www.w3.org/2000/svg' width='30000' height='30000'><rect width='1' height='1'/></svg>"u8.ToArray();
|
||||
Assert.Equal(HttpStatusCode.UnprocessableEntity, (await Upload(alice, "/api/v2/media", svg, "image/png", "a.png")).Status);
|
||||
|
||||
using var huge = NetVips.Image.Black(8000, 6000);
|
||||
var tooLarge = await Upload(alice, "/api/v2/media", huge.WriteToBuffer(".png"), "image/png", "huge.png");
|
||||
Assert.Equal(HttpStatusCode.UnprocessableEntity, tooLarge.Status);
|
||||
Assert.Contains("too large", tooLarge.Body.Text("error"));
|
||||
}
|
||||
|
||||
// an animated GIF becomes what Mastodon makes of one, a looping mp4 typed gifv; a still GIF stays an image
|
||||
[Fact]
|
||||
public async Task An_animated_gif_becomes_a_gifv_and_a_still_one_an_image()
|
||||
{
|
||||
var alice = await _host.Mastodon("alice");
|
||||
using var frame = (NetVips.Image.Black(64, 48, bands: 3) + 60).Cast(NetVips.Enums.BandFormat.Uchar);
|
||||
using var frames = NetVips.Image.Arrayjoin(new[] { frame, frame + 80, frame + 160 }, across: 1).Cast(NetVips.Enums.BandFormat.Uchar);
|
||||
using var animated = frames.Mutate(m => m.Set(NetVips.GValue.GIntType, "page-height", 48));
|
||||
|
||||
var gifv = (await Upload(alice, "/api/v2/media", animated.WriteToBuffer(".gif"), "image/gif", "dance.gif")).Ok();
|
||||
Assert.Equal("gifv", gifv.Body.Text("type"));
|
||||
Assert.EndsWith(".mp4", gifv.Body.Text("url"));
|
||||
Assert.Equal(64, gifv.Body["meta"]!["original"]!["width"]!.GetValue<int>());
|
||||
var status = (await alice.Client.Post("/api/v1/statuses", ("status", "dancing"), ("media_ids[]", gifv.Body.Text("id")))).Ok();
|
||||
Assert.Equal("gifv", status.Body["media_attachments"]![0]!.Text("type"));
|
||||
|
||||
var still = (await Upload(alice, "/api/v2/media", frame.WriteToBuffer(".gif"), "image/gif", "still.gif")).Ok();
|
||||
Assert.Equal("image", still.Body.Text("type"));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Video_is_remuxed_without_its_metadata()
|
||||
{
|
||||
|
||||
Reference in new issue
Block a user