From dbb73007eb8f9f8c46c021c3f51962779a161d64 Mon Sep 17 00:00:00 2001 From: "google-labs-jules[bot]" <161369871+google-labs-jules[bot]@users.noreply.github.com> Date: Wed, 26 Aug 2026 17:47:07 +0000 Subject: [PATCH 1/2] security: enforce HTTPS validation for remote AI endpoints Enforce HTTPS protocol for custom AI endpoint URLs to prevent MitM attacks, while permitting HTTP on loopback hosts for local development. --- .jules/sentinel.md | 7 ++++ lib/providers/settings_provider.dart | 9 +++++ lib/services/settings_service.dart | 18 ++++++++++ .../unit/settings/settings_provider_test.dart | 36 +++++++++++++++++++ 4 files changed, 70 insertions(+) create mode 100644 .jules/sentinel.md 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..e6d9add 100644 --- a/lib/providers/settings_provider.dart +++ b/lib/providers/settings_provider.dart @@ -192,6 +192,15 @@ 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..c696a7b 100644 --- a/lib/services/settings_service.dart +++ b/lib/services/settings_service.dart @@ -29,4 +29,22 @@ 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..8b112d4 100644 --- a/test/unit/settings/settings_provider_test.dart +++ b/test/unit/settings/settings_provider_test.dart @@ -454,6 +454,42 @@ 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(); From 5ce98f57e59577d13d72feceacb3fad8ad65c361 Mon Sep 17 00:00:00 2001 From: "google-labs-jules[bot]" <161369871+google-labs-jules[bot]@users.noreply.github.com> Date: Wed, 26 Aug 2026 17:54:29 +0000 Subject: [PATCH 2/2] security: enforce HTTPS validation for remote AI endpoints Enforce HTTPS protocol for custom AI endpoint URLs to prevent MitM attacks, while permitting HTTP on loopback hosts for local development. --- lib/providers/settings_provider.dart | 4 +- lib/services/settings_service.dart | 7 +- .../unit/settings/settings_provider_test.dart | 82 +++++++++++-------- 3 files changed, 55 insertions(+), 38 deletions(-) diff --git a/lib/providers/settings_provider.dart b/lib/providers/settings_provider.dart index e6d9add..ed7db3d 100644 --- a/lib/providers/settings_provider.dart +++ b/lib/providers/settings_provider.dart @@ -194,7 +194,9 @@ 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'); + throw ArgumentError( + 'Invalid endpoint URL: remote endpoints must use HTTPS', + ); } await _persist( (state.value ?? const AppSettings()).copyWith(endPoint: endPoint), diff --git a/lib/services/settings_service.dart b/lib/services/settings_service.dart index c696a7b..e682670 100644 --- a/lib/services/settings_service.dart +++ b/lib/services/settings_service.dart @@ -37,12 +37,15 @@ class SettingsService { 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')) { + 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'; + 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 8b112d4..91b9f9c 100644 --- a/test/unit/settings/settings_provider_test.dart +++ b/test/unit/settings/settings_provider_test.dart @@ -454,41 +454,53 @@ 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( + '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 {