diff --git a/.jules/sentinel.md b/.jules/sentinel.md new file mode 100644 index 0000000..8ac55e5 --- /dev/null +++ b/.jules/sentinel.md @@ -0,0 +1,7 @@ +## 2026-08-26 - Enforce HTTPS Endpoint Validation for Remote AI/API Endpoints + +**漏洞:** Unvalidated custom AI endpoint URLs allowed unencrypted HTTP connections to remote servers, exposing API keys and request payloads to Man-in-the-Middle (MitM) attacks. + +**经验心得:** Users frequently configure custom API/AI endpoints, but allowing arbitrary `http://` schemes to remote domains poses a major network security threat. Allowing `http://` only on loopback addresses (`localhost`, `127.0.0.1`, `::1`) retains developer flexibility without compromising production data security. + +**预防措施:** Always validate input URLs for network requests to enforce HTTPS for remote endpoints while explicitly whitelisting loopback hosts for local development. diff --git a/lib/providers/settings_provider.dart b/lib/providers/settings_provider.dart index a005a2f..ed7db3d 100644 --- a/lib/providers/settings_provider.dart +++ b/lib/providers/settings_provider.dart @@ -192,6 +192,17 @@ class AppSettingsNotifier extends _$AppSettingsNotifier { } // AI settings + Future setEndPoint(String endPoint) async { + if (!SettingsService.isValidEndpointUrl(endPoint)) { + throw ArgumentError( + 'Invalid endpoint URL: remote endpoints must use HTTPS', + ); + } + await _persist( + (state.value ?? const AppSettings()).copyWith(endPoint: endPoint), + ); + } + Future setUseAiDailySummary(bool enabled) async { await _persist( (state.value ?? const AppSettings()).copyWith(aiDailySummary: enabled), diff --git a/lib/services/settings_service.dart b/lib/services/settings_service.dart index c40eff9..e682670 100644 --- a/lib/services/settings_service.dart +++ b/lib/services/settings_service.dart @@ -29,4 +29,25 @@ class SettingsService { final prefs = await SharedPreferences.getInstance(); await prefs.setString(_key, jsonEncode(settings.toJson())); } + + /// Validates whether an endpoint URL is secure and well-formed. + /// + /// Remote endpoints must enforce HTTPS to prevent MiTM attacks. + /// Unencrypted HTTP is permitted only for local development on loopback hosts. + static bool isValidEndpointUrl(String url) { + if (url.trim().isEmpty) return true; + final uri = Uri.tryParse(url.trim()); + if (uri == null || + !uri.hasScheme || + (uri.scheme != 'http' && uri.scheme != 'https')) { + return false; + } + if (uri.scheme == 'http') { + final host = uri.host.toLowerCase(); + final isLoopback = + host == 'localhost' || host == '127.0.0.1' || host == '::1'; + if (!isLoopback) return false; + } + return true; + } } diff --git a/test/unit/settings/settings_provider_test.dart b/test/unit/settings/settings_provider_test.dart index c799fe9..91b9f9c 100644 --- a/test/unit/settings/settings_provider_test.dart +++ b/test/unit/settings/settings_provider_test.dart @@ -454,6 +454,54 @@ void main() { expect(container.read(appSettingsProvider).value!.aiPicToTask, true); }); + test( + 'setEndPoint accepts HTTPS and local HTTP, rejects insecure remote HTTP', + () async { + final container = ProviderContainer(); + addTearDown(() => container.dispose()); + await waitForInit(container); + + final notifier = notifierOf(container); + + // HTTPS URL should be accepted + await notifier.setEndPoint('https://api.openai.com/v1'); + expect( + container.read(appSettingsProvider).value!.endPoint, + 'https://api.openai.com/v1', + ); + + // Localhost HTTP should be accepted + await notifier.setEndPoint('http://localhost:8080/v1'); + expect( + container.read(appSettingsProvider).value!.endPoint, + 'http://localhost:8080/v1', + ); + + // Loopback IP HTTP should be accepted + await notifier.setEndPoint('http://127.0.0.1:11434/v1'); + expect( + container.read(appSettingsProvider).value!.endPoint, + 'http://127.0.0.1:11434/v1', + ); + + // Empty endpoint should be accepted + await notifier.setEndPoint(''); + expect(container.read(appSettingsProvider).value!.endPoint, ''); + + // Insecure remote HTTP URL should throw ArgumentError + expect( + () => notifier.setEndPoint('http://insecure-api.example.com/v1'), + throwsArgumentError, + ); + + // Invalid format URL should throw ArgumentError + expect( + () => notifier.setEndPoint('not-a-valid-url'), + throwsArgumentError, + ); + }, + ); + // 用户偏好 test('setTaskCreationMode 更新任务创建模式', () async { final container = ProviderContainer();