From 0d5c80fbbfdcc54e237877b113d990732e794048 Mon Sep 17 00:00:00 2001 From: Schalli Date: Wed, 16 Sep 2026 17:39:29 +0200 Subject: [PATCH] fix(18): dot-only Manifest-Dateinamen ("."/"..") in getPackage() ablehnen (CR-01) Die Zeichen-Whitelist /^[A-Za-z0-9._-]+$/ liess Namen wie ".." durch, weil Punkt und Bindestrich erlaubte Zeichen sind -- path.join(desktopDistDir, '..') loest aber in den Elternordner auf und unterlaeuft genau die Verteidigung in der Tiefe (T-18-02), die diese Zeile laut Kommentar herstellen soll. Jetzt werden "." und ".." explizit abgelehnt UND der aufgeloeste Pfad zusaetzlich gegen desktopDistDir geprueft (haelt auch kuenftige Varianten ab, falls die Whitelist anderswo wiederverwendet wird). Neue Testfaelle fuer manifest-Eintraege namens "..", "." und "../manifest.json". Co-Authored-By: Claude Opus 5 (1M context) --- apps/api/src/desktop/desktop.service.spec.ts | 24 ++++++++++++++++++++ apps/api/src/desktop/desktop.service.ts | 19 +++++++++++++--- 2 files changed, 40 insertions(+), 3 deletions(-) diff --git a/apps/api/src/desktop/desktop.service.spec.ts b/apps/api/src/desktop/desktop.service.spec.ts index caa429f..2faa3db 100644 --- a/apps/api/src/desktop/desktop.service.spec.ts +++ b/apps/api/src/desktop/desktop.service.spec.ts @@ -161,6 +161,30 @@ describe('DesktopService/DesktopController — HTTP-Durchstich (Phase 18)', () = expect(res.status).toBe(404); }); + it('Test 7a (dot-only Name im Manifest, CR-01): ".." endet mit 404', async () => { + writeManifest({ + linux: { name: '..', size: 123, sha256: 'a'.repeat(64) }, + }); + const res = await fetch(`${baseUrl}/desktop/download/linux`); + expect(res.status).toBe(404); + }); + + it('Test 7b (dot-only Name im Manifest, CR-01): "." endet mit 404', async () => { + writeManifest({ + linux: { name: '.', size: 123, sha256: 'a'.repeat(64) }, + }); + const res = await fetch(`${baseUrl}/desktop/download/linux`); + expect(res.status).toBe(404); + }); + + it('Test 7c (Name mit Traversal-Segment ausserhalb desktopDistDir, CR-01): "../manifest.json" endet mit 404', async () => { + writeManifest({ + linux: { name: '../manifest.json', size: 123, sha256: 'a'.repeat(64) }, + }); + const res = await fetch(`${baseUrl}/desktop/download/linux`); + expect(res.status).toBe(404); + }); + it('Test 9 (WR-03, ungueltiger Eintrag im Manifest): name fehlt -- getLatest wirft NotFoundException statt "undefined" als Datei zu suchen', () => { // Bewusst am `writeManifest()`-Helper vorbei direkt geschrieben -- dessen // Parametertyp verlangt `name`, hier soll aber genau dessen Fehlen diff --git a/apps/api/src/desktop/desktop.service.ts b/apps/api/src/desktop/desktop.service.ts index b829b3c..bef1521 100644 --- a/apps/api/src/desktop/desktop.service.ts +++ b/apps/api/src/desktop/desktop.service.ts @@ -145,13 +145,26 @@ export class DesktopService { } // (4) Verteidigung in der Tiefe (T-18-02): auch ein manipuliertes - // Manifest darf nicht aus dem Ordner hinausfuehren. - if (!/^[A-Za-z0-9._-]+$/.test(entry.name)) { + // Manifest darf nicht aus dem Ordner hinausfuehren. Die Zeichen-Whitelist + // allein reicht nicht -- "." und ".." bestehen ausschliesslich aus + // erlaubten Zeichen, meinen im Dateisystem aber "aktueller"/"uebergeordneter + // Ordner". Deshalb zusaetzlich explizit ausschliessen UND den aufgeloesten + // Pfad gegen den Zielordner pruefen (haelt auch kuenftige Varianten dieses + // Musters ab, falls die Zeichen-Whitelist anderswo wiederverwendet wird). + if ( + !/^[A-Za-z0-9._-]+$/.test(entry.name) || + entry.name === '.' || + entry.name === '..' + ) { + throw new NotFoundException(`No package for platform: ${knownPlatform}`); + } + const filePath = path.join(this.desktopDistDir, entry.name); + const resolvedRoot = path.resolve(this.desktopDistDir) + path.sep; + if (!path.resolve(filePath).startsWith(resolvedRoot)) { throw new NotFoundException(`No package for platform: ${knownPlatform}`); } // (5) Datei muss existieren. - const filePath = path.join(this.desktopDistDir, entry.name); if (!fs.existsSync(filePath)) { throw new NotFoundException(`Package file missing: ${entry.name}`); }