Express 5: geprueft, aber noch nicht umgestellt
Der Sprung 4 auf 5 entfernt Methoden, aendert die Pfadsyntax
grundlegend (path-to-regexp 8) und stellt Standardwerte um. Vieles
davon faellt nicht beim Start auf, sondern erst, wenn eine bestimmte
Adresse aufgerufen wird -- also im Betrieb, bei einem Kunden. Deshalb
zuerst pruefen statt installieren.
ZWEI PRUEFUNGEN, DIE SICH ERGAENZEN
pruef-express5.mjs durchsucht 89 Dateien nach den Bruchstellen aus dem
offiziellen Migrationsleitfaden: entfernte Aufrufe (res.send(zahl),
req.param, app.del, res.sendfile), die UMGEDREHTE Reihenfolge bei
res.redirect, geaenderte Pfadsyntax ("/*" ohne Namen, ":a?"),
schreibgeschuetztes req.query, entfernte static-Optionen.
server/test-express5.mjs startet einen echten Server mit genau unserem
Aufbau: Middleware-Kette mit eigenen Kopfzeilen, cookieParser,
express.json, eine umleitende Schranke, express.static, ein Router mit
Parameter, 404- und Fehlerbehandlung.
ERGEBNIS
15 von 15 Kategorien unbedenklich, 10 von 10 Laufpruefungen bestanden
gegen express 5.2.1. Der Umstieg waere ohne Codeaenderung moeglich.
Ein Fehlalarm lag dabei im Pruefer selbst: Er meldete drei
req.body-Zugriffe als ungesichert, die in Wahrheit innerhalb eines
"if (typeof req.body?.feld === 'boolean')" stehen -- der Rueckblick war
mit 60 Zeichen zu kurz fuer den Block. Ein Pruefer, der abgesicherte
Stellen anmahnt, kostet die Zeit, die er sparen soll, und beim
naechsten Mal glaubt man ihm auch die echten Funde nicht mehr.
NICHT UMGESTELLT
Bewusst. Der Bestand ist sicherheitstechnisch sauber (npm audit: 0),
express 4.22.2 wird weiter gepflegt, und der Nutzen waere gering
gegenueber dem Risiko, zwei laufende Dienste anzufassen. Die Vorarbeit
liegt vor -- wenn umgestellt wird, dann als eigener Vorgang mit
anschliessendem Live-Test, nicht nebenbei.
Co-Authored-By: Claude Opus 5 <[email protected]>
This commit is contained in:
@@ -0,0 +1,208 @@
|
||||
/* =====================================================================
|
||||
Prüft, was ein Umstieg auf Express 5 an unserem Code brechen würde.
|
||||
|
||||
Der Sprung 4 → 5 ist keiner, den man "mal eben" macht: Er entfernt
|
||||
Methoden, ändert das Verhalten bei Pfaden grundlegend (path-to-regexp
|
||||
8) und stellt Standardwerte um. Vieles davon fällt nicht beim Start
|
||||
auf, sondern erst, wenn eine bestimmte Adresse aufgerufen wird —
|
||||
also im Betrieb, bei einem Kunden.
|
||||
|
||||
Deshalb wird hier NICHT installiert und ausprobiert, sondern zuerst
|
||||
der Bestand durchsucht. Jede Fundstelle ist eine, die vor dem
|
||||
Umstieg angefasst werden muss.
|
||||
|
||||
Grundlage: der offizielle Migrationsleitfaden
|
||||
https://expressjs.com/en/guide/migrating-5.html
|
||||
===================================================================== */
|
||||
import fs from "fs";
|
||||
import path from "path";
|
||||
|
||||
let treffer = 0, sauber = 0;
|
||||
|
||||
function dateien(ordner) {
|
||||
const raus = [];
|
||||
const gehe = (p) => {
|
||||
for (const e of fs.readdirSync(p, { withFileTypes: true })) {
|
||||
if (e.name === "node_modules" || e.name.startsWith(".")) continue;
|
||||
const voll = path.join(p, e.name);
|
||||
if (e.isDirectory()) gehe(voll);
|
||||
else if (e.name.endsWith(".js") || e.name.endsWith(".mjs")) raus.push(voll);
|
||||
}
|
||||
};
|
||||
gehe(ordner);
|
||||
return raus;
|
||||
}
|
||||
|
||||
const QUELLEN = [
|
||||
...dateien("server"),
|
||||
...dateien("server-internal"),
|
||||
];
|
||||
|
||||
/* Jede Regel: was gesucht wird, warum es bricht, was zu tun ist. */
|
||||
const REGELN = [
|
||||
{ name: "res.send(zahl) — entfernt",
|
||||
muster: /res\.send\(\s*\d{3}\s*\)/g,
|
||||
hinweis: "ersetzen durch res.sendStatus(zahl)" },
|
||||
|
||||
{ name: "res.json(objekt, status) — entfernt",
|
||||
muster: /res\.json\([^)]*,\s*\d{3}\s*\)/g,
|
||||
hinweis: "ersetzen durch res.status(...).json(...)" },
|
||||
|
||||
{ name: "res.send(inhalt, status) — entfernt",
|
||||
muster: /res\.send\([^)]+,\s*\d{3}\s*\)/g,
|
||||
hinweis: "ersetzen durch res.status(...).send(...)" },
|
||||
|
||||
/* ⚠️ Die Reihenfolge ist in 5 UMGEKEHRT: res.redirect(status, url).
|
||||
Wer res.redirect(url, status) benutzt, bekommt keinen Fehler --
|
||||
die Umleitung geht nur an die falsche Stelle. */
|
||||
{ name: "res.redirect(url, status) — Reihenfolge umgedreht",
|
||||
muster: /res\.redirect\(\s*["'`][^"'`]+["'`]\s*,\s*\d{3}\s*\)/g,
|
||||
hinweis: "in Express 5: res.redirect(status, url)" },
|
||||
|
||||
{ name: 'res.redirect("back") — entfernt',
|
||||
muster: /res\.redirect\(\s*["'`]back["'`]\s*\)/g,
|
||||
hinweis: 'ersetzen durch req.get("Referrer") || "/"' },
|
||||
|
||||
{ name: "req.param(...) — entfernt",
|
||||
muster: /req\.param\(/g,
|
||||
hinweis: "req.params / req.query / req.body direkt lesen" },
|
||||
|
||||
{ name: "app.del(...) — entfernt",
|
||||
muster: /\bapp\.del\(/g,
|
||||
hinweis: "app.delete(...) benutzen" },
|
||||
|
||||
{ name: "res.sendfile(...) — entfernt",
|
||||
muster: /res\.sendfile\(/g,
|
||||
hinweis: "res.sendFile(...) (grosses F)" },
|
||||
|
||||
/* path-to-regexp 8: Der namenlose Stern ist weg. */
|
||||
{ name: 'Route mit "*" ohne Namen — Pfadsyntax geaendert',
|
||||
muster: /(app|router)\.(get|post|put|delete|all|use)\(\s*["'`][^"'`]*\*(?![a-zA-Z])/g,
|
||||
hinweis: '"/*" wird zu "/*splat" bzw. "/{*splat}"' },
|
||||
|
||||
{ name: 'Route mit "?" (optionaler Teil) — Pfadsyntax geaendert',
|
||||
muster: /(app|router)\.(get|post|put|delete|all)\(\s*["'`][^"'`]*\?[^"'`]*["'`]/g,
|
||||
hinweis: '":a?" wird zu "{:a}"' },
|
||||
|
||||
{ name: "req.query wird beschrieben — jetzt schreibgeschuetzt",
|
||||
muster: /req\.query\s*(\.[a-zA-Z_$][\w$]*)?\s*=[^=]/g,
|
||||
hinweis: "req.query ist in 5 nur noch lesbar" },
|
||||
|
||||
{ name: "express.static mit hidden/from — Optionen entfernt",
|
||||
muster: /express\.static\([^)]*\b(hidden|from)\s*:/g,
|
||||
hinweis: "dotfiles bzw. root benutzen" },
|
||||
|
||||
{ name: "express.static.mime — entfernt",
|
||||
muster: /express\.static\.mime/g,
|
||||
hinweis: 'Paket "mime-types" benutzen' },
|
||||
|
||||
{ name: "req.acceptsCharset/Encoding/Language (Einzahl) — entfernt",
|
||||
muster: /req\.accepts(Charset|Encoding|Language)\(/g,
|
||||
hinweis: "Mehrzahlform benutzen" },
|
||||
|
||||
{ name: "res.vary() ohne Feld — wirft jetzt einen Fehler",
|
||||
muster: /res\.vary\(\s*\)/g,
|
||||
hinweis: "Feldnamen angeben" },
|
||||
];
|
||||
|
||||
console.log(`Durchsucht: ${QUELLEN.length} Dateien in server/ und server-internal/\n`);
|
||||
console.log("1. ENTFERNTE UND GEAENDERTE AUFRUFE");
|
||||
|
||||
for (const r of REGELN) {
|
||||
const funde = [];
|
||||
for (const datei of QUELLEN) {
|
||||
const inhalt = fs.readFileSync(datei, "utf8");
|
||||
r.muster.lastIndex = 0;
|
||||
let t;
|
||||
while ((t = r.muster.exec(inhalt)) !== null) {
|
||||
const zeile = inhalt.slice(0, t.index).split("\n").length;
|
||||
funde.push(`${path.relative(process.cwd(), datei)}:${zeile} ${t[0].slice(0, 50)}`);
|
||||
}
|
||||
}
|
||||
if (funde.length) {
|
||||
treffer += funde.length;
|
||||
console.log(` BETROFFEN ${r.name} (${funde.length})`);
|
||||
console.log(` → ${r.hinweis}`);
|
||||
for (const f of funde.slice(0, 4)) console.log(` ${f}`);
|
||||
} else {
|
||||
sauber++;
|
||||
console.log(` ok ${r.name}`);
|
||||
}
|
||||
}
|
||||
|
||||
/* ---------------------------------------------------------------------
|
||||
Zwei Punkte, die keine Fundstelle haben, aber trotzdem wirken --
|
||||
deshalb hier ausdrücklich aufgeführt statt übersehen.
|
||||
--------------------------------------------------------------------- */
|
||||
console.log("\n2. GEAENDERTE STANDARDWERTE (keine Fundstelle, wirken trotzdem)");
|
||||
|
||||
const alleInhalte = QUELLEN.map((d) => fs.readFileSync(d, "utf8")).join("\n");
|
||||
|
||||
const urlencoded = /express\.urlencoded\(([^)]*)\)/.exec(alleInhalte);
|
||||
if (urlencoded) {
|
||||
const hatExtended = /extended/.test(urlencoded[1]);
|
||||
console.log(hatExtended
|
||||
? " ok express.urlencoded: extended ist ausdruecklich gesetzt"
|
||||
: ' BETROFFEN express.urlencoded ohne "extended" — Standard wechselt von true auf false\n'
|
||||
+ ' → ausdruecklich { extended: true } setzen, wenn verschachtelte Felder gebraucht werden');
|
||||
if (!hatExtended) treffer++;
|
||||
} else {
|
||||
console.log(" ok express.urlencoded wird nicht benutzt");
|
||||
}
|
||||
|
||||
/* dotfiles: In 5 ist der Standard "ignore" -- Pfade, die mit einem
|
||||
Punkt beginnen, liefern dann 404. Das betrifft .well-known, das
|
||||
Let's Encrypt und manche Dienste brauchen. */
|
||||
const wellknown = fs.existsSync(".well-known");
|
||||
console.log(wellknown
|
||||
? ' BETROFFEN Ein Ordner ".well-known" existiert — express.static liefert Punkt-Ordner\n'
|
||||
+ ' in Express 5 standardmaessig NICHT mehr aus (dotfiles: "ignore")\n'
|
||||
+ ' → { dotfiles: "allow" } fuer diesen Pfad setzen'
|
||||
: ' ok kein ".well-known"-Ordner im Auslieferungsverzeichnis');
|
||||
if (wellknown) treffer++;
|
||||
|
||||
/* req.body ist in 5 undefined statt {}, wenn kein Parser lief. */
|
||||
const bodyOhnePruefung = [];
|
||||
for (const datei of QUELLEN) {
|
||||
const inhalt = fs.readFileSync(datei, "utf8");
|
||||
const m = /req\.body\.[a-zA-Z_$]/g;
|
||||
let t;
|
||||
while ((t = m.exec(inhalt)) !== null) {
|
||||
if (inhalt.slice(t.index - 1, t.index) === "?") continue;
|
||||
|
||||
/* ⚠️ Der Blick zurueck muss weit genug reichen.
|
||||
|
||||
Zuerst standen hier 60 Zeichen. Damit meldete der Pruefer drei
|
||||
Stellen in webdesign-kunden.js als ungesichert -- sie stehen aber
|
||||
INNERHALB eines "if (typeof req.body?.feld === 'boolean')", sind
|
||||
also bereits geprueft. Der Block war nur laenger als der
|
||||
Rueckblick.
|
||||
|
||||
Ein Pruefer, der abgesicherte Stellen anmahnt, kostet genau die
|
||||
Zeit, die er sparen soll -- und beim naechsten Mal glaubt man ihm
|
||||
auch die echten Funde nicht mehr. Deshalb jetzt die drei Zeilen
|
||||
davor, das deckt den ueblichen Block ab. */
|
||||
const bis = inhalt.slice(0, t.index);
|
||||
const zeilenDavor = bis.split("\n").slice(-4).join("\n");
|
||||
if (/req\.body\s*(&&|\?\.)|\|\|\s*\{\}/.test(zeilenDavor)) continue;
|
||||
|
||||
bodyOhnePruefung.push(`${path.relative(process.cwd(), datei)}:${bis.split("\n").length}`);
|
||||
}
|
||||
}
|
||||
if (bodyOhnePruefung.length) {
|
||||
treffer += bodyOhnePruefung.length;
|
||||
console.log(` BETROFFEN req.body ohne Absicherung (${bodyOhnePruefung.length}) — in 5 ist es\n`
|
||||
+ " undefined statt {}, wenn kein Parser lief\n"
|
||||
+ " → req.body?.feld schreiben");
|
||||
for (const f of bodyOhnePruefung.slice(0, 4)) console.log(" " + f);
|
||||
} else {
|
||||
console.log(" ok req.body wird ueberall abgesichert gelesen");
|
||||
}
|
||||
|
||||
console.log(`\n===== ${sauber} unbedenklich, ${treffer} Fundstelle(n) zu pruefen =====`);
|
||||
if (!treffer) {
|
||||
console.log("\nDer Umstieg waere aus Sicht dieser Pruefung ohne Codeaenderung moeglich.");
|
||||
console.log("Das ersetzt keinen Testlauf -- es heisst nur, dass die bekannten");
|
||||
console.log("Bruchstellen aus dem Migrationsleitfaden hier nicht vorkommen.");
|
||||
}
|
||||
process.exit(0);
|
||||
Reference in New Issue
Block a user