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}`); }