diff --git a/cargo-audit/src/auditor.rs b/cargo-audit/src/auditor.rs index 4e07015ef..6ac772fa1 100644 --- a/cargo-audit/src/auditor.rs +++ b/cargo-audit/src/auditor.rs @@ -17,6 +17,16 @@ use std::{ // TODO: make configurable const DEFAULT_LOCK_TIMEOUT: Duration = Duration::from_secs(5 * 60); +#[cfg(feature = "binary-scanning")] +const DEFAULT_MAX_BINARY_SIZE: u64 = 100 * 1024 * 1024; // 100MB + +#[cfg(feature = "binary-scanning")] +#[derive(Clone, Copy, Debug)] +enum BinarySizeLimit { + Unlimited, + Max(u64), +} + /// Security vulnerability auditor pub struct Auditor { /// RustSec Advisory Database @@ -30,6 +40,14 @@ pub struct Auditor { /// Audit report settings report_settings: report::Settings, + + /// Binary scanning configuration (max input size) + #[cfg(feature = "binary-scanning")] + binary_size_limit: BinarySizeLimit, + + /// Binary scanning configuration (max auditable payload size) + #[cfg(feature = "binary-scanning")] + audit_data_size_limit: Option, } impl Auditor { @@ -179,6 +197,10 @@ impl Auditor { registry_index, presenter: Presenter::new(&config.output), report_settings: config.report_settings(), + #[cfg(feature = "binary-scanning")] + binary_size_limit: BinarySizeLimit::Max(DEFAULT_MAX_BINARY_SIZE), + #[cfg(feature = "binary-scanning")] + audit_data_size_limit: None, } } @@ -241,13 +263,36 @@ impl Auditor { summary } + #[cfg(feature = "binary-scanning")] + /// Configure binary-scanning limits for this `Auditor`. + /// + /// `max_binary_size` is in bytes. If unset, defaults to 100MB. If set to `0`, disables the + /// limit. + /// + /// `audit_data_size_limit` is the maximum size (in bytes) of embedded auditable payload data + /// to parse. If unset, the default from `rustsec` applies (currently 8MB). + pub fn set_binary_scan_limits( + &mut self, + max_binary_size: Option, + audit_data_size_limit: Option, + ) { + self.binary_size_limit = match max_binary_size { + Some(0) => BinarySizeLimit::Unlimited, + Some(n) => BinarySizeLimit::Max(n), + None => BinarySizeLimit::Max(DEFAULT_MAX_BINARY_SIZE), + }; + self.audit_data_size_limit = audit_data_size_limit; + } + #[cfg(feature = "binary-scanning")] /// Perform an audit of a binary file with dependency data embedded by `cargo auditable` fn audit_binary(&mut self, binary_path: &Path) -> rustsec::Result { use rustsec::binary_scanning::BinaryReport::*; - let file_contents = std::fs::read(binary_path)?; - let (binary_type, report) = - rustsec::binary_scanning::load_deps_from_binary(&file_contents, Option::None)?; + let file_contents = self.read_binary_with_limit(binary_path)?; + let (binary_type, report) = rustsec::binary_scanning::load_deps_from_binary( + &file_contents, + self.audit_data_size_limit, + )?; self.presenter.binary_scan_report(&report, binary_path); match report { Complete(lockfile) | Incomplete(lockfile) => { @@ -260,6 +305,34 @@ impl Auditor { } } + #[cfg(feature = "binary-scanning")] + fn read_binary_with_limit(&self, binary_path: &Path) -> rustsec::Result> { + let mut file = std::fs::File::open(binary_path)?; + match self.binary_size_limit { + BinarySizeLimit::Unlimited => { + let mut buffer = Vec::new(); + file.read_to_end(&mut buffer)?; + Ok(buffer) + } + BinarySizeLimit::Max(limit) => { + let mut limited = file.take(limit.saturating_add(1)); + let mut buffer = Vec::new(); + limited.read_to_end(&mut buffer)?; + if buffer.len() as u64 > limit { + return Err(Error::new( + ErrorKind::BadParam, + format!( + "binary {} exceeds max size limit of {} bytes", + binary_path.display(), + limit + ), + )); + } + Ok(buffer) + } + } + } + /// The part of the auditing process that is shared between auditing lockfiles and binary files fn audit( &mut self, diff --git a/cargo-audit/src/commands/audit/binary_scanning.rs b/cargo-audit/src/commands/audit/binary_scanning.rs index 9f791f996..5d91456c9 100644 --- a/cargo-audit/src/commands/audit/binary_scanning.rs +++ b/cargo-audit/src/commands/audit/binary_scanning.rs @@ -9,6 +9,22 @@ use std::{path::PathBuf, process::exit}; #[derive(Command, Clone, Default, Debug, Parser)] #[command()] pub struct BinCommand { + /// Maximum binary size in bytes to read + #[arg( + long = "max-binary-size", + value_name = "BYTES", + help = "Maximum binary size in bytes to read (default: 100MB; use 0 for unlimited)" + )] + max_binary_size: Option, + + /// Maximum audit data size in bytes to parse + #[arg( + long = "audit-data-size-limit", + value_name = "BYTES", + help = "Maximum audit data size in bytes to parse (default: 8MB)" + )] + audit_data_size_limit: Option, + /// Paths to the binaries to be scanned #[arg( value_parser, @@ -20,7 +36,9 @@ pub struct BinCommand { impl Runnable for BinCommand { fn run(&self) { - let report = self.auditor().audit_binaries(&self.binary_paths); + let mut auditor = self.auditor(); + auditor.set_binary_scan_limits(self.max_binary_size, self.audit_data_size_limit); + let report = auditor.audit_binaries(&self.binary_paths); if report.vulnerabilities_found { exit(1) } else if report.errors_encountered { diff --git a/cargo-audit/tests/binary_scanning.rs b/cargo-audit/tests/binary_scanning.rs index 1d820e0d8..7a4806cd5 100644 --- a/cargo-audit/tests/binary_scanning.rs +++ b/cargo-audit/tests/binary_scanning.rs @@ -4,6 +4,7 @@ use std::path::PathBuf; use abscissa_core::testing::prelude::*; use once_cell::sync::Lazy; +use std::{fs, io::Read}; use tempfile::TempDir; /// Directory containing the advisory database. @@ -39,6 +40,31 @@ fn cmd_runner() -> CmdRunner { RUNNER.clone() } +#[test] +fn oversized_binary_is_rejected() { + let tmpdir = TempDir::new().unwrap(); + let binary_path = tmpdir.path().join("not-a-real-binary"); + + // Ensure the file is larger than the configured max size. + fs::write(&binary_path, b"0123").unwrap(); + + let mut runner = cmd_runner(); + runner + .arg(&binary_path) + .arg("--max-binary-size") + .arg("1") + .capture_stderr(); + + let mut process = runner.run(); + let mut stderr = String::new(); + process.stderr().read_to_string(&mut stderr).unwrap(); + process.wait().unwrap().expect_code(2); + assert!( + stderr.contains("exceeds max size limit of 1 bytes"), + "{stderr}" + ); +} + #[test] fn panicking_binary_without_vulnerabilities_passes() { let mut binary_path = binaries_dir();