Resolves pre-existing lint issues in imgsecret.rs, time.rs, totp.rs, and crypto.rs that blocked the cargo clippy --workspace -D warnings gate. No logic changes: loop-index → iterator, manual div_ceil → .div_ceil(), manual range contains → .contains(), auto-deref cleanup. Also fixes pre-existing warnings in relicario-cli (main.rs, session.rs, device.rs, gitea.rs, helpers.rs, test helpers): dead_code suppression, too_many_arguments, literal_with_empty_format_string, manual_char_cmp, map_or → is_none_or, and repeat().take() → vec! in test helpers. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
188 lines
5.8 KiB
Rust
188 lines
5.8 KiB
Rust
use std::path::PathBuf;
|
|
use tar::{Builder, Header, EntryType};
|
|
use relicario_core::safe_unpack_git_archive;
|
|
|
|
/// Craft a raw POSIX ustar tar with a single entry using the given raw path bytes.
|
|
/// The tar crate's `Builder` sanitises paths, so we write the 512-byte header
|
|
/// manually to produce truly malicious archives.
|
|
fn raw_tar_with_path(raw_path: &[u8], content: &[u8]) -> Vec<u8> {
|
|
let mut buf = vec![0u8; 512]; // one header block
|
|
|
|
// Bytes 0-99: name field (null-padded)
|
|
let name_len = raw_path.len().min(100);
|
|
buf[..name_len].copy_from_slice(&raw_path[..name_len]);
|
|
|
|
// Bytes 100-107: mode = "0000644\0"
|
|
buf[100..108].copy_from_slice(b"0000644\0");
|
|
|
|
// Bytes 108-115: uid
|
|
buf[108..116].copy_from_slice(b"0000000\0");
|
|
|
|
// Bytes 116-123: gid
|
|
buf[116..124].copy_from_slice(b"0000000\0");
|
|
|
|
// Bytes 124-135: size (octal, 11 digits + null)
|
|
let size_str = format!("{:011o}\0", content.len());
|
|
buf[124..136].copy_from_slice(size_str.as_bytes());
|
|
|
|
// Bytes 136-147: mtime
|
|
buf[136..148].copy_from_slice(b"00000000000\0");
|
|
|
|
// Bytes 148-155: checksum placeholder (spaces during compute)
|
|
buf[148..156].copy_from_slice(b" ");
|
|
|
|
// Byte 156: typeflag = '0' (regular file)
|
|
buf[156] = b'0';
|
|
|
|
// Bytes 257-262: magic "ustar\0"
|
|
buf[257..263].copy_from_slice(b"ustar\0");
|
|
// Bytes 263-264: version "00"
|
|
buf[263..265].copy_from_slice(b"00");
|
|
|
|
// Compute checksum (sum of all bytes, checksum field treated as spaces).
|
|
let checksum: u32 = buf.iter().map(|&b| b as u32).sum();
|
|
let cksum_str = format!("{:06o}\0 ", checksum);
|
|
buf[148..156].copy_from_slice(cksum_str.as_bytes());
|
|
|
|
// Append padded content blocks.
|
|
let mut out = buf;
|
|
if !content.is_empty() {
|
|
out.extend_from_slice(content);
|
|
// Pad to 512-byte boundary.
|
|
let remainder = content.len() % 512;
|
|
if remainder != 0 {
|
|
out.extend(vec![0u8; 512 - remainder]);
|
|
}
|
|
}
|
|
|
|
// Two zero blocks = end-of-archive.
|
|
out.extend(vec![0u8; 1024]);
|
|
out
|
|
}
|
|
|
|
/// Build a tar with a raw symlink entry (typeflag = '2').
|
|
fn raw_symlink_tar() -> Vec<u8> {
|
|
let mut buf = vec![0u8; 512];
|
|
|
|
// name
|
|
buf[..9].copy_from_slice(b"evil_link");
|
|
// mode
|
|
buf[100..108].copy_from_slice(b"0000755\0");
|
|
// uid/gid
|
|
buf[108..116].copy_from_slice(b"0000000\0");
|
|
buf[116..124].copy_from_slice(b"0000000\0");
|
|
// size = 0
|
|
buf[124..136].copy_from_slice(b"00000000000\0");
|
|
// mtime
|
|
buf[136..148].copy_from_slice(b"00000000000\0");
|
|
// checksum placeholder
|
|
buf[148..156].copy_from_slice(b" ");
|
|
// typeflag = '2' (symlink)
|
|
buf[156] = b'2';
|
|
// linkname
|
|
let target = b"/etc/passwd";
|
|
buf[157..157 + target.len()].copy_from_slice(target);
|
|
// magic
|
|
buf[257..263].copy_from_slice(b"ustar\0");
|
|
buf[263..265].copy_from_slice(b"00");
|
|
|
|
// Compute checksum.
|
|
let checksum: u32 = buf.iter().map(|&b| b as u32).sum();
|
|
let cksum_str = format!("{:06o}\0 ", checksum);
|
|
buf[148..156].copy_from_slice(cksum_str.as_bytes());
|
|
|
|
let mut out = buf;
|
|
out.extend(vec![0u8; 1024]); // end-of-archive
|
|
out
|
|
}
|
|
|
|
fn build_normal_tar() -> Vec<u8> {
|
|
let mut buf = Vec::new();
|
|
{
|
|
let mut builder = Builder::new(&mut buf);
|
|
let content = b"hello";
|
|
let mut header = Header::new_gnu();
|
|
header.set_entry_type(EntryType::Regular);
|
|
header.set_size(content.len() as u64);
|
|
header.set_cksum();
|
|
builder
|
|
.append_data(&mut header, "subdir/hello.txt", content.as_ref())
|
|
.unwrap();
|
|
builder.finish().unwrap();
|
|
}
|
|
buf
|
|
}
|
|
|
|
fn build_oversize_tar() -> Vec<u8> {
|
|
// Actual 2048-byte body; test will use cap=1024
|
|
let mut buf = Vec::new();
|
|
{
|
|
let mut builder = Builder::new(&mut buf);
|
|
let content = vec![0u8; 2048];
|
|
let mut header = Header::new_gnu();
|
|
header.set_entry_type(EntryType::Regular);
|
|
header.set_size(content.len() as u64);
|
|
header.set_cksum();
|
|
builder
|
|
.append_data(&mut header, "bigfile.bin", content.as_slice())
|
|
.unwrap();
|
|
builder.finish().unwrap();
|
|
}
|
|
buf
|
|
}
|
|
|
|
#[test]
|
|
fn restore_rejects_path_traversal() {
|
|
// Craft a tar with "../../escaped.txt" using raw bytes (Builder sanitises paths).
|
|
let bytes = raw_tar_with_path(b"../../escaped.txt", b"evil content");
|
|
let err = safe_unpack_git_archive(&bytes, 1024 * 1024).unwrap_err();
|
|
let msg = format!("{err:#}");
|
|
assert!(
|
|
msg.contains("path traversal") || msg.contains(".."),
|
|
"got: {msg}"
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn restore_rejects_absolute_path() {
|
|
// Craft a tar with "/etc/escaped.txt" using raw bytes.
|
|
let bytes = raw_tar_with_path(b"/etc/escaped.txt", b"evil content");
|
|
let err = safe_unpack_git_archive(&bytes, 1024 * 1024).unwrap_err();
|
|
let msg = format!("{err:#}");
|
|
assert!(
|
|
msg.contains("path traversal") || msg.contains("absolute"),
|
|
"got: {msg}"
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn restore_rejects_symlink() {
|
|
let bytes = raw_symlink_tar();
|
|
let err = safe_unpack_git_archive(&bytes, 1024 * 1024).unwrap_err();
|
|
let msg = format!("{err:#}");
|
|
assert!(
|
|
msg.contains("symlink") || msg.contains("link"),
|
|
"got: {msg}"
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn restore_rejects_size_bomb() {
|
|
let bytes = build_oversize_tar(); // actual 2048-byte entry
|
|
let err = safe_unpack_git_archive(&bytes, 1024).unwrap_err(); // cap = 1024 bytes
|
|
let msg = format!("{err:#}");
|
|
assert!(
|
|
msg.contains("size") || msg.contains("cap") || msg.contains("too large"),
|
|
"got: {msg}"
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn restore_accepts_normal_files() {
|
|
let buf = build_normal_tar();
|
|
let entries = safe_unpack_git_archive(&buf, 1024 * 1024).expect("happy path");
|
|
assert_eq!(entries.len(), 1);
|
|
assert_eq!(entries[0].0, PathBuf::from("subdir/hello.txt"));
|
|
assert_eq!(entries[0].1, b"hello");
|
|
}
|