Skip to content

Commit 3564cb2

Browse files
Merge pull request #408 from QueryaHub/issue/400-theme-remote-sha256
fix(theme): require SHA256 checksum for remote theme install
2 parents 2617a81 + 7ea3731 commit 3564cb2

3 files changed

Lines changed: 36 additions & 2 deletions

File tree

‎docs/security.md‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ On upgrade from older databases, existing plaintext secrets in SQLite are **migr
2222

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

25+
2526
## Archive install limits (extensions and updates)
2627

2728
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:
@@ -35,3 +36,4 @@ Marketplace downloads, local extension sideload (`.zip` / `.qext`), and in-app u
3536
| Max compression ratio (uncompressed ÷ compressed) | 100:1 |
3637

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

‎lib/core/theme/theme_remote_install_service.dart‎

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -75,6 +75,12 @@ class ThemeRemoteInstallService {
7575
final expectedChecksum = _normalizeSha256(
7676
sha256Checksum ?? uri.queryParameters['sha256'],
7777
);
78+
if (expectedChecksum == null) {
79+
return const ThemeDefinitionImportFailure(
80+
'SHA256 checksum is required for remote theme install. '
81+
'Add ?sha256= to the URL or pass sha256Checksum.',
82+
);
83+
}
7884

7985
File? tempFile;
8086
try {
@@ -92,7 +98,7 @@ class ThemeRemoteInstallService {
9298
}
9399

94100
final actualChecksum = sha256.convert(utf8.encode(body)).toString();
95-
if (expectedChecksum != null && expectedChecksum != actualChecksum) {
101+
if (expectedChecksum != actualChecksum) {
96102
return const ThemeDefinitionImportFailure(
97103
'Checksum mismatch. Theme was not installed.',
98104
);

‎test/core/theme/theme_remote_install_service_test.dart‎

Lines changed: 27 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
1+
import 'dart:convert';
12
import 'dart:io';
23

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

31+
String _sha256Hex(String body) => sha256.convert(utf8.encode(body)).toString();
32+
2933
void main() {
3034
TestWidgetsFlutterBinding.ensureInitialized();
3135

@@ -100,6 +104,7 @@ void main() {
100104

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

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

144+
test('rejects install when SHA256 checksum is missing', () async {
145+
final service = ThemeRemoteInstallService(
146+
registry,
147+
allowLocalhostInDebug: false,
148+
);
149+
150+
final result = await service.installFromUrl(
151+
'https://cdn.example.com/themes/querya_custom_dark.json',
152+
);
153+
154+
expect(result, isA<ThemeDefinitionImportFailure>());
155+
expect(
156+
(result as ThemeDefinitionImportFailure).message,
157+
contains('SHA256 checksum is required'),
158+
);
159+
});
160+
139161
test('rejects invalid JSON without writing to themes folder', () async {
162+
const brokenBody = '{ not valid json';
140163
final service = ThemeRemoteInstallService(
141164
registry,
142165
allowLocalhostInDebug: false,
143166
httpGet: (_) async => const RemoteThemeHttpResponse(
144167
statusCode: 200,
145-
body: '{ not valid json',
168+
body: brokenBody,
146169
),
147170
);
148171

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

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

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

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

236261
final result = await controller.importRegistryThemeFromUrl(
237262
'https://cdn.example.com/themes/querya_custom_dark.json',
263+
sha256Checksum: _sha256Hex(raw),
238264
remoteInstallService: ThemeRemoteInstallService(
239265
registry,
240266
allowLocalhostInDebug: false,

0 commit comments

Comments
 (0)