From df946463716ffc0120e0a0516366936a48be5042 Mon Sep 17 00:00:00 2001 From: Florian Wilde Date: Mon, 27 Jul 2026 00:14:10 +0200 Subject: [PATCH] fix(security): Bildoptimierer abschalten /_next/image war ohne Anmeldung erreichbar und antwortete mit 200. Die Middleware nimmt den Pfad aus, weil dort sonst nur Rechenzeit verbraucht wuerde - gedacht war das fuer ausgelieferte Dateien, nicht fuer einen Endpunkt, der Bilder verarbeitet. Er reicht jedes Bild durch sharp und damit durch libvips, fuer das es regelmaessig Schwachstellen gibt; derzeit sind vier offen (CVE-2026-33327, -33328, -35590, -35591). Ausnutzbar waere das hier nur mit einer Datei, die schon auf dem Server liegt - hochladen kann man nichts. Aber Nebula benutzt next/image ohnehin nirgends: eine Angriffsflaeche ohne Gegenwert. Der Optimierer ist deshalb abgeschaltet, und der Pfad ist nicht mehr von der Zugangsschranke ausgenommen. Ein Test haelt fest, dass er ohne Anmeldung nicht mehr mit 200 antwortet. Die Advisories zu postcss und sharp bleiben in npm audit stehen: Next pinnt beide fest, auch in 16.2.12 - ein Versionssprung wuerde daran nichts aendern. postcss laeuft nur zur Bauzeit ueber eigenes CSS. Co-Authored-By: Claude Opus 5 (1M context) --- e2e/api.spec.ts | 13 +++++++++++++ next.config.ts | 15 +++++++++++++++ src/middleware.ts | 5 ++++- 3 files changed, 32 insertions(+), 1 deletion(-) diff --git a/e2e/api.spec.ts b/e2e/api.spec.ts index b795283..44da962 100644 --- a/e2e/api.spec.ts +++ b/e2e/api.spec.ts @@ -532,6 +532,19 @@ test("Projektvorlagen: aus Projekt sichern und anwenden", async ({ request, play expect((await request.delete(`/api/templates/${vorlage.id}`)).ok()).toBeTruthy(); }); +test("Der Bildoptimierer ist zu", async ({ playwright }) => { + // /_next/image reicht jedes Bild durch sharp und damit durch libvips, fuer + // das es regelmaessig Schwachstellen gibt. Nebula benutzt next/image + // nirgends – der Endpunkt darf ohne Anmeldung nicht antworten. + const anonym = await playwright.request.newContext({ + baseURL: test.info().project.use.baseURL, + maxRedirects: 0, + }); + const antwort = await anonym.get("/_next/image?url=%2Ficon-192.png&w=64&q=75"); + expect(antwort.status(), "kein 200 ohne Anmeldung").not.toBe(200); + await anonym.dispose(); +}); + test("Export enthaelt keine Geheimnisse, Import beachtet die Strategie", async ({ request }) => { await login(request, ADMIN); diff --git a/next.config.ts b/next.config.ts index 239ce44..aeda258 100644 --- a/next.config.ts +++ b/next.config.ts @@ -9,6 +9,21 @@ const nextConfig: NextConfig = { poweredByHeader: false, // Prisma darf nicht gebundlet werden, sonst findet der Client seine Engine nicht. serverExternalPackages: ["@prisma/client", "prisma"], + images: { + /** + * Der Bildoptimierer ist abgeschaltet. + * + * Nebula benutzt next/image nirgends – die paar Symbole sind SVG und + * PNG in fester Groesse. Der Endpunkt /_next/image waere aber trotzdem + * da, und zwar ohne Anmeldung: die Middleware nimmt ihn aus, weil dort + * sonst nur Rechenzeit verbraucht wuerde. + * + * Er reicht jedes Bild durch sharp und damit durch libvips, fuer das es + * regelmaessig Schwachstellen gibt (aktuell CVE-2026-33327 und drei + * weitere). Eine Angriffsflaeche ohne Gegenwert – also weg damit. + */ + unoptimized: true, + }, }; export default nextConfig; diff --git a/src/middleware.ts b/src/middleware.ts index 320773c..f2cf86f 100644 --- a/src/middleware.ts +++ b/src/middleware.ts @@ -147,7 +147,10 @@ export const config = { * weder CSRF-Pruefung noch Nonce und wuerden nur Rechenzeit kosten. */ { - source: "/((?!_next/static|_next/image|favicon.ico|icon.svg).*)", + // _next/image ist bewusst *nicht* ausgenommen: der Bildoptimierer ist + // abgeschaltet (siehe next.config.ts), und was von ihm uebrig bleibt, + // soll nicht ohne Sitzung erreichbar sein. + source: "/((?!_next/static|favicon.ico|icon.svg).*)", missing: [ { type: "header", key: "next-router-prefetch" }, { type: "header", key: "purpose", value: "prefetch" },