From dfa19c999bf7b0fd4041fe959d15d61020c9c907 Mon Sep 17 00:00:00 2001 From: Dustin Date: Tue, 4 Aug 2026 20:48:31 +0200 Subject: [PATCH] =?UTF-8?q?v2.50.3=20=E2=80=94=20sanitize.dart=20geh=C3=A4?= =?UTF-8?q?rtet:=20Windows-Namen=20+=20Traversal-Sequenzen?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## lib/utils/sanitize.dart - Windows-reservierte Namen (CON, NUL, PRN, AUX, COM1-9, LPT1-9) abgefangen — auch mit Endung (CON.txt) und case-insensitiv; COM10/LPT10 bleiben erlaubt - Mehrfache ..-Sequenzen nach Sonderzeichen-Entfernung → unknown - Trimmen VOR der Punkt-Prüfung (Regression: ' ..' ergab vorher '..' = Path-Traversal) - Trailing Punkte/Leerzeichen entfernt (Windows-Dateisystem ignoriert sie) - Umlaute bleiben erhalten (unicode:true + \p{L}/\p{N} statt ASCII-\w — 'Müller.mp3' wurde vorher zu 'Mller.mp3') ## test/sanitize_test.dart (18 Tests) - Traversal: Slash/Backslash-Pfade, '..', ' ..', '..:..', versteckte Dateien - Windows: alle reservierten Namen exakt/mit Endung/case-insensitiv, COM10-Freigabe, Prefix-Fälle (console, LPT1 - Drucker) - Normalisierung: NULL-Bytes, Steuerzeichen, Sonderzeichen, trailing Dots, leer, 200-Limit, Umlaute --- lib/utils/sanitize.dart | 36 +++++++++++-- test/sanitize_test.dart | 117 ++++++++++++++++++++++++++++++++++++++++ 2 files changed, 148 insertions(+), 5 deletions(-) create mode 100644 test/sanitize_test.dart diff --git a/lib/utils/sanitize.dart b/lib/utils/sanitize.dart index 96df035..3f25e78 100644 --- a/lib/utils/sanitize.dart +++ b/lib/utils/sanitize.dart @@ -1,5 +1,6 @@ /// Zentrale Sanitize-Funktion für Dateinamen. -/// Verhindert Path-Traversal (../), Null-Bytes und andere gefährliche Zeichen. +/// Verhindert Path-Traversal (../), Null-Bytes, Windows-reservierte Namen +/// (CON, NUL, PRN, AUX, COM1-9, LPT1-9) und andere gefährliche Zeichen. String sanitizeDateiname(String name) { // 1. Extrahiere nur den Dateinamen (keine Pfad-Komponenten) var safe = name.split('/').last.split('\\').last; @@ -8,16 +9,41 @@ String sanitizeDateiname(String name) { safe = safe.replaceAll(RegExp(r'[\x00-\x1F\x7F]'), ''); // 3. Entferne alles außer Wort-Zeichen, Leerzeichen und Bindestrichen - safe = safe.replaceAll(RegExp(r'[^\w\s.-]'), ''); + // (unicode: true + \p{L}/\p{N} → auch Umlaute wie ü/ä/ö bleiben, + // sonst würden deutsche Dateinamen wie "Müller.mp3" → "Mller.mp3") + safe = safe.replaceAll(RegExp(r'[^\p{L}\p{N}_\s.-]', unicode: true), ''); - // 4. Entferne führende Punkte (versteckte Dateien) + // 4. Trimmen VOR der Punkt-Prüfung — sonst umgeht " .." (Leerzeichen vor + // den Punkten) die Entfernung führender Punkte und ergäbe ".." als Namen. + safe = safe.trim(); + + // 5. Entferne führende Punkte (versteckte Dateien) safe = safe.replaceAll(RegExp(r'^\.+'), ''); - // 5. Trimmen und Default-Name falls leer + // 6. Nach Sonderzeichen-Entfernung können Punkt-Sequenzen übrig bleiben: + // ".." / "..." als kompletter Name (oder führende "..") wäre + // Pfad-Traversal → verwerfen. + if (safe == '..' || safe == '...' || safe.startsWith('..')) { + safe = 'unknown'; + } + + // 7. Windows-reservierte Namen abfangen (CON, NUL, PRN, AUX, COM1-9, + // LPT1-9) — auch mit Endung (z.B. "CON.txt") und case-insensitiv. + // Windows behandelt diese Namen unabhängig von der Endung als Geräte. + final basis = safe.split('.').first.toUpperCase(); + if (RegExp(r'^(CON|NUL|PRN|AUX|COM[1-9]|LPT[1-9])$').hasMatch(basis)) { + safe = 'unknown'; + } + + // 8. Windows: Punkte/Leerzeichen am Ende entfernen (werden vom + // Dateisystem ignoriert und erzeugen sonst Kollisionen) + safe = safe.replaceAll(RegExp(r'[. ]+$'), ''); + + // 9. Trimmen und Default-Name falls leer safe = safe.trim(); if (safe.isEmpty) safe = 'unknown'; - // 6. Auf sinnvolle Länge kürzen (255 Zeichen ist typisches Dateisystem-Limit) + // 10. Auf sinnvolle Länge kürzen (255 Zeichen ist typisches Dateisystem-Limit) if (safe.length > 200) safe = safe.substring(0, 200); return safe; diff --git a/test/sanitize_test.dart b/test/sanitize_test.dart new file mode 100644 index 0000000..3d69bdd --- /dev/null +++ b/test/sanitize_test.dart @@ -0,0 +1,117 @@ +import 'package:flutter_test/flutter_test.dart'; +import 'package:melo_app/utils/sanitize.dart'; + +void main() { + group('sanitizeDateiname — Path-Traversal', () { + test('entfernt Pfad-Komponenten (Slash)', () { + expect(sanitizeDateiname('../../etc/passwd'), 'passwd'); + expect(sanitizeDateiname('a/b/c.mp3'), 'c.mp3'); + }); + + test('entfernt Pfad-Komponenten (Backslash)', () { + expect(sanitizeDateiname(r'..\..\windows\system32'), 'system32'); + expect(sanitizeDateiname(r'a\b.mp3'), 'b.mp3'); + }); + + test('kompletter ".."-Name → unknown', () { + expect(sanitizeDateiname('..'), 'unknown'); + expect(sanitizeDateiname('...'), 'unknown'); + }); + + test('Leerzeichen VOR den Punkten umgeht die Punkt-Entfernung nicht', () { + // Regression: " .." wurde früher erst nach der Punkt-Prüfung getrimmt + // und ergab ".." als Dateinamen (Pfad-Traversal). + expect(sanitizeDateiname(' ..'), 'unknown'); + expect(sanitizeDateiname(' ... '), 'unknown'); + }); + + test('mehrfache ".."-Sequenzen nach Sonderzeichen-Entfernung → unknown', + () { + // "..:.." → Sonderzeichen ':' weg → "...." → führende Punkte weg → leer + expect(sanitizeDateiname('..:..'), 'unknown'); + expect(sanitizeDateiname('..!..'), 'unknown'); + }); + + test('führende Punkte (versteckte Dateien) werden entfernt', () { + expect(sanitizeDateiname('.hidden'), 'hidden'); + expect(sanitizeDateiname('..hidden'), 'hidden'); + }); + }); + + group('sanitizeDateiname — Windows-reservierte Namen', () { + test('CON/NUL/PRN/AUX exakt → unknown', () { + expect(sanitizeDateiname('CON'), 'unknown'); + expect(sanitizeDateiname('NUL'), 'unknown'); + expect(sanitizeDateiname('PRN'), 'unknown'); + expect(sanitizeDateiname('AUX'), 'unknown'); + }); + + test('COM1-9 und LPT1-9 exakt → unknown', () { + for (var i = 1; i <= 9; i++) { + expect(sanitizeDateiname('COM$i'), 'unknown'); + expect(sanitizeDateiname('LPT$i'), 'unknown'); + } + }); + + test('reservierte Namen auch MIT Endung → unknown', () { + expect(sanitizeDateiname('CON.txt'), 'unknown'); + expect(sanitizeDateiname('NUL.mp3'), 'unknown'); + expect(sanitizeDateiname('AUX.melo'), 'unknown'); + expect(sanitizeDateiname('COM1.mp3'), 'unknown'); + expect(sanitizeDateiname('LPT9.lrc'), 'unknown'); + }); + + test('case-insensitiv', () { + expect(sanitizeDateiname('con'), 'unknown'); + expect(sanitizeDateiname('nul'), 'unknown'); + expect(sanitizeDateiname('CoM4'), 'unknown'); + expect(sanitizeDateiname('aux'), 'unknown'); + }); + + test('COM10/LPT10 sind NICHT reserviert (nur 1-9)', () { + expect(sanitizeDateiname('COM10'), 'COM10'); + expect(sanitizeDateiname('LPT10'), 'LPT10'); + }); + + test('Namen, die nur mit reservierten Wörtern anfangen, bleiben', () { + expect(sanitizeDateiname('console'), 'console'); + expect(sanitizeDateiname('constante'), 'constante'); + expect(sanitizeDateiname('LPT1 - Drucker'), 'LPT1 - Drucker'); + }); + }); + + group('sanitizeDateiname — Zeichen & Normalisierung', () { + test('entfernt NULL-Bytes und Steuerzeichen', () { + expect(sanitizeDateiname('song\u0000.mp3'), 'song.mp3'); + expect(sanitizeDateiname('a\u0001b\u001Fc'), 'abc'); + }); + + test('entfernt Sonderzeichen, behält Wort-Zeichen/Leerzeichen/Punkte/-', + () { + expect(sanitizeDateiname('Mein Song - Remix (2026)!.mp3'), + 'Mein Song - Remix 2026.mp3'); + }); + + test('trailing Punkte/Leerzeichen werden entfernt (Windows)', () { + expect(sanitizeDateiname('song..'), 'song'); + expect(sanitizeDateiname('song. '), 'song'); + }); + + test('leere/unlesbare Eingabe → unknown', () { + expect(sanitizeDateiname(''), 'unknown'); + expect(sanitizeDateiname('///'), 'unknown'); + expect(sanitizeDateiname('...'), 'unknown'); + expect(sanitizeDateiname(' '), 'unknown'); + }); + + test('kürzt auf 200 Zeichen', () { + final lang = 'a' * 300; + expect(sanitizeDateiname(lang).length, 200); + }); + + test('normaler Dateiname bleibt unverändert', () { + expect(sanitizeDateiname('Künstler - Titel 2026.mp3'), + 'Künstler - Titel 2026.mp3'); + }); + }); +}