fix: 6 Claude-Audit-Fixes (3 CRIT + 3 HIGH)
CRITICAL: - Logout: LoginScreen statt MeloHome pushen (home_screen.dart) - Path Traversal: sanitizeDateiname() zentral + sync-loop abgesichert - SecureStorage: flutter_secure_storage für Token+Passwort (auth_service, navidrome) HIGH: - Seek: PlayerService.seek() reload-frei (audio_handler) - Play/Pause: Zielzustand statt Toggle für System-Controls - X-User: Header entfernt, Server verlässt sich auf JWT
This commit is contained in:
@@ -5,6 +5,7 @@ import 'package:path_provider/path_provider.dart';
|
|||||||
import 'package:shared_preferences/shared_preferences.dart';
|
import 'package:shared_preferences/shared_preferences.dart';
|
||||||
import 'package:flutter_local_notifications/flutter_local_notifications.dart';
|
import 'package:flutter_local_notifications/flutter_local_notifications.dart';
|
||||||
import '../utils/farb_theme.dart';
|
import '../utils/farb_theme.dart';
|
||||||
|
import '../utils/sanitize.dart';
|
||||||
import '../services/cloud_service.dart';
|
import '../services/cloud_service.dart';
|
||||||
import '../services/auth_service.dart';
|
import '../services/auth_service.dart';
|
||||||
import '../database/db_helper.dart';
|
import '../database/db_helper.dart';
|
||||||
@@ -216,7 +217,8 @@ class _CloudScreenState extends State<CloudScreen> {
|
|||||||
|
|
||||||
_updateSync('Download: $title...',
|
_updateSync('Download: $title...',
|
||||||
0.1 + (0.4 * processed / (totalNew > 0 ? totalNew : 1)));
|
0.1 + (0.4 * processed / (totalNew > 0 ? totalNew : 1)));
|
||||||
final dest = '${dir.path}/$title';
|
final safeTitle = sanitizeDateiname(title);
|
||||||
|
final dest = '${dir.path}/$safeTitle';
|
||||||
if (await widget.cloud.download(sid, dest)) {
|
if (await widget.cloud.download(sid, dest)) {
|
||||||
downloaded++;
|
downloaded++;
|
||||||
// In DB eintragen mit cloud_id
|
// In DB eintragen mit cloud_id
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ import '../widgets/song_tile.dart';
|
|||||||
import 'download_screen.dart';
|
import 'download_screen.dart';
|
||||||
import 'cloud_screen.dart';
|
import 'cloud_screen.dart';
|
||||||
import 'settings_screen.dart';
|
import 'settings_screen.dart';
|
||||||
|
import 'login_screen.dart';
|
||||||
import '../services/cloud_service.dart';
|
import '../services/cloud_service.dart';
|
||||||
import '../services/auth_service.dart';
|
import '../services/auth_service.dart';
|
||||||
import '../config/app_config.dart';
|
import '../config/app_config.dart';
|
||||||
@@ -397,7 +398,7 @@ class _MeloHomeState extends State<MeloHome> {
|
|||||||
await AuthService().logout();
|
await AuthService().logout();
|
||||||
if (mounted) {
|
if (mounted) {
|
||||||
Navigator.of(context).pushReplacement(
|
Navigator.of(context).pushReplacement(
|
||||||
MaterialPageRoute(builder: (_) => const MeloHome()),
|
MaterialPageRoute(builder: (_) => const LoginScreen()),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
|
|||||||
@@ -54,20 +54,20 @@ class MeloAudioHandler extends BaseAudioHandler {
|
|||||||
Future<void> play() async {
|
Future<void> play() async {
|
||||||
if (_player.aktuellerSong != null) {
|
if (_player.aktuellerSong != null) {
|
||||||
_playing = true;
|
_playing = true;
|
||||||
await _player.playPause();
|
await _player.play();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Future<void> pause() async {
|
Future<void> pause() async {
|
||||||
_playing = false;
|
_playing = false;
|
||||||
await _player.playPause();
|
await _player.pause();
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Future<void> stop() async {
|
Future<void> stop() async {
|
||||||
_playing = false;
|
_playing = false;
|
||||||
await _player.playPause();
|
await _player.pause();
|
||||||
playbackState.add(playbackState.value.copyWith(
|
playbackState.add(playbackState.value.copyWith(
|
||||||
controls: [MediaControl.play],
|
controls: [MediaControl.play],
|
||||||
playing: false,
|
playing: false,
|
||||||
@@ -77,7 +77,7 @@ class MeloAudioHandler extends BaseAudioHandler {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
Future<void> seek(Duration position) async {
|
Future<void> seek(Duration position) async {
|
||||||
_player.spiele(_player.aktuellerSong!, position: position.inSeconds);
|
await _player.seek(position);
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import 'dart:convert';
|
import 'dart:convert';
|
||||||
import 'package:http/http.dart' as http;
|
import 'package:http/http.dart' as http;
|
||||||
import 'package:shared_preferences/shared_preferences.dart';
|
import 'package:flutter_secure_storage/flutter_secure_storage.dart';
|
||||||
import '../config/app_config.dart';
|
import '../config/app_config.dart';
|
||||||
import '../services/melo_logger.dart';
|
import '../services/melo_logger.dart';
|
||||||
|
|
||||||
@@ -11,6 +11,8 @@ class AuthService {
|
|||||||
factory AuthService() => _instance;
|
factory AuthService() => _instance;
|
||||||
AuthService._();
|
AuthService._();
|
||||||
|
|
||||||
|
final FlutterSecureStorage _secure = const FlutterSecureStorage();
|
||||||
|
|
||||||
String? _token;
|
String? _token;
|
||||||
String _user = '';
|
String _user = '';
|
||||||
bool _initialisiert = false;
|
bool _initialisiert = false;
|
||||||
@@ -34,9 +36,8 @@ class AuthService {
|
|||||||
Future<bool> initialisieren() async {
|
Future<bool> initialisieren() async {
|
||||||
if (_initialisiert) return istEingeloggt;
|
if (_initialisiert) return istEingeloggt;
|
||||||
try {
|
try {
|
||||||
final prefs = await SharedPreferences.getInstance();
|
_token = await _secure.read(key: 'baka_token');
|
||||||
_token = prefs.getString('baka_token');
|
_user = await _secure.read(key: 'baka_user') ?? '';
|
||||||
_user = prefs.getString('baka_user') ?? '';
|
|
||||||
|
|
||||||
if (_token != null && _token!.isNotEmpty) {
|
if (_token != null && _token!.isNotEmpty) {
|
||||||
// Token beim Server validieren (nicht blind vertrauen)
|
// Token beim Server validieren (nicht blind vertrauen)
|
||||||
@@ -46,8 +47,8 @@ class AuthService {
|
|||||||
MeloLogger().zustand('auth_token_expired', {'user': _user});
|
MeloLogger().zustand('auth_token_expired', {'user': _user});
|
||||||
_token = null;
|
_token = null;
|
||||||
_user = '';
|
_user = '';
|
||||||
await prefs.remove('baka_token');
|
await _secure.delete(key: 'baka_token');
|
||||||
await prefs.remove('baka_user');
|
await _secure.delete(key: 'baka_user');
|
||||||
} else {
|
} else {
|
||||||
MeloLogger().zustand('auth_restored', {'user': _user});
|
MeloLogger().zustand('auth_restored', {'user': _user});
|
||||||
}
|
}
|
||||||
@@ -100,9 +101,8 @@ class AuthService {
|
|||||||
_user = user;
|
_user = user;
|
||||||
|
|
||||||
if (_token != null && _token!.isNotEmpty) {
|
if (_token != null && _token!.isNotEmpty) {
|
||||||
final prefs = await SharedPreferences.getInstance();
|
await _secure.write(key: 'baka_token', value: _token!);
|
||||||
await prefs.setString('baka_token', _token!);
|
await _secure.write(key: 'baka_user', value: _user);
|
||||||
await prefs.setString('baka_user', _user);
|
|
||||||
MeloLogger().aktion('auth_login_ok', {'user': _user});
|
MeloLogger().aktion('auth_login_ok', {'user': _user});
|
||||||
return AuthResult.ok;
|
return AuthResult.ok;
|
||||||
}
|
}
|
||||||
@@ -144,9 +144,8 @@ class AuthService {
|
|||||||
_user = user;
|
_user = user;
|
||||||
|
|
||||||
if (_token != null && _token!.isNotEmpty) {
|
if (_token != null && _token!.isNotEmpty) {
|
||||||
final prefs = await SharedPreferences.getInstance();
|
await _secure.write(key: 'baka_token', value: _token!);
|
||||||
await prefs.setString('baka_token', _token!);
|
await _secure.write(key: 'baka_user', value: _user);
|
||||||
await prefs.setString('baka_user', _user);
|
|
||||||
MeloLogger().aktion('auth_register_ok', {'user': _user});
|
MeloLogger().aktion('auth_register_ok', {'user': _user});
|
||||||
return AuthResult.ok;
|
return AuthResult.ok;
|
||||||
}
|
}
|
||||||
@@ -172,9 +171,8 @@ class AuthService {
|
|||||||
Future<void> logout() async {
|
Future<void> logout() async {
|
||||||
_token = null;
|
_token = null;
|
||||||
_user = '';
|
_user = '';
|
||||||
final prefs = await SharedPreferences.getInstance();
|
await _secure.delete(key: 'baka_token');
|
||||||
await prefs.remove('baka_token');
|
await _secure.delete(key: 'baka_user');
|
||||||
await prefs.remove('baka_user');
|
|
||||||
MeloLogger().aktion('auth_logout', {});
|
MeloLogger().aktion('auth_logout', {});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -11,11 +11,9 @@ class CloudService {
|
|||||||
static final http.Client _client = http.Client();
|
static final http.Client _client = http.Client();
|
||||||
|
|
||||||
static String get _base => AppConfig.cloudUrl;
|
static String get _base => AppConfig.cloudUrl;
|
||||||
String _user = '';
|
|
||||||
|
|
||||||
/// Login mit Baka-Auth – Token wird aus AuthService bezogen
|
/// Login mit Baka-Auth – Token wird aus AuthService bezogen
|
||||||
Future<bool> login(String user) async {
|
Future<bool> login(String user) async {
|
||||||
_user = user;
|
|
||||||
try {
|
try {
|
||||||
final r = await _client
|
final r = await _client
|
||||||
.get(Uri.parse('$_base/api/cloud/status'),
|
.get(Uri.parse('$_base/api/cloud/status'),
|
||||||
@@ -32,9 +30,6 @@ class CloudService {
|
|||||||
final headers = <String, String>{
|
final headers = <String, String>{
|
||||||
'X-API-Key': AppConfig.ytProxyApiKey,
|
'X-API-Key': AppConfig.ytProxyApiKey,
|
||||||
};
|
};
|
||||||
if (_user.isNotEmpty) {
|
|
||||||
headers['X-User'] = _user;
|
|
||||||
}
|
|
||||||
if (token != null && token.isNotEmpty) {
|
if (token != null && token.isNotEmpty) {
|
||||||
headers['Authorization'] = 'Bearer $token';
|
headers['Authorization'] = 'Bearer $token';
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ import '../utils/audio_validator.dart';
|
|||||||
import 'melo_logger.dart';
|
import 'melo_logger.dart';
|
||||||
import '../config/app_config.dart';
|
import '../config/app_config.dart';
|
||||||
import '../main.dart'; // für notificationsPlugin
|
import '../main.dart'; // für notificationsPlugin
|
||||||
|
import '../utils/sanitize.dart';
|
||||||
|
|
||||||
/// Download-Service: Lädt YouTube-Audio über den yt-proxy herunter.
|
/// Download-Service: Lädt YouTube-Audio über den yt-proxy herunter.
|
||||||
/// Nutzt ChangeNotifier für UI-Updates via ListenableBuilder.
|
/// Nutzt ChangeNotifier für UI-Updates via ListenableBuilder.
|
||||||
@@ -292,9 +293,9 @@ class DownloadService extends ChangeNotifier {
|
|||||||
: Directory('${(await getApplicationDocumentsDirectory()).path}/music');
|
: Directory('${(await getApplicationDocumentsDirectory()).path}/music');
|
||||||
if (!await dir.exists()) await dir.create(recursive: true);
|
if (!await dir.exists()) await dir.create(recursive: true);
|
||||||
|
|
||||||
// Sicheren Dateinamen erstellen
|
// Sicheren Dateinamen erstellen (zentrale Sanitize-Funktion)
|
||||||
final safeName = titel.replaceAll(RegExp(r'[^\w\s-]'), '').trim();
|
final safeName = sanitizeDateiname(titel);
|
||||||
final lokalerName = '${safeName.isEmpty ? "song" : safeName}.mp3';
|
final lokalerName = '$safeName.mp3';
|
||||||
dateiPfad = '${dir.path}/$lokalerName';
|
dateiPfad = '${dir.path}/$lokalerName';
|
||||||
|
|
||||||
final stopwatch2 = Stopwatch()..start();
|
final stopwatch2 = Stopwatch()..start();
|
||||||
|
|||||||
@@ -5,8 +5,10 @@ import 'package:crypto/crypto.dart';
|
|||||||
import 'package:http/http.dart' as http;
|
import 'package:http/http.dart' as http;
|
||||||
import 'package:path_provider/path_provider.dart';
|
import 'package:path_provider/path_provider.dart';
|
||||||
import 'package:shared_preferences/shared_preferences.dart';
|
import 'package:shared_preferences/shared_preferences.dart';
|
||||||
|
import 'package:flutter_secure_storage/flutter_secure_storage.dart';
|
||||||
import '../models/song.dart';
|
import '../models/song.dart';
|
||||||
import '../database/db_helper.dart';
|
import '../database/db_helper.dart';
|
||||||
|
import '../utils/sanitize.dart';
|
||||||
|
|
||||||
/// Ein Song aus der Subsonic-API
|
/// Ein Song aus der Subsonic-API
|
||||||
class SubsonicSong {
|
class SubsonicSong {
|
||||||
@@ -63,6 +65,7 @@ class SubsonicAlbum {
|
|||||||
|
|
||||||
class NavidromeService {
|
class NavidromeService {
|
||||||
final DbHelper _db = DbHelper();
|
final DbHelper _db = DbHelper();
|
||||||
|
final FlutterSecureStorage _secure = const FlutterSecureStorage();
|
||||||
|
|
||||||
String _serverUrl = '';
|
String _serverUrl = '';
|
||||||
String _user = '';
|
String _user = '';
|
||||||
@@ -86,7 +89,7 @@ class NavidromeService {
|
|||||||
final prefs = await SharedPreferences.getInstance();
|
final prefs = await SharedPreferences.getInstance();
|
||||||
final url = prefs.getString('navidrome_url');
|
final url = prefs.getString('navidrome_url');
|
||||||
final user = prefs.getString('navidrome_user');
|
final user = prefs.getString('navidrome_user');
|
||||||
final pass = prefs.getString('navidrome_pass');
|
final pass = await _secure.read(key: 'navidrome_pass');
|
||||||
if (url != null && user != null && pass != null && url.isNotEmpty) {
|
if (url != null && user != null && pass != null && url.isNotEmpty) {
|
||||||
setCredentials(url, user, pass);
|
setCredentials(url, user, pass);
|
||||||
}
|
}
|
||||||
@@ -101,7 +104,7 @@ class NavidromeService {
|
|||||||
final prefs = await SharedPreferences.getInstance();
|
final prefs = await SharedPreferences.getInstance();
|
||||||
await prefs.setString('navidrome_url', url);
|
await prefs.setString('navidrome_url', url);
|
||||||
await prefs.setString('navidrome_user', user);
|
await prefs.setString('navidrome_user', user);
|
||||||
await prefs.setString('navidrome_pass', password);
|
await _secure.write(key: 'navidrome_pass', value: password);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
debugPrint('Fehler beim Speichern der Navidrome-Zugangsdaten: $e');
|
debugPrint('Fehler beim Speichern der Navidrome-Zugangsdaten: $e');
|
||||||
}
|
}
|
||||||
@@ -178,9 +181,9 @@ class NavidromeService {
|
|||||||
final musikDir = Directory('${dir.path}/music');
|
final musikDir = Directory('${dir.path}/music');
|
||||||
if (!await musikDir.exists()) await musikDir.create(recursive: true);
|
if (!await musikDir.exists()) await musikDir.create(recursive: true);
|
||||||
|
|
||||||
final safeName = s.titel.replaceAll(RegExp(r'[^\w\s-]'), '').trim();
|
final safeName = sanitizeDateiname(s.titel);
|
||||||
final kurzId = s.id.length > 8 ? s.id.substring(0, 8) : s.id;
|
final kurzId = s.id.length > 8 ? s.id.substring(0, 8) : s.id;
|
||||||
final dateiName = '${safeName.isEmpty ? "song" : safeName}_$kurzId.mp3';
|
final dateiName = '${safeName}_$kurzId.mp3';
|
||||||
final dateiPfad = '${musikDir.path}/$dateiName';
|
final dateiPfad = '${musikDir.path}/$dateiName';
|
||||||
|
|
||||||
final file = File(dateiPfad);
|
final file = File(dateiPfad);
|
||||||
|
|||||||
@@ -86,6 +86,24 @@ class PlayerService {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Explizit abspielen (kein Toggle) — für System-Controls (Bluetooth, etc.)
|
||||||
|
Future<void> play() async {
|
||||||
|
if (_player == null || aktuellerSong == null) return;
|
||||||
|
await _p.play();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Explizit pausieren (kein Toggle) — für System-Controls (Bluetooth, etc.)
|
||||||
|
Future<void> pause() async {
|
||||||
|
if (_player == null) return;
|
||||||
|
await _p.pause();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Nur seeken ohne Reload der Quelle
|
||||||
|
Future<void> seek(Duration position) async {
|
||||||
|
if (_player == null) return;
|
||||||
|
await _p.seek(position);
|
||||||
|
}
|
||||||
|
|
||||||
Future<void> vorheriges() async {
|
Future<void> vorheriges() async {
|
||||||
if (_player == null || aktuellerSong == null) return;
|
if (_player == null || aktuellerSong == null) return;
|
||||||
final pos = _p.position;
|
final pos = _p.position;
|
||||||
|
|||||||
@@ -0,0 +1,24 @@
|
|||||||
|
/// Zentrale Sanitize-Funktion für Dateinamen.
|
||||||
|
/// Verhindert Path-Traversal (../), Null-Bytes und andere gefährliche Zeichen.
|
||||||
|
String sanitizeDateiname(String name) {
|
||||||
|
// 1. Extrahiere nur den Dateinamen (keine Pfad-Komponenten)
|
||||||
|
var safe = name.split('/').last.split('\\').last;
|
||||||
|
|
||||||
|
// 2. Entferne NULL-Bytes und andere gefährliche Steuerzeichen
|
||||||
|
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.-]'), '');
|
||||||
|
|
||||||
|
// 4. Entferne führende Punkte (versteckte Dateien)
|
||||||
|
safe = safe.replaceAll(RegExp(r'^\.+'), '');
|
||||||
|
|
||||||
|
// 5. 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)
|
||||||
|
if (safe.length > 200) safe = safe.substring(0, 200);
|
||||||
|
|
||||||
|
return safe;
|
||||||
|
}
|
||||||
@@ -269,7 +269,7 @@ class MeloHomeViewModel extends ChangeNotifier {
|
|||||||
@override
|
@override
|
||||||
void dispose() {
|
void dispose() {
|
||||||
_positionsSub?.cancel();
|
_positionsSub?.cancel();
|
||||||
player.dispose();
|
// PlayerService ist Singleton — nicht hier disposesen, lebt für die App-Lebensdauer
|
||||||
super.dispose();
|
super.dispose();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user