From 81bcae7b7ca68255bfa054dbb77707c7c10ba271 Mon Sep 17 00:00:00 2001 From: Chirag Aggarwal Date: Sat, 24 Oct 2026 20:11:17 +0100 Subject: [PATCH 1/2] fix: patch the Brotli decoder for CVE-2026-111497 --- Dockerfile | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/Dockerfile b/Dockerfile index 6f1f518..e5fe042 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,5 +1,5 @@ # debian so we can re-use! -FROM oven/bun:1.3.2-debian AS base +FROM oven/bun:1.4.4-debian AS base WORKDIR /app @@ -11,16 +11,19 @@ RUN bun install --frozen-lockfile --production && \ rm -rf ~/.bun/install/cache /tmp/* # well-known OSS docker image -FROM chromedp/headless-shell:143.0.7445.3 AS final +FROM chromedp/headless-shell:157.0.8103.15 AS final -# install fonts only +# install fonts, and the patched Brotli decoder RUN apt-get update && \ apt-get install -y --no-install-recommends \ tini \ ca-certificates \ fonts-liberation \ fonts-noto-color-emoji \ - fonts-wqy-zenhei && \ + fonts-wqy-zenhei \ + libbrotli1 && \ + # CVE-2026-111497: refuse to build on a Brotli decoder older than the Debian fix + dpkg --compare-versions "$(dpkg-query -W -f='${Version}' libbrotli1)" ge 1.1.0-2+deb13u1 && \ apt-get clean && \ rm -rf /var/lib/apt/lists/* /tmp/* /var/tmp/* /var/cache/apt/archives/* From b17e41ef98f5c527a601718ddd6abb90a066d20a Mon Sep 17 00:00:00 2001 From: Chirag Aggarwal Date: Sat, 24 Oct 2026 20:17:17 +0100 Subject: [PATCH 2/2] test: deflake the full-page screenshot and egress launch tests The full-page test compared PNG byte sizes on an external site that can serve CI a short page; it now compares captured heights on a long document. The egress suite's beforeAll launches the runner's Chrome, which can exceed bun's 5s hook timeout. --- tests/e2e/endpoints.test.ts | 48 +++++++++++++++----------------- tests/integration/egress.test.ts | 3 +- 2 files changed, 24 insertions(+), 27 deletions(-) diff --git a/tests/e2e/endpoints.test.ts b/tests/e2e/endpoints.test.ts index 06e9aef..abbecde 100644 --- a/tests/e2e/endpoints.test.ts +++ b/tests/e2e/endpoints.test.ts @@ -101,32 +101,28 @@ describe("E2E Tests - /v1/screenshots", () => { }, 15000); test("should capture full page screenshot", async () => { - const partialResponse = await fetch(`${BASE_URL}/v1/screenshots`, { - method: "POST", - headers: { "Content-Type": "application/json" }, - body: JSON.stringify({ - url: "https://itznotabug.dev", - fullPage: false, - }), - }); - - const partialBuffer = await partialResponse.arrayBuffer(); - - const fullPageResponse = await fetch(`${BASE_URL}/v1/screenshots`, { - method: "POST", - headers: { "Content-Type": "application/json" }, - body: JSON.stringify({ - url: "https://itznotabug.dev", - fullPage: true, - }), - }); - - expect(fullPageResponse.status).toBe(200); - const fullPageBuffer = await fullPageResponse.arrayBuffer(); - - // Full page screenshot should be larger than partial - expect(fullPageBuffer.byteLength).toBeGreaterThan(partialBuffer.byteLength); - }, 15000); + // A PNG's height is stored big-endian at bytes 20-23 of its IHDR chunk. + const height = (buffer: ArrayBuffer) => new DataView(buffer).getUint32(20); + const capture = async (fullPage: boolean) => { + const response = await fetch(`${BASE_URL}/v1/screenshots`, { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ + url: "https://en.wikipedia.org/wiki/Brotli", + fullPage, + }), + }); + expect(response.status).toBe(200); + return response.arrayBuffer(); + }; + + const partial = await capture(false); + const fullPage = await capture(true); + + // Compressed size depends on Chrome's PNG encoder; the captured height does not. + expect(height(partial)).toBe(720); + expect(height(fullPage)).toBeGreaterThan(height(partial)); + }, 30000); test("should capture clipped region", async () => { const fullResponse = await fetch(`${BASE_URL}/v1/screenshots`, { diff --git a/tests/integration/egress.test.ts b/tests/integration/egress.test.ts index c256c3b..2944e59 100644 --- a/tests/integration/egress.test.ts +++ b/tests/integration/egress.test.ts @@ -144,7 +144,8 @@ beforeAll(async () => { : { channel: "chrome" }), }; browser = await chromium.launch(options); -}); + // Launching the runner's Chrome can take longer than bun's 5s default hook timeout. +}, 30_000); afterAll(async () => { await browser?.close();