From mboxrd@z Thu Jan 1 00:00:00 1970 Return-Path: Received: from firstgate.proxmox.com (firstgate.proxmox.com [212.224.123.68]) by lore.proxmox.com (Postfix) with ESMTPS id 4FAE31FF136 for ; Mon, 23 Mar 2026 16:31:55 +0100 (CET) Received: from firstgate.proxmox.com (localhost [127.0.0.1]) by firstgate.proxmox.com (Proxmox) with ESMTP id E9C1130CF0; Mon, 23 Mar 2026 16:32:13 +0100 (CET) From: Manuel Federanko To: pbs-devel@lists.proxmox.com Subject: [PATCH proxmox-backup] fix #5247: relative paths in exclude patterns. Date: Mon, 23 Mar 2026 16:32:02 +0100 Message-ID: <20260323153202.76874-1-m.federanko@proxmox.com> X-Mailer: git-send-email 2.47.3 MIME-Version: 1.0 Content-Transfer-Encoding: 8bit X-SPAM-LEVEL: Spam detection results: 0 AWL -0.126 Adjusted score from AWL reputation of From: address BAYES_00 -1.9 Bayes spam probability is 0 to 1% DMARC_MISSING 0.1 Missing DMARC policy HEADER_FROM_DIFFERENT_DOMAINS 0.25 From and EnvelopeFrom 2nd level mail domains are different KAM_DMARC_STATUS 0.01 Test Rule for DKIM or SPF Failure with Strict Alignment KAM_LAZY_DOMAIN_SECURITY 1 Sending domain does not have any anti-forgery methods RDNS_NONE 0.793 Delivered to internal network by a host with no rDNS SPF_HELO_NONE 0.001 SPF: HELO does not publish an SPF Record SPF_NONE 0.001 SPF: sender does not publish an SPF Record Message-ID-Hash: 5PYKXAXMHEG26HMSU4J2R3OLMPEGOCTH X-Message-ID-Hash: 5PYKXAXMHEG26HMSU4J2R3OLMPEGOCTH X-MailFrom: mfederanko@dev.localdomain X-Mailman-Rule-Misses: dmarc-mitigation; no-senders; approved; loop; banned-address; emergency; member-moderation; nonmember-moderation; administrivia; implicit-dest; max-recipients; max-size; news-moderation; no-subject; digests; suspicious-header X-Mailman-Version: 3.3.10 Precedence: list List-Id: Proxmox Backup Server development discussion List-Help: List-Owner: List-Post: List-Subscribe: List-Unsubscribe: Patterns which start with ./ or for that matter contain /../ or similar constructs will never match, since they get compared to sanitized paths from directory traversal, sanitize those patterns too. Tested by creating a .pxarexclude file and via the --exclude command line option. I'm unsure if we should accept those silently, currently I added a warning for patterns supplied via the cli option, since that feels most susceptible to wrong usage. It might also make sense to base the patterns for the cli arg on the cwd, but that would be a breaking change. Signed-off-by: Manuel Federanko Fixes: https://bugzilla.proxmox.com/show_bug.cgi?id=5247 --- pbs-client/src/pxar/create.rs | 7 ++++++- pbs-client/src/pxar/tools.rs | 32 +++++++++++++++++++++++++++++++ proxmox-backup-client/src/main.rs | 16 ++++++++++++++-- 3 files changed, 52 insertions(+), 3 deletions(-) diff --git a/pbs-client/src/pxar/create.rs b/pbs-client/src/pxar/create.rs index e42bcc87..1c878cb7 100644 --- a/pbs-client/src/pxar/create.rs +++ b/pbs-client/src/pxar/create.rs @@ -560,7 +560,12 @@ impl Archiver { (line, MatchType::Exclude, false) }; - match MatchEntry::parse_pattern(line, PatternFlag::PATH_NAME, mode) { + let line_normalized = crate::pxar::tools::normalize_lexically(OsStr::from_bytes(line)); + match MatchEntry::parse_pattern( + line_normalized.as_path().as_os_str().as_bytes(), + PatternFlag::PATH_NAME, + mode, + ) { Ok(pattern) => { if anchored { self.patterns.push(pattern.add_flags(MatchFlag::ANCHORED)); diff --git a/pbs-client/src/pxar/tools.rs b/pbs-client/src/pxar/tools.rs index 475c08ad..71a38b29 100644 --- a/pbs-client/src/pxar/tools.rs +++ b/pbs-client/src/pxar/tools.rs @@ -76,6 +76,38 @@ fn assert_single_path_component_do(path: &Path) -> Result<(), Error> { Ok(()) } +pub fn normalize_lexically + ?Sized>(path: &S) -> PathBuf { + normalize_lexically_do(Path::new(path)) +} +fn normalize_lexically_do(path: &Path) -> PathBuf { + // Once std::path::normalize_lexically is stabilized we can switch + // to that + use std::path::Component; + let has_trailing_slash = path + .as_os_str() + .as_encoded_bytes() + .last() + .copied() + .is_some_and(|c: u8| c == std::path::MAIN_SEPARATOR_STR.bytes().last().unwrap()); + let mut new = PathBuf::new(); + let iter = path.components(); + for component in iter { + match component { + Component::RootDir => new.push(Component::RootDir), + Component::Prefix(p) => new.push(Component::Prefix(p)), + Component::CurDir => continue, + Component::ParentDir => { + new.pop(); + } + Component::Normal(n) => new.push(n), + }; + } + if has_trailing_slash { + new.push(""); + } + new +} + #[rustfmt::skip] fn symbolic_mode(c: u64, special: bool, special_x: u8, special_no_x: u8) -> [u8; 3] { [ diff --git a/proxmox-backup-client/src/main.rs b/proxmox-backup-client/src/main.rs index 2ada87bd..4ed23d6e 100644 --- a/proxmox-backup-client/src/main.rs +++ b/proxmox-backup-client/src/main.rs @@ -826,9 +826,21 @@ async fn create_backup( let entry = entry .as_str() .ok_or_else(|| format_err!("Invalid pattern string slice"))?; + let entry_normalized = pbs_client::pxar::tools::normalize_lexically(entry); + if entry_normalized.as_os_str() != entry { + log::info!( + "Sanitized exclude pattern. Exclude patterns are relative to backup root, \ + not the current working directory and should not contain '.' or '..' as path \ + segments" + ); + } pattern_list.push( - MatchEntry::parse_pattern(entry, PatternFlag::PATH_NAME, MatchType::Exclude) - .map_err(|err| format_err!("invalid exclude pattern entry: {}", err))?, + MatchEntry::parse_pattern( + entry_normalized.as_os_str().as_encoded_bytes(), + PatternFlag::PATH_NAME, + MatchType::Exclude, + ) + .map_err(|err| format_err!("invalid exclude pattern entry: {}", err))?, ); } -- 2.47.3