Skip to content
Merged
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
2 changes: 2 additions & 0 deletions docs/security.md
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ On upgrade from older databases, existing plaintext secrets in SQLite are **migr

Automated tests use an **in-memory** secrets backend (see `test/flutter_test_config.dart`) so CI does not require a desktop keyring.


## Archive install limits (extensions and updates)

Marketplace downloads, local extension sideload (`.zip` / `.qext`), and in-app updater extraction use `SafeZipExtractor` (`lib/core/security/safe_zip_extractor.dart`) with shared default limits:
Expand All @@ -35,3 +36,4 @@ Marketplace downloads, local extension sideload (`.zip` / `.qext`), and in-app u
| Max compression ratio (uncompressed ÷ compressed) | 100:1 |

Archives exceeding these bounds fail closed before files are written to disk. Path traversal checks remain in `archive_path_guard.dart`.

8 changes: 7 additions & 1 deletion lib/core/theme/theme_remote_install_service.dart
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,12 @@ class ThemeRemoteInstallService {
final expectedChecksum = _normalizeSha256(
sha256Checksum ?? uri.queryParameters['sha256'],
);
if (expectedChecksum == null) {
return const ThemeDefinitionImportFailure(
'SHA256 checksum is required for remote theme install. '
'Add ?sha256= to the URL or pass sha256Checksum.',
);
}

File? tempFile;
try {
Expand All @@ -92,7 +98,7 @@ class ThemeRemoteInstallService {
}

final actualChecksum = sha256.convert(utf8.encode(body)).toString();
if (expectedChecksum != null && expectedChecksum != actualChecksum) {
if (expectedChecksum != actualChecksum) {
return const ThemeDefinitionImportFailure(
'Checksum mismatch. Theme was not installed.',
);
Expand Down
28 changes: 27 additions & 1 deletion test/core/theme/theme_remote_install_service_test.dart
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
import 'dart:convert';
import 'dart:io';

import 'package:crypto/crypto.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:path/path.dart' as p;
import 'package:path_provider_platform_interface/path_provider_platform_interface.dart';
Expand All @@ -26,6 +28,8 @@ Future<String> _fixtureAssetLoader(String assetPath) async {
return File(p.join('test/fixtures/themes', fileName)).readAsString();
}

String _sha256Hex(String body) => sha256.convert(utf8.encode(body)).toString();

void main() {
TestWidgetsFlutterBinding.ensureInitialized();

Expand Down Expand Up @@ -100,6 +104,7 @@ void main() {

final result = await service.installFromUrl(
'https://cdn.example.com/themes/querya_custom_dark.json',
sha256Checksum: _sha256Hex(raw),
);

expect(result, isA<ThemeDefinitionImportSuccess>());
Expand Down Expand Up @@ -136,18 +141,37 @@ void main() {
expect(await ExtensionPaths.mockExtensionsDirectory!.list().length, 0);
});

test('rejects install when SHA256 checksum is missing', () async {
final service = ThemeRemoteInstallService(
registry,
allowLocalhostInDebug: false,
);

final result = await service.installFromUrl(
'https://cdn.example.com/themes/querya_custom_dark.json',
);

expect(result, isA<ThemeDefinitionImportFailure>());
expect(
(result as ThemeDefinitionImportFailure).message,
contains('SHA256 checksum is required'),
);
});

test('rejects invalid JSON without writing to themes folder', () async {
const brokenBody = '{ not valid json';
final service = ThemeRemoteInstallService(
registry,
allowLocalhostInDebug: false,
httpGet: (_) async => const RemoteThemeHttpResponse(
statusCode: 200,
body: '{ not valid json',
body: brokenBody,
),
);

final result = await service.installFromUrl(
'https://cdn.example.com/themes/broken.json',
sha256Checksum: _sha256Hex(brokenBody),
);

expect(result, isA<ThemeDefinitionImportFailure>());
Expand Down Expand Up @@ -219,6 +243,7 @@ void main() {

final result = await service.installFromUrl(
'https://cdn.example.com/themes/querya_custom_dark.json',
sha256Checksum: _sha256Hex(raw),
);

expect(result, isA<ThemeDefinitionImportSuccess>());
Expand All @@ -235,6 +260,7 @@ void main() {

final result = await controller.importRegistryThemeFromUrl(
'https://cdn.example.com/themes/querya_custom_dark.json',
sha256Checksum: _sha256Hex(raw),
remoteInstallService: ThemeRemoteInstallService(
registry,
allowLocalhostInDebug: false,
Expand Down
Loading