mirror of
https://github.com/ksyasuda/SubMiner.git
synced 2026-08-16 13:55:51 -07:00
fix(anime): harden the extension bridge against untrusted repos and hangs
Addresses CodeRabbit review feedback on the Anime Browser: - reject repository package/apk names that are not plain identifiers, and verify the install target resolves inside the extensions directory - count mpv's %n% option escape in UTF-8 bytes, and escape backslashes in header values so a trailing one cannot eat the list separator - key the bridge extension-id cache by APK content, so an in-place upgrade re-uploads instead of running the previous build - bound every bridge, release-listing, and download request with a timeout - enforce the APK size limit while streaming rather than after buffering - read APK bytes on demand instead of holding a base64 copy per extension for the lifetime of the browser - serialize preference mutations and write the file atomically - handle the sidecar spawn error event, and wait for the child to exit in stop() before returning - report a failed Anime Browser bootstrap instead of showing the starting banner forever - keep the preferences panel's save confirmation and in-flight multi-select edits by re-rendering only on a structural schema change
This commit is contained in:
@@ -138,6 +138,49 @@ test('an oversized download is refused even when the length header lies', async
|
||||
assert.equal(existsSync(path.join(dir, `${PKG}.apk`)), false);
|
||||
});
|
||||
|
||||
test('the byte limit stops the read instead of buffering the whole body', async () => {
|
||||
const dir = await mkdtemp(path.join(tmpdir(), 'subminer-install-'));
|
||||
let pushed = 0;
|
||||
// Endless body: if the limit were only checked after buffering, this hangs.
|
||||
const body = new ReadableStream<Uint8Array>({
|
||||
pull(controller) {
|
||||
pushed += 1;
|
||||
controller.enqueue(new Uint8Array(512));
|
||||
},
|
||||
});
|
||||
const fetchImpl = (async () => new Response(body, { status: 200 })) as typeof fetch;
|
||||
|
||||
await assert.rejects(
|
||||
() =>
|
||||
installExtension({
|
||||
extensionsDir: dir,
|
||||
extension: repoExtension(),
|
||||
fetchImpl,
|
||||
maxBytes: 1024,
|
||||
}),
|
||||
/larger than the 1024 byte limit/,
|
||||
);
|
||||
// Only enough chunks to cross the limit were ever read.
|
||||
assert.ok(pushed <= 4, `read ${pushed} chunks before aborting`);
|
||||
});
|
||||
|
||||
test('a package name carrying path separators cannot escape the extensions dir', async () => {
|
||||
const root = await mkdtemp(path.join(tmpdir(), 'subminer-install-'));
|
||||
const dir = path.join(root, 'extensions');
|
||||
const escaping = `eu.kanade.tachiyomi.animeextension${path.sep}..${path.sep}..${path.sep}pwned`;
|
||||
|
||||
await assert.rejects(
|
||||
() =>
|
||||
installExtension({
|
||||
extensionsDir: dir,
|
||||
extension: repoExtension({ pkg: escaping }),
|
||||
fetchImpl: respondWith(apkBytes()),
|
||||
}),
|
||||
/not a valid file name/,
|
||||
);
|
||||
assert.equal(existsSync(path.join(root, 'pwned.apk')), false);
|
||||
});
|
||||
|
||||
test('removeExtension deletes the file and tolerates a missing one', async () => {
|
||||
const dir = await mkdtemp(path.join(tmpdir(), 'subminer-install-'));
|
||||
const file = path.join(dir, `${PKG}.apk`);
|
||||
|
||||
Reference in New Issue
Block a user