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 B0D851FF146 for ; Tue, 28 Apr 2026 10:07:10 +0200 (CEST) Received: from firstgate.proxmox.com (localhost [127.0.0.1]) by firstgate.proxmox.com (Proxmox) with ESMTP id 8F30397FC; Tue, 28 Apr 2026 10:07:10 +0200 (CEST) From: Manuel Federanko To: pbs-devel@lists.proxmox.com Subject: [PATCH proxmox-backup v3] fix #5247: relative paths in exclude patterns. Date: Tue, 28 Apr 2026 10:06:34 +0200 Message-ID: <20260428080634.79054-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.281 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 URIBL_BLOCKED 0.001 ADMINISTRATOR NOTICE: The query to URIBL was blocked. See http://wiki.apache.org/spamassassin/DnsBlocklists#dnsbl-block for more information. [proxmox.com,tools.rs,main.rs,create.rs] Message-ID-Hash: I7DEAJHQ5X75M7FKUVOZEXWLN5HS2GBM X-Message-ID-Hash: I7DEAJHQ5X75M7FKUVOZEXWLN5HS2GBM 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. Signed-off-by: Manuel Federanko Fixes: https://bugzilla.proxmox.com/show_bug.cgi?id=5247 --- changed since v2: - log a warning for the cli option and the exclude files - simplify trailing slash detection changed since v1: - inline normalize_lexically helper - bump log level from info to warn pbs-client/src/pxar/create.rs | 14 +++++++++++++- pbs-client/src/pxar/tools.rs | 29 +++++++++++++++++++++++++++++ proxmox-backup-client/src/main.rs | 16 ++++++++++++++-- 3 files changed, 56 insertions(+), 3 deletions(-) diff --git a/pbs-client/src/pxar/create.rs b/pbs-client/src/pxar/create.rs index 70b858092..b8fc94ab6 100644 --- a/pbs-client/src/pxar/create.rs +++ b/pbs-client/src/pxar/create.rs @@ -589,7 +589,19 @@ impl Archiver { (line, MatchType::Exclude, false) }; - match MatchEntry::parse_pattern(line, PatternFlag::PATH_NAME, mode) { + let line = OsStr::from_bytes(line); + let line_normalized = crate::pxar::tools::normalize_lexically(line); + if line_normalized.as_os_str() != line { + warn!( + "Sanitized exclude pattern. Exclude patterns are relative to the current backup root, \ + not the current working directory and should not contain '.' or '..' as path segments" + ); + } + 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 475c08ad3..28f66801b 100644 --- a/pbs-client/src/pxar/tools.rs +++ b/pbs-client/src/pxar/tools.rs @@ -76,6 +76,35 @@ 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() + .ends_with(std::path::MAIN_SEPARATOR_STR.as_bytes()); + 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 a1533b79b..fd8d6a61a 100644 --- a/proxmox-backup-client/src/main.rs +++ b/proxmox-backup-client/src/main.rs @@ -842,9 +842,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