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:
Dustin
2026-08-03 01:41:04 +02:00
parent de71119235
commit e46ffa76a3
10 changed files with 76 additions and 34 deletions
+4 -4
View File
@@ -54,20 +54,20 @@ class MeloAudioHandler extends BaseAudioHandler {
Future<void> play() async {
if (_player.aktuellerSong != null) {
_playing = true;
await _player.playPause();
await _player.play();
}
}
@override
Future<void> pause() async {
_playing = false;
await _player.playPause();
await _player.pause();
}
@override
Future<void> stop() async {
_playing = false;
await _player.playPause();
await _player.pause();
playbackState.add(playbackState.value.copyWith(
controls: [MediaControl.play],
playing: false,
@@ -77,7 +77,7 @@ class MeloAudioHandler extends BaseAudioHandler {
@override
Future<void> seek(Duration position) async {
_player.spiele(_player.aktuellerSong!, position: position.inSeconds);
await _player.seek(position);
}
@override
+13 -15
View File
@@ -1,6 +1,6 @@
import 'dart:convert';
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 '../services/melo_logger.dart';
@@ -11,6 +11,8 @@ class AuthService {
factory AuthService() => _instance;
AuthService._();
final FlutterSecureStorage _secure = const FlutterSecureStorage();
String? _token;
String _user = '';
bool _initialisiert = false;
@@ -34,9 +36,8 @@ class AuthService {
Future<bool> initialisieren() async {
if (_initialisiert) return istEingeloggt;
try {
final prefs = await SharedPreferences.getInstance();
_token = prefs.getString('baka_token');
_user = prefs.getString('baka_user') ?? '';
_token = await _secure.read(key: 'baka_token');
_user = await _secure.read(key: 'baka_user') ?? '';
if (_token != null && _token!.isNotEmpty) {
// Token beim Server validieren (nicht blind vertrauen)
@@ -46,8 +47,8 @@ class AuthService {
MeloLogger().zustand('auth_token_expired', {'user': _user});
_token = null;
_user = '';
await prefs.remove('baka_token');
await prefs.remove('baka_user');
await _secure.delete(key: 'baka_token');
await _secure.delete(key: 'baka_user');
} else {
MeloLogger().zustand('auth_restored', {'user': _user});
}
@@ -100,9 +101,8 @@ class AuthService {
_user = user;
if (_token != null && _token!.isNotEmpty) {
final prefs = await SharedPreferences.getInstance();
await prefs.setString('baka_token', _token!);
await prefs.setString('baka_user', _user);
await _secure.write(key: 'baka_token', value: _token!);
await _secure.write(key: 'baka_user', value: _user);
MeloLogger().aktion('auth_login_ok', {'user': _user});
return AuthResult.ok;
}
@@ -144,9 +144,8 @@ class AuthService {
_user = user;
if (_token != null && _token!.isNotEmpty) {
final prefs = await SharedPreferences.getInstance();
await prefs.setString('baka_token', _token!);
await prefs.setString('baka_user', _user);
await _secure.write(key: 'baka_token', value: _token!);
await _secure.write(key: 'baka_user', value: _user);
MeloLogger().aktion('auth_register_ok', {'user': _user});
return AuthResult.ok;
}
@@ -172,9 +171,8 @@ class AuthService {
Future<void> logout() async {
_token = null;
_user = '';
final prefs = await SharedPreferences.getInstance();
await prefs.remove('baka_token');
await prefs.remove('baka_user');
await _secure.delete(key: 'baka_token');
await _secure.delete(key: 'baka_user');
MeloLogger().aktion('auth_logout', {});
}
}
-5
View File
@@ -11,11 +11,9 @@ class CloudService {
static final http.Client _client = http.Client();
static String get _base => AppConfig.cloudUrl;
String _user = '';
/// Login mit Baka-Auth Token wird aus AuthService bezogen
Future<bool> login(String user) async {
_user = user;
try {
final r = await _client
.get(Uri.parse('$_base/api/cloud/status'),
@@ -32,9 +30,6 @@ class CloudService {
final headers = <String, String>{
'X-API-Key': AppConfig.ytProxyApiKey,
};
if (_user.isNotEmpty) {
headers['X-User'] = _user;
}
if (token != null && token.isNotEmpty) {
headers['Authorization'] = 'Bearer $token';
}
+4 -3
View File
@@ -12,6 +12,7 @@ import '../utils/audio_validator.dart';
import 'melo_logger.dart';
import '../config/app_config.dart';
import '../main.dart'; // für notificationsPlugin
import '../utils/sanitize.dart';
/// Download-Service: Lädt YouTube-Audio über den yt-proxy herunter.
/// Nutzt ChangeNotifier für UI-Updates via ListenableBuilder.
@@ -292,9 +293,9 @@ class DownloadService extends ChangeNotifier {
: Directory('${(await getApplicationDocumentsDirectory()).path}/music');
if (!await dir.exists()) await dir.create(recursive: true);
// Sicheren Dateinamen erstellen
final safeName = titel.replaceAll(RegExp(r'[^\w\s-]'), '').trim();
final lokalerName = '${safeName.isEmpty ? "song" : safeName}.mp3';
// Sicheren Dateinamen erstellen (zentrale Sanitize-Funktion)
final safeName = sanitizeDateiname(titel);
final lokalerName = '$safeName.mp3';
dateiPfad = '${dir.path}/$lokalerName';
final stopwatch2 = Stopwatch()..start();
+7 -4
View File
@@ -5,8 +5,10 @@ import 'package:crypto/crypto.dart';
import 'package:http/http.dart' as http;
import 'package:path_provider/path_provider.dart';
import 'package:shared_preferences/shared_preferences.dart';
import 'package:flutter_secure_storage/flutter_secure_storage.dart';
import '../models/song.dart';
import '../database/db_helper.dart';
import '../utils/sanitize.dart';
/// Ein Song aus der Subsonic-API
class SubsonicSong {
@@ -63,6 +65,7 @@ class SubsonicAlbum {
class NavidromeService {
final DbHelper _db = DbHelper();
final FlutterSecureStorage _secure = const FlutterSecureStorage();
String _serverUrl = '';
String _user = '';
@@ -86,7 +89,7 @@ class NavidromeService {
final prefs = await SharedPreferences.getInstance();
final url = prefs.getString('navidrome_url');
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) {
setCredentials(url, user, pass);
}
@@ -101,7 +104,7 @@ class NavidromeService {
final prefs = await SharedPreferences.getInstance();
await prefs.setString('navidrome_url', url);
await prefs.setString('navidrome_user', user);
await prefs.setString('navidrome_pass', password);
await _secure.write(key: 'navidrome_pass', value: password);
} catch (e) {
debugPrint('Fehler beim Speichern der Navidrome-Zugangsdaten: $e');
}
@@ -178,9 +181,9 @@ class NavidromeService {
final musikDir = Directory('${dir.path}/music');
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 dateiName = '${safeName.isEmpty ? "song" : safeName}_$kurzId.mp3';
final dateiName = '${safeName}_$kurzId.mp3';
final dateiPfad = '${musikDir.path}/$dateiName';
final file = File(dateiPfad);
+18
View File
@@ -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 {
if (_player == null || aktuellerSong == null) return;
final pos = _p.position;