Astro 7: die AVIF-Lücke in sharp/Astro 5 schließen

GHSA-26w7-cxv4-gfx2 (kritisch, RCE beim Verarbeiten von AVIF) hat für
Astro 5 keinen Fix. Also hoch auf Astro 7.3.2, @astrojs/node 11,
@astrojs/react 6 und sharp 0.35. `npm audit`: 0 Funde.

Zwei Stellen mussten mit:

- `experimental.csp` heißt seit Astro 7 `security.csp`. Das ist keine
  kosmetische Umbenennung: ein `csp` auf oberster Ebene wird ohne Warnung
  geschluckt und die Seite läuft ganz ohne Policy. Gemessen — Header
  vorher weg, nach der Korrektur wieder da, mit denselben Skript-Hashes
  wie unter Astro 5.
- sharp exportiert den Typ `Sharp` jetzt benannt statt als Namensraum.

Dazu das Image: node 20 → 22 (Astro 7 verlangt mehr als 20.19, die Tests
brauchen 22.6 für --experimental-strip-types), und `npm ci` mit Lockfile
statt `npm install` — sonst baut das Image nicht die Versionen, die
getestet wurden.

Geprüft gegen echtes Postgres: alle Seiten 200, Upload durch sharp 0.35,
Bogen-PDF auf 210,00 × 297,00 mm gemessen, Einzelbild 35 × 45 mm bei
300 dpi = 413 × 531 px mit korrekter dpi-Angabe. 124 Tests grün.
This commit is contained in:
till committed 2026-09-15 20:14:10 +02:00
1 parent 5ba448f250
commit 2c2ef9d458
6 files changed
+1746 -3845

No files matched your search

