Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions .jules/sentinel.md
Original file line number Diff line number Diff line change
@@ -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`).
12 changes: 12 additions & 0 deletions lib/providers/settings_provider.dart
Original file line number Diff line number Diff line change
@@ -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';

Expand Down Expand Up @@ -192,6 +193,17 @@ class AppSettingsNotifier extends _$AppSettingsNotifier {
}

// AI settings
Future<bool> 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<void> setUseAiDailySummary(bool enabled) async {
await _persist(
(state.value ?? const AppSettings()).copyWith(aiDailySummary: enabled),
Expand Down
26 changes: 26 additions & 0 deletions lib/services/ai_endpoint_validator.dart
Original file line number Diff line number Diff line change
@@ -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;
}
}
47 changes: 47 additions & 0 deletions test/unit/services/ai_endpoint_validator_test.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
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);
});
});
}
27 changes: 27 additions & 0 deletions test/unit/settings/settings_provider_test.dart
Original file line number Diff line number Diff line change
Expand Up @@ -414,6 +414,33 @@ 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());
Expand Down
Loading