From d245e7c0df6085b531aaae127abdbd3f93211b17 Mon Sep 17 00:00:00 2001 From: ZhuchkaTriplesix Date: Mon, 27 Jul 2026 06:22:16 +0300 Subject: [PATCH] fix(marketplace): require SHA256 checksum before HttpMarketplace install MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Closes #396 — marketplace install fails closed when manifest checksum is missing or empty, matching the updater security model. --- .../market/http_marketplace_repository.dart | 25 +++++--- .../market/marketplace_repository_test.dart | 61 +++++++++++++++++++ 2 files changed, 76 insertions(+), 10 deletions(-) diff --git a/lib/core/market/http_marketplace_repository.dart b/lib/core/market/http_marketplace_repository.dart index 1d1a5caf..f85a65ca 100644 --- a/lib/core/market/http_marketplace_repository.dart +++ b/lib/core/market/http_marketplace_repository.dart @@ -124,21 +124,26 @@ class HttpMarketplaceRepository implements MarketplaceRepository { try { // Step 2: SHA-256 Integrity Verification (Critical Security Check) - if (manifest.sha256Checksum != null && manifest.sha256Checksum!.trim().isNotEmpty) { - final bytes = await archiveFile.readAsBytes(); - final actualSha256 = sha256.convert(bytes).toString().toLowerCase(); - final expectedSha256 = manifest.sha256Checksum!.trim().toLowerCase(); - if (actualSha256 != expectedSha256) { - throw MarketplaceException( - 'SHA256 checksum mismatch for "${manifest.id}". Expected: $expectedSha256, Actual: $actualSha256. Installation aborted.', - ); - } + final expectedSha256 = manifest.sha256Checksum?.trim().toLowerCase(); + if (expectedSha256 == null || expectedSha256.isEmpty) { + throw MarketplaceException( + 'Extension manifest is missing SHA256 checksum for "${manifest.id}". ' + 'Installation aborted.', + ); + } + + final bytes = await archiveFile.readAsBytes(); + final actualSha256 = sha256.convert(bytes).toString().toLowerCase(); + if (actualSha256 != expectedSha256) { + throw MarketplaceException( + 'SHA256 checksum mismatch for "${manifest.id}". ' + 'Expected: $expectedSha256, Actual: $actualSha256. Installation aborted.', + ); } onProgress?.call(0.85); // Step 3: Safe Archive Extraction (Preventing Path Traversal / Zip Bomb - Issue #242) - final bytes = await archiveFile.readAsBytes(); final archive = ZipDecoder().decodeBytes(bytes); final dir = await ExtensionPaths.extensionsDirectory(); diff --git a/test/core/market/marketplace_repository_test.dart b/test/core/market/marketplace_repository_test.dart index db9e14d2..e3aeeda1 100644 --- a/test/core/market/marketplace_repository_test.dart +++ b/test/core/market/marketplace_repository_test.dart @@ -231,6 +231,67 @@ void main() { ); }); + test('install aborts when SHA256 checksum is missing', () async { + final archive = Archive(); + archive.addFile(ArchiveFile('test.txt', 4, utf8.encode('good'))); + final zipBytes = ZipEncoder().encode(archive); + + final mockClient = MockClient((request) async { + return http.Response.bytes(zipBytes, 200); + }); + + final repo = HttpMarketplaceRepository(client: mockClient); + const manifest = ExtensionManifest( + id: 'test.no-sha256', + name: 'No SHA256', + version: '1.0.0', + publisher: 'Test', + type: ExtensionType.theme, + engines: {'querya_desktop': '*'}, + downloadUrl: 'http://localhost:8000/test.zip', + ); + + expect( + () => repo.install(manifest), + throwsA(isA().having( + (e) => e.message, + 'message', + contains('missing SHA256 checksum'), + )), + ); + }); + + test('install aborts when SHA256 checksum is empty', () async { + final archive = Archive(); + archive.addFile(ArchiveFile('test.txt', 4, utf8.encode('good'))); + final zipBytes = ZipEncoder().encode(archive); + + final mockClient = MockClient((request) async { + return http.Response.bytes(zipBytes, 200); + }); + + final repo = HttpMarketplaceRepository(client: mockClient); + const manifest = ExtensionManifest( + id: 'test.empty-sha256', + name: 'Empty SHA256', + version: '1.0.0', + publisher: 'Test', + type: ExtensionType.theme, + engines: {'querya_desktop': '*'}, + downloadUrl: 'http://localhost:8000/test.zip', + sha256Checksum: ' ', + ); + + expect( + () => repo.install(manifest), + throwsA(isA().having( + (e) => e.message, + 'message', + contains('missing SHA256 checksum'), + )), + ); + }); + test('install prevents Path Traversal during archive unpacking (Issue #242)', () async { final archive = Archive(); archive.addFile(ArchiveFile('../evil.txt', 4, utf8.encode('evil')));