Security-Fixes aus dem flutter-security Audit
CRIT: - Cloud-Token jetzt in flutter_secure_storage (Keystore/Keychain) statt SharedPreferences, mit Migration alter Eintraege - usesCleartextTraffic entfernt (kein HTTP-Klartext mehr) HIGH: - Path-Traversal gefixt: p.basename bei YouTube-Download, Navidrome-Download und Cloud-Auto-Sync MED: - allowBackup=false (kein Backup von Token-Daten) - Navidrome-Salt kryptographisch sicher (Random.secure statt Timestamp)
This commit is contained in:
@@ -5,13 +5,15 @@
|
||||
<uses-permission android:name="android.permission.WAKE_LOCK"/>
|
||||
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" android:maxSdkVersion="32"/>
|
||||
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" android:maxSdkVersion="29"/>
|
||||
<uses-permission android:name="android.permission.MANAGE_EXTERNAL_STORAGE"/>
|
||||
<uses-permission android:name="android.permission.READ_MEDIA_AUDIO"/>
|
||||
<uses-permission android:name="android.permission.POST_NOTIFICATIONS"/>
|
||||
<application
|
||||
android:label="Melo"
|
||||
android:name="${applicationName}"
|
||||
android:icon="@mipmap/ic_launcher"
|
||||
android:usesCleartextTraffic="true">
|
||||
android:allowBackup="false"
|
||||
android:requestLegacyExternalStorage="true">
|
||||
<activity
|
||||
android:name=".MainActivity"
|
||||
android:exported="true"
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import 'dart:convert';
|
||||
import 'dart:io';
|
||||
import 'package:http/http.dart' as http;
|
||||
import 'package:flutter_secure_storage/flutter_secure_storage.dart';
|
||||
import 'package:shared_preferences/shared_preferences.dart';
|
||||
import '../config/app_config.dart';
|
||||
import '../services/melo_logger.dart';
|
||||
@@ -19,6 +20,10 @@ class CloudService {
|
||||
String _user = '';
|
||||
String _token = '';
|
||||
|
||||
/// Token liegt verschlüsselt im Keychain/Keystore (flutter_secure_storage) —
|
||||
/// nicht mehr im Klartext in SharedPreferences (Security-Audit CRIT-1).
|
||||
static const _secure = FlutterSecureStorage();
|
||||
|
||||
String get user => _user;
|
||||
String get token => _token;
|
||||
bool get istAngemeldet => _token.isNotEmpty;
|
||||
@@ -48,12 +53,29 @@ class CloudService {
|
||||
}
|
||||
|
||||
/// Stellt gespeicherten Token wieder her (Auto-Login nach App-Start).
|
||||
/// Migriert einmalig alte SharedPreferences-Einträge in SecureStorage.
|
||||
Future<bool> restoreLogin() async {
|
||||
final prefs = await SharedPreferences.getInstance();
|
||||
final t = prefs.getString('melo_cloud_token') ?? '';
|
||||
var t = await _secure.read(key: 'melo_cloud_token') ?? '';
|
||||
var u = await _secure.read(key: 'melo_cloud_user') ?? '';
|
||||
// Migration alter Versionen (Token lag früher in SharedPreferences)
|
||||
if (t.isEmpty) {
|
||||
final altT = prefs.getString('melo_cloud_token') ?? '';
|
||||
final altU = prefs.getString('melo_cloud_user') ?? '';
|
||||
if (altT.isNotEmpty) {
|
||||
t = altT;
|
||||
u = altU;
|
||||
await _secure.write(key: 'melo_cloud_token', value: t);
|
||||
if (u.isNotEmpty) {
|
||||
await _secure.write(key: 'melo_cloud_user', value: u);
|
||||
}
|
||||
await prefs.remove('melo_cloud_token');
|
||||
await prefs.remove('melo_cloud_user');
|
||||
}
|
||||
}
|
||||
if (t.isEmpty) return false;
|
||||
_token = t;
|
||||
_user = prefs.getString('melo_cloud_user') ?? '';
|
||||
_user = u;
|
||||
MeloLogger.cloudToken = t;
|
||||
return true;
|
||||
}
|
||||
@@ -62,15 +84,13 @@ class CloudService {
|
||||
_token = '';
|
||||
_user = '';
|
||||
MeloLogger.cloudToken = null;
|
||||
final prefs = await SharedPreferences.getInstance();
|
||||
await prefs.remove('melo_cloud_token');
|
||||
await prefs.remove('melo_cloud_user');
|
||||
await _secure.delete(key: 'melo_cloud_token');
|
||||
await _secure.delete(key: 'melo_cloud_user');
|
||||
}
|
||||
|
||||
Future<void> _speichereToken() async {
|
||||
final prefs = await SharedPreferences.getInstance();
|
||||
await prefs.setString('melo_cloud_token', _token);
|
||||
await prefs.setString('melo_cloud_user', _user);
|
||||
await _secure.write(key: 'melo_cloud_token', value: _token);
|
||||
await _secure.write(key: 'melo_cloud_user', value: _user);
|
||||
}
|
||||
|
||||
Map<String, String> get _authHeader => {
|
||||
|
||||
@@ -3,6 +3,7 @@ import 'dart:convert';
|
||||
import 'dart:io';
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:http/http.dart' as http;
|
||||
import 'package:path/path.dart' as p;
|
||||
import 'package:path_provider/path_provider.dart';
|
||||
import '../models/song.dart';
|
||||
import '../database/db_helper.dart';
|
||||
@@ -249,8 +250,8 @@ 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();
|
||||
// Sicheren Dateinamen erstellen (p.basename verhindert Path-Traversal)
|
||||
final safeName = p.basename(titel).replaceAll(RegExp(r'[^\w\s-]'), '').trim();
|
||||
final lokalerName = '${safeName.isEmpty ? "song" : safeName}.mp3';
|
||||
dateiPfad = '${dir.path}/$lokalerName';
|
||||
|
||||
|
||||
@@ -1,8 +1,10 @@
|
||||
import 'dart:convert';
|
||||
import 'dart:io';
|
||||
import 'dart:math';
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:crypto/crypto.dart';
|
||||
import 'package:http/http.dart' as http;
|
||||
import 'package:path/path.dart' as p;
|
||||
import 'package:path_provider/path_provider.dart';
|
||||
import 'package:flutter_secure_storage/flutter_secure_storage.dart';
|
||||
import '../models/song.dart';
|
||||
@@ -81,7 +83,9 @@ class NavidromeService {
|
||||
_serverUrl = url.endsWith('/') ? url.substring(0, url.length - 1) : url;
|
||||
_user = user;
|
||||
_password = password;
|
||||
_salt = DateTime.now().millisecondsSinceEpoch.toString();
|
||||
// Kryptographisch sicherer Salt (vorher millisecondsSinceEpoch = vorhersagbar)
|
||||
final rng = Random.secure();
|
||||
_salt = base64Encode(List.generate(16, (_) => rng.nextInt(256)));
|
||||
_token = md5.convert(utf8.encode(_password + _salt)).toString();
|
||||
}
|
||||
|
||||
@@ -180,7 +184,7 @@ 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 = p.basename(s.titel).replaceAll(RegExp(r'[^\w\s-]'), '').trim();
|
||||
final kurzId = s.id.length > 8 ? s.id.substring(0, 8) : s.id;
|
||||
final dateiName = '${safeName.isEmpty ? "song" : safeName}_$kurzId.mp3';
|
||||
final dateiPfad = '${musikDir.path}/$dateiName';
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import 'dart:async';
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:path/path.dart' as p;
|
||||
import '../database/db_helper.dart';
|
||||
import '../services/player_service.dart';
|
||||
import '../services/musik_scanner.dart';
|
||||
@@ -309,7 +310,7 @@ class MeloHomeViewModel extends ChangeNotifier {
|
||||
.map((f) => f.path.split('/').last).toSet();
|
||||
|
||||
for (final s in serverSongs) {
|
||||
final title = s['title'].toString();
|
||||
final title = p.basename(s['title'].toString());
|
||||
if (localFiles.contains(title)) continue; // schon lokal
|
||||
await cloud.download(s['id'].toString(), '${dir.path}/$title');
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user