* [PATCH proxmox-backup v2] fix #5247: relative paths in exclude patterns.
@ 2026-03-30 8:11 Manuel Federanko
2026-03-31 14:04 ` Wolfgang Bumiller
0 siblings, 1 reply; 3+ messages in thread
From: Manuel Federanko @ 2026-03-30 8:11 UTC (permalink / raw)
To: pbs-devel
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 <m.federanko@proxmox.com>
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<S: AsRef<OsStr> + ?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
^ permalink raw reply [flat|nested] 3+ messages in thread* Re: [PATCH proxmox-backup v2] fix #5247: relative paths in exclude patterns.
2026-03-30 8:11 [PATCH proxmox-backup v2] fix #5247: relative paths in exclude patterns Manuel Federanko
@ 2026-03-31 14:04 ` Wolfgang Bumiller
2026-03-31 14:48 ` Manuel Federanko
0 siblings, 1 reply; 3+ messages in thread
From: Wolfgang Bumiller @ 2026-03-31 14:04 UTC (permalink / raw)
To: Manuel Federanko; +Cc: pbs-devel
On Mon, 30 Mar 2026 10:11:10 +0200, Manuel Federanko <m.federanko@proxmox.com> wrote:
> 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.
In the `.pxarexclude` case, logging may make sense as well IMO.
As for `--exclude`, I wonder we should just *bail* instead?
Although that might "break" some automated
otherwise-working-but-probably-too-big backups, so we can't... (although
for `foo/../bar` it's *really* tempting...)
>
>
> diff --git a/pbs-client/src/pxar/tools.rs b/pbs-client/src/pxar/tools.rs
> index 475c08ad3..7ad4009fe 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<S: AsRef<OsStr> + ?Sized>(path: &S) -> PathBuf {
(Note: the monomorphization from v1 was actually fine, but if the `_do`
variant is only used by this one function it could live *inside* here.)
> + // 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());
Could just use
.as_encoded_bytes()
.ends_with(MAIN_SEPARATOR_STR.as_bytes())
--
^ permalink raw reply [flat|nested] 3+ messages in thread* Re: [PATCH proxmox-backup v2] fix #5247: relative paths in exclude patterns.
2026-03-31 14:04 ` Wolfgang Bumiller
@ 2026-03-31 14:48 ` Manuel Federanko
0 siblings, 0 replies; 3+ messages in thread
From: Manuel Federanko @ 2026-03-31 14:48 UTC (permalink / raw)
To: Wolfgang Bumiller; +Cc: pbs-devel
On 2026-03-31 4:04 PM, Wolfgang Bumiller wrote:
> On Mon, 30 Mar 2026 10:11:10 +0200, Manuel Federanko <m.federanko@proxmox.com> wrote:
>> 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.
>
> In the `.pxarexclude` case, logging may make sense as well IMO.
ack
> As for `--exclude`, I wonder we should just *bail* instead?
>
> Although that might "break" some automated
> otherwise-working-but-probably-too-big backups, so we can't... (although
> for `foo/../bar` it's *really* tempting...)
I'm a bit undecided, maybe log a warning with a deprecation notice that
this will lead to an error in the future? Same goes for the cli option
now that I'm thinking about it.
>>
>>
>> diff --git a/pbs-client/src/pxar/tools.rs b/pbs-client/src/pxar/tools.rs
>> index 475c08ad3..7ad4009fe 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<S: AsRef<OsStr> + ?Sized>(path: &S) -> PathBuf {
>
> (Note: the monomorphization from v1 was actually fine, but if the `_do`
> variant is only used by this one function it could live *inside* here.)
>
>> + // 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());
>
> Could just use
>
> .as_encoded_bytes()
> .ends_with(MAIN_SEPARATOR_STR.as_bytes())
ack, will change in v3 once warning/bailing has been decided
^ permalink raw reply [flat|nested] 3+ messages in thread
end of thread, other threads:[~2026-03-31 14:47 UTC | newest]
Thread overview: 3+ messages (download: mbox.gz / follow: Atom feed)
-- links below jump to the message on this page --
2026-03-30 8:11 [PATCH proxmox-backup v2] fix #5247: relative paths in exclude patterns Manuel Federanko
2026-03-31 14:04 ` Wolfgang Bumiller
2026-03-31 14:48 ` Manuel Federanko
This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox