diff --git a/tests/test_frame_apk.py b/tests/test_frame_apk.py index 13340c5..40119d4 100644 --- a/tests/test_frame_apk.py +++ b/tests/test_frame_apk.py @@ -165,6 +165,14 @@ class ApkInfo(unittest.TestCase): frame_apk.MAX_MANIFEST = limit self.assertLess(peak, 8 * 1024**2) + def test_refuses_compression_android_cant_read(self): + for method in (zipfile.ZIP_BZIP2, zipfile.ZIP_LZMA): + buf = io.BytesIO() + with zipfile.ZipFile(buf, 'w', method) as z: + z.writestr('AndroidManifest.xml', manifest('com.example.odd', 0x7f010000, 0x7f010001, 21)) + with self.assertRaisesRegex(frame_apk.ApkError, 'compression'): + self.read(buf.getvalue()) + def test_reference_cycles_and_fan_out_are_bounded(self): res = frame_apk.Resources(b'') ref = frame_apk.T_REF diff --git a/ui/frame_apk.py b/ui/frame_apk.py index 66a778b..943af2a 100644 --- a/ui/frame_apk.py +++ b/ui/frame_apk.py @@ -189,7 +189,12 @@ def _icons(attr, res): def _read(z, name, limit): """A member's bytes, inflating at most limit + 1 of them whatever its header claims (ZipFile.read inflates everything first, then trims to the declared size).""" - size = z.getinfo(name).file_size + info = z.getinfo(name) + # Android only reads stored and deflated entries, and only those bound what + # a read inflates (Python 3.9's bzip2 and lzma readers don't). + if info.compress_type not in (zipfile.ZIP_STORED, zipfile.ZIP_DEFLATED): + raise ApkError(f'{name} in the APK uses a compression Android does not') + size = info.file_size if size > limit: raise ApkError(f'{name} in the APK is {size / 1024**2:.0f} MB, more than a real one ({limit // 1024**2} MB)') with z.open(name) as f: diff --git a/ui/index.html b/ui/index.html index 7b2d1cd..8af8bbd 100644 --- a/ui/index.html +++ b/ui/index.html @@ -1277,10 +1277,13 @@ async function installTitle(token, choice) { } finally { $("prog").style.display = "none"; loadTitles(); } } let installedTitles = null; // lower-case ids, so the install dialog can warn before replacing one; null: unknown +let titlesSeq = 0; // a slower, older request mustn't overwrite a newer answer async function loadTitles() { - let list; - try { list = (await api("/api/titles")).titles; } - catch (e) { installedTitles = null; $("titleList").innerHTML = `
${esc(e.message)}
`; return; } + const seq = ++titlesSeq; + let list, err; + try { list = (await api("/api/titles")).titles; } catch (e) { err = e; } + if (seq !== titlesSeq) return; + if (err) { installedTitles = null; $("titleList").innerHTML = `
${esc(err.message)}
`; return; } installedTitles = new Set(list.map(t => String(t.id).toLowerCase())); $("titleCount").textContent = list.length ? `${list.length}` : ""; $("titleList").innerHTML = list.length ? list.map(t => `