From c636981db57d7bab347d501cdc67fb7c90e064c9 Mon Sep 17 00:00:00 2001 From: "google-labs-jules[bot]" <161369871+google-labs-jules[bot]@users.noreply.github.com> Date: Wed, 16 Sep 2026 17:24:52 +0000 Subject: [PATCH 1/2] =?UTF-8?q?=F0=9F=9B=A1=EF=B8=8F=20Sentinel:=20Enforce?= =?UTF-8?q?=20HTTPS=20scheme=20for=20remote=20AI=20endpoints?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Enforces HTTPS transport for remote AI API endpoints to prevent MITM eavesdropping, while permitting HTTP for local loopback development hosts. --- .jules/sentinel.md | 3 ++ lib/providers/settings_provider.dart | 12 +++++ lib/services/ai_endpoint_validator.dart | 26 ++++++++++ .../services/ai_endpoint_validator_test.dart | 50 +++++++++++++++++++ .../unit/settings/settings_provider_test.dart | 26 ++++++++++ 5 files changed, 117 insertions(+) create mode 100644 .jules/sentinel.md create mode 100644 lib/services/ai_endpoint_validator.dart create mode 100644 test/unit/services/ai_endpoint_validator_test.dart diff --git a/.jules/sentinel.md b/.jules/sentinel.md new file mode 100644 index 0000000..79618ac --- /dev/null +++ b/.jules/sentinel.md @@ -0,0 +1,3 @@ +# Sentinel Security Journal + +## 2026-09-13 - AI Endpoint Transport Security Validation **Vulnerability:** Remote AI service endpoints permitted unencrypted HTTP, risking plaintext transmission of user credentials and sensitive AI prompt data over untrusted networks. **Learning:** User-configurable endpoints often mix remote API services with local development servers (e.g., Ollama), requiring explicit differentiation between loopback hosts and remote endpoints. **Prevention:** Enforce HTTPS scheme validation for remote endpoints while allowing HTTP strictly for local loopback hosts (`localhost`, `127.0.0.1`, `::1`). diff --git a/lib/providers/settings_provider.dart b/lib/providers/settings_provider.dart index a005a2f..9208d22 100644 --- a/lib/providers/settings_provider.dart +++ b/lib/providers/settings_provider.dart @@ -1,5 +1,6 @@ import 'package:flutter/material.dart'; import 'package:janus/models/app_settings.dart'; +import 'package:janus/services/ai_endpoint_validator.dart'; import 'package:janus/services/settings_service.dart'; import 'package:riverpod_annotation/riverpod_annotation.dart'; @@ -192,6 +193,17 @@ class AppSettingsNotifier extends _$AppSettingsNotifier { } // AI settings + Future setEndPoint(String endPoint) async { + final trimmed = endPoint.trim(); + if (trimmed.isNotEmpty && !AiEndpointValidator.isValidEndpoint(trimmed)) { + return false; + } + await _persist( + (state.value ?? const AppSettings()).copyWith(endPoint: trimmed), + ); + return true; + } + Future setUseAiDailySummary(bool enabled) async { await _persist( (state.value ?? const AppSettings()).copyWith(aiDailySummary: enabled), diff --git a/lib/services/ai_endpoint_validator.dart b/lib/services/ai_endpoint_validator.dart new file mode 100644 index 0000000..2a9fe89 --- /dev/null +++ b/lib/services/ai_endpoint_validator.dart @@ -0,0 +1,26 @@ +/// Utility for validating AI service API endpoints for secure transport. +class AiEndpointValidator { + AiEndpointValidator._(); + + /// Validates whether [url] is a secure AI endpoint URL. + /// + /// Enforces HTTPS for remote connections to prevent plaintext credential + /// interception via MITM attacks, while permitting HTTP for local loopback hosts + /// (`localhost`, `127.0.0.1`, `::1`) used during local LLM development. + static bool isValidEndpoint(String url) { + if (url.trim().isEmpty) return false; + final uri = Uri.tryParse(url.trim()); + if (uri == null || !uri.hasScheme) return false; + + final scheme = uri.scheme.toLowerCase(); + if (scheme == 'https') return true; + if (scheme == 'http') { + final host = uri.host.toLowerCase(); + return host == 'localhost' || + host == '127.0.0.1' || + host == '::1' || + host == '[::1]'; + } + return false; + } +} diff --git a/test/unit/services/ai_endpoint_validator_test.dart b/test/unit/services/ai_endpoint_validator_test.dart new file mode 100644 index 0000000..d95a8d5 --- /dev/null +++ b/test/unit/services/ai_endpoint_validator_test.dart @@ -0,0 +1,50 @@ +import 'package:flutter_test/flutter_test.dart'; +import 'package:janus/services/ai_endpoint_validator.dart'; + +void main() { + group('AiEndpointValidator', () { + test('allows valid HTTPS remote URLs', () { + expect( + AiEndpointValidator.isValidEndpoint('https://api.openai.com/v1'), + isTrue, + ); + expect( + AiEndpointValidator.isValidEndpoint('https://api.deepseek.com'), + isTrue, + ); + }); + + test('rejects unencrypted HTTP remote URLs', () { + expect( + AiEndpointValidator.isValidEndpoint('http://api.openai.com/v1'), + isFalse, + ); + expect( + AiEndpointValidator.isValidEndpoint('http://example.com/api'), + isFalse, + ); + }); + + test('allows HTTP URLs for local development loopback hosts', () { + expect( + AiEndpointValidator.isValidEndpoint('http://localhost:11434'), + isTrue, + ); + expect( + AiEndpointValidator.isValidEndpoint('http://127.0.0.1:8080/v1'), + isTrue, + ); + expect( + AiEndpointValidator.isValidEndpoint('http://[::1]:11434'), + isTrue, + ); + }); + + test('rejects empty, invalid, or non-http/https URIs', () { + expect(AiEndpointValidator.isValidEndpoint(''), isFalse); + expect(AiEndpointValidator.isValidEndpoint(' '), isFalse); + expect(AiEndpointValidator.isValidEndpoint('ftp://server.com'), isFalse); + expect(AiEndpointValidator.isValidEndpoint('not_a_url'), isFalse); + }); + }); +} diff --git a/test/unit/settings/settings_provider_test.dart b/test/unit/settings/settings_provider_test.dart index c799fe9..56d3bea 100644 --- a/test/unit/settings/settings_provider_test.dart +++ b/test/unit/settings/settings_provider_test.dart @@ -414,6 +414,32 @@ void main() { }); // AI + test('setEndPoint saves valid HTTPS and loopback endpoints', () async { + final container = ProviderContainer(); + addTearDown(() => container.dispose()); + await waitForInit(container); + + final success = + await notifierOf(container).setEndPoint('https://api.openai.com/v1'); + expect(success, isTrue); + expect( + container.read(appSettingsProvider).value!.endPoint, + 'https://api.openai.com/v1', + ); + }); + + test('setEndPoint rejects unencrypted remote HTTP endpoints', () async { + final container = ProviderContainer(); + addTearDown(() => container.dispose()); + await waitForInit(container); + + final success = await notifierOf( + container, + ).setEndPoint('http://insecure.remote.com/api'); + expect(success, isFalse); + expect(container.read(appSettingsProvider).value!.endPoint, ''); + }); + test('setUseAiDailySummary 更新 AI 日报开关', () async { final container = ProviderContainer(); addTearDown(() => container.dispose()); From 06c2e24f52330946d4e0ad2d6f1e66b595bf1471 Mon Sep 17 00:00:00 2001 From: "google-labs-jules[bot]" <161369871+google-labs-jules[bot]@users.noreply.github.com> Date: Wed, 16 Sep 2026 17:32:35 +0000 Subject: [PATCH 2/2] =?UTF-8?q?=F0=9F=9B=A1=EF=B8=8F=20Sentinel:=20Enforce?= =?UTF-8?q?=20HTTPS=20scheme=20for=20remote=20AI=20endpoints?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Enforces HTTPS transport for remote AI API endpoints to prevent MITM eavesdropping, while permitting HTTP for local loopback development hosts. Formatted updated test files with dart format. --- test/unit/services/ai_endpoint_validator_test.dart | 5 +---- test/unit/settings/settings_provider_test.dart | 5 +++-- 2 files changed, 4 insertions(+), 6 deletions(-) diff --git a/test/unit/services/ai_endpoint_validator_test.dart b/test/unit/services/ai_endpoint_validator_test.dart index d95a8d5..8560a2b 100644 --- a/test/unit/services/ai_endpoint_validator_test.dart +++ b/test/unit/services/ai_endpoint_validator_test.dart @@ -34,10 +34,7 @@ void main() { AiEndpointValidator.isValidEndpoint('http://127.0.0.1:8080/v1'), isTrue, ); - expect( - AiEndpointValidator.isValidEndpoint('http://[::1]:11434'), - isTrue, - ); + expect(AiEndpointValidator.isValidEndpoint('http://[::1]:11434'), isTrue); }); test('rejects empty, invalid, or non-http/https URIs', () { diff --git a/test/unit/settings/settings_provider_test.dart b/test/unit/settings/settings_provider_test.dart index 56d3bea..247db5d 100644 --- a/test/unit/settings/settings_provider_test.dart +++ b/test/unit/settings/settings_provider_test.dart @@ -419,8 +419,9 @@ void main() { addTearDown(() => container.dispose()); await waitForInit(container); - final success = - await notifierOf(container).setEndPoint('https://api.openai.com/v1'); + final success = await notifierOf( + container, + ).setEndPoint('https://api.openai.com/v1'); expect(success, isTrue); expect( container.read(appSettingsProvider).value!.endPoint,