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 8C2EB1FF141 for ; Mon, 30 Mar 2026 10:11:35 +0200 (CEST) Received: from firstgate.proxmox.com (localhost [127.0.0.1]) by firstgate.proxmox.com (Proxmox) with ESMTP id 9FE2219C8C; Mon, 30 Mar 2026 10:12:01 +0200 (CEST) From: Manuel Federanko To: pbs-devel@lists.proxmox.com Subject: [PATCH proxmox-backup v2] fix #5247: relative paths in exclude patterns. Date: Mon, 30 Mar 2026 10:11:10 +0200 Message-ID: <20260330081110.8527-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: 1 AWL -1.658 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.248 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 RCVD_IN_VALIDITY_CERTIFIED_BLOCKED 1 ADMINISTRATOR NOTICE: The query to Validity was blocked. See https://knowledge.validity.com/hc/en-us/articles/20961730681243 for more information. RCVD_IN_VALIDITY_RPBL_BLOCKED 1 ADMINISTRATOR NOTICE: The query to Validity was blocked. See https://knowledge.validity.com/hc/en-us/articles/20961730681243 for more information. RCVD_IN_VALIDITY_SAFE_BLOCKED 1 ADMINISTRATOR NOTICE: The query to Validity was blocked. See https://knowledge.validity.com/hc/en-us/articles/20961730681243 for more information. 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: W7MCSZMF7XUHM2CWWBGL7NIVU64IOZN6 X-Message-ID-Hash: W7MCSZMF7XUHM2CWWBGL7NIVU64IOZN6 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. Implement this for both .pxarexclude files and the --exclude command line option. Log a warning if a path was sanitized that was provided via --exclude. Signed-off-by: Manuel Federanko Fixes: https://bugzilla.proxmox.com/show_bug.cgi?id=5247 --- changes since v1: - inline normalize_lexically helper - bump log level from info to warn pbs-client/src/pxar/create.rs | 7 ++++++- pbs-client/src/pxar/tools.rs | 31 +++++++++++++++++++++++++++++++ proxmox-backup-client/src/main.rs | 16 ++++++++++++++-- 3 files changed, 51 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..7ad4009f 100644 --- a/pbs-client/src/pxar/tools.rs +++ b/pbs-client/src/pxar/tools.rs @@ -76,6 +76,37 @@ fn assert_single_path_component_do(path: &Path) -> Result<(), Error> { Ok(()) } +pub fn normalize_lexically + ?Sized>(path: &S) -> PathBuf { + // FIXME: Once std::path::normalize_lexically is stabilized we can + // switch to that + use std::path::Component; + + let path = Path::new(path); + 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..303b6ecd 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::warn!( + "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