fix: harden downloader against path traversal, URL escape, oversized bodies

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
2026-06-12 13:11:18 +09:00
parent bfa9c09b28
commit a820e97237
2 changed files with 60 additions and 0 deletions
@@ -129,4 +129,51 @@ void main() {
).sync();
expect(updated, isTrue);
});
test('malicious seasonId never escapes the cache dir', () async {
final outside = Directory('${tmp.path}/../bs_escape_probe');
addTearDown(() async {
if (await outside.exists()) await outside.delete(recursive: true);
});
final client = MockClient((request) async {
if (request.url.path.endsWith('manifest.json')) {
return http.Response(
jsonEncode({
'schemaVersion': 1,
'minAppBuild': 1,
'current': 'x',
'seasons': [
{
'seasonId': '../bs_escape_probe',
'version': 1,
'packUrl': 'seasons/x/pack.json',
'sha256': shaOf(packBody),
},
],
}),
200,
);
}
return http.Response(packBody, 200);
});
final downloader = ContentDownloader(
baseUrl: 'https://example.com/content',
cacheDir: tmp,
client: client,
);
expect(await downloader.sync(), isFalse);
expect(await outside.exists(), isFalse);
});
test('corrupt cache index is treated as empty and re-downloads', () async {
await File('${tmp.path}/cache_index.json').create(recursive: true);
await File('${tmp.path}/cache_index.json').writeAsString('{broken');
final downloader = ContentDownloader(
baseUrl: 'https://example.com/content',
cacheDir: tmp,
client: okClient(),
);
expect(await downloader.sync(), isTrue);
expect(downloader.cachedVersion('season_002'), 1);
});
}