+5 -3
View File
@@ -1,4 +1,4 @@
FROM node:20-bookworm-slim FROM node:22-bookworm-slim
WORKDIR /app WORKDIR /app
ENV NODE_ENV=production PORT=4321 HOST=0.0.0.0 ENV NODE_ENV=production PORT=4321 HOST=0.0.0.0
@@ -8,8 +8,10 @@ RUN apt-get update && apt-get install -y --no-install-recommends \
python3 make g++ ca-certificates \ python3 make g++ ca-certificates \
&& rm -rf /var/lib/apt/lists/* && rm -rf /var/lib/apt/lists/*
COPY package.json ./ # Lockfile zuerst: `npm ci` baut exakt die Versionen, die getestet wurden —
RUN npm install --no-audit --no-fund # nicht das, wozu die ^-Bereiche heute zufällig auflösen.
COPY package.json package-lock.json ./
RUN npm ci --no-audit --no-fund
COPY . . COPY . .
RUN npm run build RUN npm run build
+6 -3
View File
@@ -9,11 +9,14 @@ export default defineConfig({
integrations: [react()], integrations: [react()],
// CSRF: eigener Schutz über SameSite=Lax-Session-Cookie. Astros checkOrigin // CSRF: eigener Schutz über SameSite=Lax-Session-Cookie. Astros checkOrigin
// bricht multipart-Uploads hinter dem Reverse-Proxy (http↔https-Mismatch). // bricht multipart-Uploads hinter dem Reverse-Proxy (http↔https-Mismatch).
security: { checkOrigin: false }, //
// Content-Security-Policy mit Hashes statt 'unsafe-inline': Astro rechnet die // Content-Security-Policy mit Hashes statt 'unsafe-inline': Astro rechnet die
// Hashes seiner eigenen inline-Skripte und -Styles beim Bauen aus. Die übrigen // Hashes seiner eigenen inline-Skripte und -Styles beim Bauen aus. Die übrigen
// Direktiven setzt die Middleware (src/middleware.ts). // Direktiven ergänzt die Middleware (src/middleware.ts).
experimental: { // Seit Astro 7 stabil und unter `security.csp` — nicht mehr unter `experimental`.
// Achtung: ein `csp` auf oberster Ebene wird stillschweigend ignoriert.
security: {
checkOrigin: false,
csp: { csp: {
directives: [ directives: [
"default-src 'self'", "default-src 'self'",
+1725 -3832
View File
File diff suppressed because it is too large. Load diff
+7 -4
View File
@@ -3,6 +3,9 @@
"type": "module", "type": "module",
"version": "0.1.0", "version": "0.1.0",
"private": true, "private": true,
"engines": {
"node": ">=22.6"
},
"scripts": { "scripts": {
"dev": "astro dev", "dev": "astro dev",
"build": "astro build", "build": "astro build",
@@ -12,15 +15,15 @@
"test": "node --experimental-strip-types --test tests/*.test.ts" "test": "node --experimental-strip-types --test tests/*.test.ts"
}, },
"dependencies": { "dependencies": {
"@astrojs/node": "^9.1.3", "@astrojs/node": "^11.1.5",
"@astrojs/react": "^4.2.1", "@astrojs/react": "^6.0.5",
"@aws-sdk/client-s3": "^3.700.0", "@aws-sdk/client-s3": "^3.700.0",
"@aws-sdk/s3-request-presigner": "^3.700.0", "@aws-sdk/s3-request-presigner": "^3.700.0",
"@fontsource/space-grotesk": "^5.1.0", "@fontsource/space-grotesk": "^5.1.0",
"@fontsource/space-mono": "^5.1.0", "@fontsource/space-mono": "^5.1.0",
"archiver": "^7.0.1", "archiver": "^7.0.1",
"argon2": "^0.41.1", "argon2": "^0.41.1",
"astro": "^5.5.0", "astro": "^7.3.2",
"basic-ftp": "^5.0.5", "basic-ftp": "^5.0.5",
"grammy": "^1.30.0", "grammy": "^1.30.0",
"heic-convert": "^2.1.0", "heic-convert": "^2.1.0",
@@ -30,7 +33,7 @@
"pg-boss": "^10.1.5", "pg-boss": "^10.1.5",
"react": "^19.0.0", "react": "^19.0.0",
"react-dom": "^19.0.0", "react-dom": "^19.0.0",
"sharp": "^0.33.5", "sharp": "^0.35.4",
"ssh2-sftp-client": "^11.0.0" "ssh2-sftp-client": "^11.0.0"
}, },
"devDependencies": { "devDependencies": {
+2 -2
View File
@@ -1,7 +1,7 @@
// Rendern des Druckbogens: Zuschnitt (sharp) + PDF-Aufbau (pdf-lib). // Rendern des Druckbogens: Zuschnitt (sharp) + PDF-Aufbau (pdf-lib).
// Bewusst ohne KI — reine Geometrie und Skalierung, damit das Ergebnis // Bewusst ohne KI — reine Geometrie und Skalierung, damit das Ergebnis
// vorhersagbar und wiederholbar ist. // vorhersagbar und wiederholbar ist.
import sharp from 'sharp'; import sharp, { type Sharp } from 'sharp';
import { PDFDocument, StandardFonts, rgb, degrees } from 'pdf-lib'; import { PDFDocument, StandardFonts, rgb, degrees } from 'pdf-lib';
import { mmToPt, mmToPx } from './paper.ts'; import { mmToPt, mmToPx } from './paper.ts';
import type { Line, Page, SheetSpec } from './printlayout.ts'; import type { Line, Page, SheetSpec } from './printlayout.ts';
@@ -110,7 +110,7 @@ export async function renderCell(
const ext: 'jpg' | 'png' = opt.ext === 'png' || hasAlpha ? 'png' : 'jpg'; const ext: 'jpg' | 'png' = opt.ext === 'png' || hasAlpha ? 'png' : 'jpg';
if (hasAlpha && ext === 'jpg') img = img.flatten({ background: bg }); if (hasAlpha && ext === 'jpg') img = img.flatten({ background: bg });
const encode = (pipe: sharp.Sharp) => (ext === 'png' const encode = (pipe: Sharp) => (ext === 'png'
? pipe.withMetadata({ density: dpi }).png({ compressionLevel: 9 }) ? pipe.withMetadata({ density: dpi }).png({ compressionLevel: 9 })
: pipe.withMetadata({ density: dpi }).jpeg({ quality: 94, mozjpeg: true })); : pipe.withMetadata({ density: dpi }).jpeg({ quality: 94, mozjpeg: true }));
+1 -1
View File
@@ -54,7 +54,7 @@ export const onRequest = defineMiddleware(async (ctx, next) => {
/** /**
* Sicherheits-Header. Die Content-Security-Policy selbst baut Astro beim Bauen * Sicherheits-Header. Die Content-Security-Policy selbst baut Astro beim Bauen
* (`experimental.csp` in `astro.config.mjs`) und hängt echte **Hashes** für jedes * (`csp` in `astro.config.mjs`) und hängt echte **Hashes** für jedes
* eigene inline-Skript an — also kein `'unsafe-inline'` für Skripte, das ist der * eigene inline-Skript an — also kein `'unsafe-inline'` für Skripte, das ist der
* wichtige Teil gegen eingeschleusten Code. * wichtige Teil gegen eingeschleusten Code.
* *