v2.50.3 — sanitize.dart gehärtet: Windows-Namen + Traversal-Sequenzen
## 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
This commit is contained in:
+31
-5
@@ -1,5 +1,6 @@
|
|||||||
/// Zentrale Sanitize-Funktion für Dateinamen.
|
/// 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) {
|
String sanitizeDateiname(String name) {
|
||||||
// 1. Extrahiere nur den Dateinamen (keine Pfad-Komponenten)
|
// 1. Extrahiere nur den Dateinamen (keine Pfad-Komponenten)
|
||||||
var safe = name.split('/').last.split('\\').last;
|
var safe = name.split('/').last.split('\\').last;
|
||||||
@@ -8,16 +9,41 @@ String sanitizeDateiname(String name) {
|
|||||||
safe = safe.replaceAll(RegExp(r'[\x00-\x1F\x7F]'), '');
|
safe = safe.replaceAll(RegExp(r'[\x00-\x1F\x7F]'), '');
|
||||||
|
|
||||||
// 3. Entferne alles außer Wort-Zeichen, Leerzeichen und Bindestrichen
|
// 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'^\.+'), '');
|
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();
|
safe = safe.trim();
|
||||||
if (safe.isEmpty) safe = 'unknown';
|
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);
|
if (safe.length > 200) safe = safe.substring(0, 200);
|
||||||
|
|
||||||
return safe;
|
return safe;
|
||||||
|
|||||||
@@ -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');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user