{"uuid": "aaa26058-c48b-4eb6-85a6-a96765885719", "vulnerability_lookup_origin": "1a89b78e-f703-45f3-bb86-59eb712668bd", "author": "9f56dd64-161d-43a6-b9c3-555944290a09", "vulnerability": "GHSA-89xv-2m56-2m9x", "type": "seen", "source": "https://gist.github.com/klemmanton-lgtm/851652e5d9c31db2c2148d15fd9e704c", "content": "diff --git a/AGENTS.md b/AGENTS.md\nindex 0e7bda3..df9c466 100644\n--- a/AGENTS.md\n+++ b/AGENTS.md\n@@ -99,8 +99,8 @@ Diese Regeln stammen aus realen Bugs. Nicht davon abweichen, ohne R\u00fccksprache.\n - **Style-Stack-Koexistenz ist gewollt:** Filter-Components nutzen Tailwind v4 + shadcn/ui, alles andere (Cards, Hero, Detail-Modals, Mein-Bereich, Login) nutzt Inline-Style. Beim Bauen das jeweilige Muster der Umgebung \u00fcbernehmen, nicht vereinheitlichen.\n \n **`npm audit` / Dependency-Gate:**\n-- **Der `next`-Treffer `9.3.4-canary.0 - 16.3.0-preview.10` ist KEIN Advisory gegen unsere Version.** npm kollabiert Stable- und 16.3-Preview-Range zu einem Intervall \u2014 ein Datenbank-Artefakt, keine Aussage \u00fcber 16.2.x. Das Juli-B\u00fcndel (4 high, 5 moderate) ist in **16.2.11** gepatcht, wir stehen auf **16.2.12**. Belegt am **09.08.2026** gegen die Vercel-Meldung.\n-- Diese Fehldeutung ist wiederholt neu entstanden. Also: **nicht erneut untersuchen, kein `npm audit fix`** (es z\u00f6ge `next` mit hoch).\n+- **Der alte `next`-Range `9.3.4-canary.0 - 16.3.0-preview.10` ist erledigt.** Er endete bei `16.3.0-preview.10`; seit PR #372 stehen wir auf **16.3.1** und liegen damit formal au\u00dferhalb (mit semver gegengepr\u00fcft, mit und ohne `includePrerelease`). Die Allowlist in `audit-ci.jsonc` ist seither **leer**, `npm audit` meldet high 0 / critical 0. Historie und Beweistabelle stehen dort im Kommentar \u2014 hier nicht wiederholen.\n+- **Kein `npm audit fix`.** Eigene Begr\u00fcndung, unabh\u00e4ngig vom erledigten Range: `fix` l\u00f6st Ranges neu auf und fasst dabei Pakete an, die niemand gepr\u00fcft hat \u2014 bei uns zuletzt belegt an einer vollst\u00e4ndigen Lockfile-Neuerzeugung, die 177 Versionen bewegte, darunter Herabstufungen und ein ungewollter `@react-email/ui`-Bump. Einzelne Advisories gezielt mit `npm install @` heben, nie pauschal.\n - **Der Sprung auf `next` 16.3 war nie vom Netlify-Adapter blockiert** (gekl\u00e4rt 16.08.2026). Es gibt keinen Pin und keine Repo-Deklaration: `netlify.toml` hat keinen `[[plugins]]`-Block, `@netlify/plugin-nextjs` steht weder in `package.json` noch in `node_modules`. Der Adapter kommt **ausschlie\u00dflich** aus der Netlify-Oberfl\u00e4che (`origin: \"ui\"`) und aktualisiert sich dort selbst. Blockiert hat der **Netlify-Secrets-Scan**: seit `next` 16.3.0 ist `experimental.turbopackFileSystemCacheForBuild` per Default an und legt einen Turbopack-Build-Cache an, der einen **Klartext-Abzug von `process.env`** enth\u00e4lt; der Scan erfasst ihn und bricht den Deploy ab. Der Build selbst l\u00e4uft unter 16.3.0 durch, der ausgelieferte Output ist frei davon. Abgestellt in PR #371 (`turbopackFileSystemCacheForBuild: false`). Langfassung in der Projektdoku v13.2.\n \n ---\ndiff --git a/audit-ci.jsonc b/audit-ci.jsonc\nindex 110fc6b..d5fc7ba 100644\n--- a/audit-ci.jsonc\n+++ b/audit-ci.jsonc\n@@ -5,59 +5,64 @@\n // `continue-on-error: true` und meldete dauerhaft \u201efail\", ohne je etwas zu\n // verhindern \u2014 CI-Ger\u00e4usch, das man irgendwann nicht mehr liest.\n //\n-// Mit der Allowlist unten ist die Baseline sauber, deshalb ist das Gate jetzt\n-// BLOCKIEREND (das ist die im Workflow dokumentierte \u201ePhase 2\").\n+// Das Gate ist BLOCKIEREND (die im Workflow dokumentierte \u201ePhase 2\").\n //\n // ALLOWLIST-DISZIPLIN: Eintr\u00e4ge sind GHSA-genau, nie paketweit. `\"next\"` als\n // Eintrag w\u00fcrde auch jedes ZUK\u00dcNFTIGE next-Advisory verschlucken \u2014 genau das,\n-// wogegen das Gate sch\u00fctzen soll.\n-//\n-// Jeder Eintrag tr\u00e4gt Begr\u00fcndung und Verfallsdatum. Das Datum ist nicht nur\n-// Kommentar: `src/lib/utils/audit-allowlist.test.ts` liest diese Datei und\n-// schl\u00e4gt fehl, sobald ein Eintrag abgelaufen ist. Eine Allowlist, die\n-// niemand mehr \u00fcberpr\u00fcft, ist eine stillgelegte Sicherung.\n+// wogegen das Gate sch\u00fctzen soll. Jeder Eintrag tr\u00e4gt Begr\u00fcndung UND\n+// Ablaufdatum; `src/lib/utils/audit-allowlist.test.ts` liest diese Datei und\n+// erzwingt beides. Eine Allowlist, die niemand mehr \u00fcberpr\u00fcft, ist eine\n+// stillgelegte Sicherung.\n {\n   // Schwelle wie beim Vorg\u00e4nger: high und h\u00f6her. Moderate Findings (uuid\n-  // GHSA-w5hq-g745-h8pq, diverse next/postcss) sind bewusst nicht erfasst.\n+  // GHSA-w5hq-g745-h8pq \u00fcber exceljs) sind bewusst nicht erfasst.\n   \"high\": true,\n   \"critical\": true,\n \n   // \u2500\u2500 Allowlist \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n-  // ALLE sechs Eintr\u00e4ge h\u00e4ngen an DERSELBEN Ursache: dem blockierten Sprung auf\n-  // next 16.3.0. Sie fallen gemeinsam weg, sobald er m\u00f6glich ist.\n+  // LEER, und das ist der Zielzustand. Das Gate steht ohne Ausnahmen.\n   //\n-  // Gemeinsamer Ausl\u00f6ser f\u00fcr die Wiedervorlage (gekl\u00e4rt 16.08.2026):\n-  //   Der Netlify-Secrets-Scan durfte am Turbopack-Build-Cache nicht mehr\n-  //   abbrechen. Seit next 16.3.0 wird der Cache per Default angelegt und\n-  //   enth\u00e4lt einen Klartext-Abzug von process.env; abgestellt \u00fcber\n-  //   experimental.turbopackFileSystemCacheForBuild: false (PR #371).\n-  //   NICHT die Netlify-Runtime \u2014 die hat nie geblockt, ist ungepinnt und\n-  //   kommt ausschlie\u00dflich aus der Netlify-UI (origin: \"ui\"); der Build lief\n-  //   unter 16.3.0 durch, erst der Scan danach brach ab.\n-  //   An der Blockade hingen: #319, #323, #326 (Produktions-Gruppe, jeweils\n-  //   direkt mit `next: ^16.3.0`) und #331 (Dev-Gruppe, indirekt \u00fcber\n-  //   @react-email/ui 6.9.2 mit exaktem `next: 16.3.0` ohne Caret).\n-  // Gemeinsames Verfallsdatum: 2026-11-30\n-  \"allowlist\": [\n-    // \u2500\u2500 next: vier Treffer, die uns NICHT betreffen \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n-    // npm kollabiert Stable- und 16.3-Preview-Range zu einem Intervall\n-    // (\"9.3.4-canary.0 - 16.3.0-preview.10\"). Das Juli-B\u00fcndel ist in 16.2.11\n-    // gepatcht, wir stehen auf 16.2.12. Belegt am 09.08.2026 gegen die\n-    // Vercel-Meldung; siehe AGENTS.md, Abschnitt \u201enpm audit / Dependency-Gate\".\n-    // Verf\u00e4llt: 2026-11-30\n-    \"GHSA-6gpp-xcg3-4w24\", // Middleware/Proxy bypass im App Router\n-    \"GHSA-89xv-2m56-2m9x\", // SSRF in Server Actions\n-    \"GHSA-m99w-x7hq-7vfj\", // DoS im App Router \u00fcber Server Actions\n-    \"GHSA-p9j2-gv94-2wf4\", // SSRF in rewrites\n-\n-    // \u2500\u2500 postcss: real, aber nur \u00fcber next erreichbar \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n-    // Beide Pfade sind `next&gt;postcss` bzw. `@react-email/ui&gt;next&gt;postcss` \u2014\n-    // kein direkter Import. postcss l\u00e4uft ausschlie\u00dflich zur BUILD-Zeit \u00fcber\n-    // unser eigenes CSS, verarbeitet also keine Angreifer-Eingaben. Ein Fix\n-    // kommt mit dem next-Upgrade, ein eigener Override w\u00e4re ein zweiter\n-    // Sonderweg neben dem sharp-Override.\n-    // Verf\u00e4llt: 2026-11-30\n-    \"GHSA-6g55-p6wh-862q\", // Arbitrary file read \u00fcber Source-Map-Aufl\u00f6sung\n-    \"GHSA-r28c-9q8g-f849\"  // Path Traversal beim Auto-Loading alter Source Maps\n-  ]\n+  // HISTORIE (aufgel\u00f6st am 17.08.2026). Bis dahin standen hier sechs Eintr\u00e4ge \u2014\n+  // vier next-GHSAs, zwei postcss-GHSAs \u2014 unter einem gemeinsamen Ablaufdatum im\n+  // November 2026, mit der Begr\u00fcndung, sie hingen alle am blockierten Sprung auf\n+  // next 16.3 und fielen gemeinsam weg, sobald er m\u00f6glich sei. Beide H\u00e4lften\n+  // dieser Begr\u00fcndung waren falsch, und beide sind inzwischen belegt aufgel\u00f6st:\n+  //\n+  //   1. Blockiert hat nie der Netlify-Adapter (der war ungepinnt und kam\n+  //      ausschlie\u00dflich aus der Netlify-UI), sondern der Netlify-Secrets-Scan am\n+  //      Turbopack-Build-Cache: seit next 16.3.0 wird der Cache per Default\n+  //      angelegt und enth\u00e4lt einen Klartext-Abzug von process.env. Abgestellt\n+  //      in PR #371 (turbopackFileSystemCacheForBuild: false).\n+  //\n+  //   2. Sie fielen NICHT gemeinsam mit dem Sprung. Nach PR #372 (next 16.3.1)\n+  //      war die Wurzel sauber, aber alle sechs liefen weiter \u2014 ausschlie\u00dflich\n+  //      \u00fcber die Kette @react-email/ui 6.9.1 &gt; next 16.2.6 &gt; postcss 8.4.31.\n+  //      Erst PR #366 (@react-email/ui 6.9.2, nested next 16.3.0 + postcss\n+  //      8.5.23) hat diese Kette entfernt.\n+  //\n+  // WARUM DIE FEHLDEUTUNG IMMER WIEDER NEU ENTSTAND: Der next-Advisory-Range\n+  // umfasste unseren damaligen Stand tats\u00e4chlich. Das war kein Lesefehler,\n+  // sondern der naheliegende Schluss aus einer zutreffenden Beobachtung \u2014\n+  // deshalb musste in AGENTS.md ein ausdr\u00fcckliches Verbot stehen, ihn erneut zu\n+  // untersuchen. Mit 16.3.1 liegen wir formal au\u00dferhalb, damit erledigt sich\n+  // beides. Gegengepr\u00fcft mit semver, mit und ohne includePrerelease:\n+  //\n+  //   Range 9.3.4-canary.0 - 16.3.0-preview.10\n+  //     16.2.6             IM RANGE\n+  //     16.2.12            IM RANGE      &lt;- unser Stand bis PR #372\n+  //     16.3.0-preview.10  IM RANGE\n+  //     16.3.0             au\u00dferhalb\n+  //     16.3.1             au\u00dferhalb     &lt;- unser Stand seit PR #372\n+  //   postcss &lt;=8.5.22\n+  //     8.4.31             IM RANGE\n+  //     8.5.23             au\u00dferhalb     &lt;- Stand seit PR #372 / #366\n+  //\n+  // Beleg f\u00fcr den Endzustand, `npm audit` ohne Allowlist auf main:\n+  // high 0, critical 0, zwei moderate (uuid &lt;11.1.1 \u00fcber exceljs).\n+  //\n+  // WENN HIER JE WIEDER EIN EINTRAG ENTSTEHT: GHSA-genau, nie paketweit, mit\n+  // Begr\u00fcndung UND einer Ablauf-Marke im Kommentar. Der W\u00e4chter erzwingt den\n+  // Gleichstand in beide Richtungen \u2014 Eintr\u00e4ge ohne Marke sind rot, eine Marke\n+  // ohne Eintr\u00e4ge ebenso.\n+  \"allowlist\": []\n }\ndiff --git a/src/lib/utils/audit-allowlist.test.ts b/src/lib/utils/audit-allowlist.test.ts\nindex fc994b1..27d4d33 100644\n--- a/src/lib/utils/audit-allowlist.test.ts\n+++ b/src/lib/utils/audit-allowlist.test.ts\n@@ -2,11 +2,33 @@\n // ---------------------------------------------------------------------------\n // Eine Allowlist ohne Ablaufkontrolle ist eine stillgelegte Sicherung: Der\n // Eintrag bleibt, der Grund verschwindet, und niemand merkt es. Dieser Test\n-// macht aus dem Verfallsdatum im Kommentar eine echte Bedingung \u2014 l\u00e4uft ein\n+// macht aus dem Ablaufdatum im Kommentar eine echte Bedingung \u2014 l\u00e4uft ein\n // Eintrag ab, wird CI rot und jemand muss entscheiden.\n //\n-// Zus\u00e4tzlich: Die Allowlist muss GHSA-genau bleiben. Ein paketweiter Eintrag\n-// (\"next\") w\u00fcrde jedes zuk\u00fcnftige Advisory desselben Pakets verschlucken.\n+// MANDAT (seit 17.08.2026): Die Allowlist ist LEER. Der W\u00e4chter bewacht damit\n+// nicht mehr sechs konkrete Eintr\u00e4ge, sondern die REGELN, unter denen Eintr\u00e4ge\n+// \u00fcberhaupt entstehen d\u00fcrfen. Er ruht, solange die Liste leer ist, und ist in\n+// dem Moment wieder scharf, in dem jemand den ersten Eintrag hinzuf\u00fcgt.\n+//\n+// Das ist eine bewusste Entscheidung gegen zwei Alternativen: Eine Z\u00e4hlung\n+// (`length &gt; 0`) h\u00e4tte beim Leeren der Liste angepasst werden m\u00fcssen \u2014 und ein\n+// Test, den man einmal anpasst, weil er im Weg steht, wird beim n\u00e4chsten Mal\n+// ganz entfernt. Eine Behauptung \u201edie Liste IST leer\" h\u00e4tte jeden legitimen\n+// k\u00fcnftigen Eintrag zu einer Test\u00e4nderung gezwungen, mit demselben Ergebnis.\n+//\n+// Die vier Pr\u00fcfungen:\n+//   1. Schwelle high/critical \u2014 unabh\u00e4ngig von der Allowlist, das ist der Kern\n+//      des Gates und l\u00e4uft immer.\n+//   2. Jeder Eintrag ist GHSA-genau. Ein paketweiter Eintrag (\"next\") w\u00fcrde\n+//      jedes ZUK\u00dcNFTIGE Advisory desselben Pakets verschlucken.\n+//   3. GLEICHSTAND von Eintr\u00e4gen und Ablaufdatum, in BEIDE Richtungen.\n+//      Eintr\u00e4ge ohne Datum sind eine Allowlist ohne Ablaufkontrolle. Ein Datum\n+//      ohne Eintr\u00e4ge ist eine verwaiste Marke, die irgendwann grundlos rot wird\n+//      und dann jemanden dazu bringt, die Ablaufkontrolle abzuschalten.\n+//      Bewusst eine Pr\u00e4senz-\u00c4quivalenz, KEINE Z\u00e4hlung: die Allowlist trug\n+//      zuletzt sechs Eintr\u00e4ge unter EINEM gemeinsamen Datum, eine\n+//      Eins-zu-eins-Zuordnung gab es nie und soll auch nicht erzwungen werden.\n+//   4. Kein Ablaufdatum liegt in der Vergangenheit.\n \n import { readFileSync } from 'node:fs'\n import { join } from 'node:path'\n@@ -36,6 +58,8 @@ const KONFIG = JSON.parse(ohneKommentare(ROH)) as {\n   allowlist?: string[]\n }\n \n+const EINTRAEGE = KONFIG.allowlist ?? []\n+\n /** Alle \u201eVerf\u00e4llt: YYYY-MM-TT\"-Marken aus den Kommentaren. */\n const VERFALLSDATEN = [...ROH.matchAll(/Verf\u00e4llt:\\s*(\\d{4}-\\d{2}-\\d{2})/g)].map((m) =&gt; m[1])\n \n@@ -46,15 +70,23 @@ describe('audit-ci.jsonc', () =&gt; {\n   })\n \n   it('f\u00fchrt ausschlie\u00dflich GHSA-genaue Eintr\u00e4ge, nie paketweite', () =&gt; {\n-    const eintraege = KONFIG.allowlist ?? []\n-    expect(eintraege.length).toBeGreaterThan(0)\n-    for (const eintrag of eintraege) {\n+    for (const eintrag of EINTRAEGE) {\n       expect(eintrag, `\u201e${eintrag}\" ist keine GHSA-ID`).toMatch(/^GHSA-[a-z0-9]{4}-[a-z0-9]{4}-[a-z0-9]{4}$/)\n     }\n   })\n \n-  it('belegt jede Gruppe mit einem Verfallsdatum', () =&gt; {\n-    expect(VERFALLSDATEN.length).toBeGreaterThan(0)\n+  it('h\u00e4lt Eintr\u00e4ge und Ablaufdatum im Gleichstand (beide Richtungen)', () =&gt; {\n+    expect(\n+      VERFALLSDATEN.length &gt; 0,\n+      EINTRAEGE.length &gt; 0\n+        ? `Die Allowlist f\u00fchrt ${EINTRAEGE.length} Eintrag/Eintr\u00e4ge, aber KEINE ` +\n+          'Ablauf-Marke. Trage im Kommentarblock eine Zeile \u201eVerf\u00e4llt: YYYY-MM-TT\" ' +\n+          'nach \u2014 ohne sie bleibt der Eintrag stehen, wenn sein Grund verschwindet.'\n+        : 'Die Allowlist ist LEER, aber es steht noch eine Ablauf-Marke ' +\n+          `(${VERFALLSDATEN.join(', ')}) in der Datei. Entferne sie: eine verwaiste ` +\n+          'Marke wird an ihrem Datum grundlos rot und bringt dann jemanden dazu, ' +\n+          'die Ablaufkontrolle ganz abzuschalten.',\n+    ).toBe(EINTRAEGE.length &gt; 0)\n   })\n \n   it('hat keinen abgelaufenen Eintrag', () =&gt; {\n@@ -62,9 +94,9 @@ describe('audit-ci.jsonc', () =&gt; {\n     const abgelaufen = VERFALLSDATEN.filter((datum) =&gt; datum &lt; heute)\n     expect(\n       abgelaufen,\n-      `Allowlist-Eintrag abgelaufen (${abgelaufen.join(', ')}). Entweder ist das ` +\n-        'next-Upgrade jetzt m\u00f6glich (dann Eintr\u00e4ge raus), oder das Datum wird mit ' +\n-        'neuer Begr\u00fcndung verl\u00e4ngert. Nicht kommentarlos weiterschieben.',\n+      `Allowlist-Eintrag abgelaufen (${abgelaufen.join(', ')}). Entweder ist die ` +\n+        'Ursache inzwischen behoben (dann Eintr\u00e4ge UND Marke raus), oder das Datum ' +\n+        'wird mit neuer Begr\u00fcndung verl\u00e4ngert. Nicht kommentarlos weiterschieben.',\n     ).toEqual([])\n   })\n })\n", "creation_timestamp": "2026-08-17T10:21:36.709387Z"}