Skip to content

Commit c6812c1

Browse files
committed
refactor(fe): carry only the content type onto the capped blob
Rewrapping the capped stream with the whole original header set attaches a `Content-Length` and `Content-Encoding` that no longer describe it -- harmless for `.blob()`, but only by accident. The content type is the one header the blob actually needs, so that's all that comes along. Also assert what the decoder is handed: `createImageBitmap` must receive a `Blob`, which is what keeps the encoded image out of the JS heap. That property was easy to regress silently. Co-Authored-By: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01ApQTUxtZxjzpFsoJqKCXxo
1 parent 130f460 commit c6812c1

2 files changed

Lines changed: 9 additions & 3 deletions

File tree

src/frontend/src/lib/utils/appMetadata.test.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -120,14 +120,16 @@ test("should fetch a same-origin logo and render it from a blob url", async () =
120120
Response.json({ name: "Example App", logo: "/assets/logo.png" }),
121121
imageResponse(),
122122
);
123-
const { createObjectURL, toBlob } = setupImageMock();
123+
const { createImageBitmap, createObjectURL, toBlob } = setupImageMock();
124124

125125
const result = await fetchAppMetadata(ORIGIN);
126126

127127
expect(result).toEqual({
128128
name: "Example App",
129129
logo: LOGO_OBJECT_URL,
130130
});
131+
// The downloaded bytes reach the decoder as a blob, never as a JS buffer.
132+
expect(createImageBitmap).toHaveBeenCalledWith(expect.any(Blob));
131133
// The rendered bytes are II's own re-encoding, held in the browser's blob
132134
// store: no attacker-controlled payload reaches the DOM or the JS heap, as a
133135
// `data:` URL would.

src/frontend/src/lib/utils/appMetadata.ts

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -137,8 +137,12 @@ const readBodyCapped = async (
137137
}),
138138
);
139139
try {
140-
// The headers come along so the blob keeps the response's content type.
141-
return await new Response(capped, { headers: response.headers }).blob();
140+
// Only the content type is carried over, so the blob keeps it; copying the
141+
// rest would attach a `Content-Length` and `Content-Encoding` that no
142+
// longer describe this stream.
143+
return await new Response(capped, {
144+
headers: { "content-type": response.headers.get("content-type") ?? "" },
145+
}).blob();
142146
} catch {
143147
return undefined;
144148
}

0 commit comments

Comments
 (0)