From 139d1da2616955fc22ff2106e4327dd0515ff004 Mon Sep 17 00:00:00 2001 From: stqfdyr <89149493+stqfdyr@users.noreply.github.com> Date: Fri, 25 Sep 2026 02:47:50 +0800 Subject: [PATCH] =?UTF-8?q?fix:=20=E4=B8=BB=E9=A2=98=E5=8C=85=E6=A0=A1?= =?UTF-8?q?=E9=AA=8C=20gzip=20=E6=A0=A1=E9=AA=8C=E5=92=8C=EF=BC=8C?= =?UTF-8?q?=E8=A2=AB=E6=94=B9=E8=BF=87=E7=9A=84=E5=8C=85=E4=B8=8D=E5=86=8D?= =?UTF-8?q?=E8=83=BD=E8=A3=85=E4=B8=8A?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit tar 读到结束块就停,读不到 gzip 尾部的校验和:中途被改过字节、或者只丢了 结束块之后几个字节的包,每个条目都读得出来,照样装上。解压完把压缩流读到底, 校验和不对时给出与截断包相同的「主题包损坏或不完整」。 结束块之后只允许 1 MiB 的填充:不设上限时,塞在后面的零会以每 MiB 上传 1 GiB 解压的比例空耗 CPU。 --- src/frontend.rs | 35 +++++++++++++++++++++++++++++++++++ 1 file changed, 35 insertions(+) diff --git a/src/frontend.rs b/src/frontend.rs index c1d22756..eecf81dc 100644 --- a/src/frontend.rs +++ b/src/frontend.rs @@ -257,6 +257,11 @@ const MAX_ENTRIES: usize = 2_000; const MAX_FILE: u64 = 8 << 20; const MAX_EXPANDED: u64 = 64 << 20; +/// What may follow tar's end marker, which is padding to a whole record: 10 KiB +/// by default, and a mebibyte covers any blocking factor in use. Unbounded, +/// zeros there would inflate at 1 GiB per MiB uploaded, 1.4 s of CPU each. +const MAX_PADDING: u64 = 1 << 20; + /// Installs a theme from its published `theme.tar.gz`, under the name its own /// manifest carries. /// @@ -335,6 +340,14 @@ fn unpack(archive: R, into: &Path) -> Result<()> { refuse!("主题包里的路径越出了主题目录"); } } + // Read to the end, where gzip keeps its checksum. The entries stop at tar's + // end marker, short of it, so otherwise an archive whose bytes changed in + // transit would install as long as its headers survived. + let mut rest = archive.into_inner(); + std::io::copy(&mut (&mut rest).take(MAX_PADDING), &mut std::io::sink()).map_err(archive_error)?; + if rest.read(&mut [0]).map_err(archive_error)? != 0 { + refuse!("主题包在 tar 结尾之后还有超过 1 MiB 的数据,包本身有问题,请联系主题作者"); + } Ok(()) } @@ -534,6 +547,28 @@ mod tests { panic!("half an archive installed") }; assert_eq!(e.downcast_ref::().map(|s| s.0.as_str()), Some(DAMAGED), "{e:#}"); + // Cut past tar's end marker, or with one byte changed on the way: every + // header reads, so only the gzip checksum can refuse either. + let mut altered = whole.clone(); + let crc = altered.len() - 8; + altered[crc] ^= 1; + for damaged in [&whole[..whole.len() - 4], &altered[..]] { + let Err(e) = install(&base, damaged, None) else { panic!("a damaged archive installed") }; + assert_eq!(e.downcast_ref::().map(|s| s.0.as_str()), Some(DAMAGED), "{e:#}"); + } + // Past the end marker, padding and nothing more. + let mut padded = tar::Builder::new(Vec::new()); + let mut header = tar::Header::new_gnu(); + header.set_size(2); + header.set_mode(0o644); + padded.append_data(&mut header, "theme.json", &b"{}"[..]).unwrap(); + let mut gz = flate2::write::GzEncoder::new(Vec::new(), flate2::Compression::fast()); + std::io::Write::write_all(&mut gz, &padded.into_inner().unwrap()).unwrap(); + std::io::Write::write_all(&mut gz, &vec![0; (MAX_PADDING + 1) as usize]).unwrap(); + let Err(e) = install(&base, &gz.finish().unwrap()[..], None) else { + panic!("an oversized tail installed") + }; + assert!(e.to_string().contains("tar 结尾之后"), "{e:#}"); // None of that affected the theme being served or left a staging // directory behind.