Skip to content

Commit

Permalink
add MultiComponentPath*
Browse files Browse the repository at this point in the history
  • Loading branch information
TheAlgorythm committed Dec 6, 2023
1 parent 20bface commit 1357d93
Show file tree
Hide file tree
Showing 2 changed files with 192 additions and 6 deletions.
177 changes: 171 additions & 6 deletions src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,9 @@ use std::path::{Path, PathBuf};
/// A safe wrapper for a `PathBuf` with only a single component.
/// This prevents path traversal attacks.
///
/// The owned variant of [`SingleComponentPath`].
/// There is [`MultiComponentPathBuf`] when multiple components should be allowed.
///
/// It allows just a single normal path element and no parent, root directory or prefix like `C:`.
/// Allows reference to the current directory of the path (`.`).
#[derive(Clone, Debug, PartialEq, PartialOrd, Eq, Ord, Hash)]
Expand All @@ -50,9 +53,12 @@ impl SingleComponentPathBuf {
///
/// # #[cfg(unix)]
/// # {
/// let some_valid_folder: SingleComponentPathBuf = SingleComponentPathBuf::new("foo").unwrap();
/// let some_valid_file: SingleComponentPathBuf = SingleComponentPathBuf::new("bar.txt").unwrap();
/// let with_backreference: SingleComponentPathBuf = SingleComponentPathBuf::new("./bar.txt").unwrap();
/// let some_valid_folder = SingleComponentPathBuf::new("foo").unwrap();
/// let some_valid_file = SingleComponentPathBuf::new("bar.txt").unwrap();
/// let with_backreference = SingleComponentPathBuf::new("./bar.txt").unwrap();
/// assert!(SingleComponentPathBuf::new("foo/bar.txt").is_none());
/// assert!(SingleComponentPathBuf::new("..").is_none());
/// assert!(SingleComponentPathBuf::new("/").is_none());
/// assert!(SingleComponentPathBuf::new("/etc/shadow").is_none());
/// # }
/// ```
Expand Down Expand Up @@ -84,6 +90,9 @@ impl AsRef<Path> for SingleComponentPathBuf {
/// A safe wrapper for a `Path` with only a single component.
/// This prevents path traversal attacks.
///
/// The borrowed variant of [`SingleComponentPathBuf`].
/// There is [`MultiComponentPath`] when multiple components should be allowed.
///
/// It allows just a single normal path element and no parent, root directory or prefix like `C:`.
/// Allows reference to the current directory of the path (`.`).
#[derive(Debug, PartialEq, PartialOrd, Eq, Ord, Hash)]
Expand All @@ -100,9 +109,12 @@ impl<'p> SingleComponentPath<'p> {
///
/// # #[cfg(unix)]
/// # {
/// let some_valid_folder: SingleComponentPath = SingleComponentPath::new("foo").unwrap();
/// let some_valid_file: SingleComponentPath = SingleComponentPath::new("bar.txt").unwrap();
/// let with_backreference: SingleComponentPath = SingleComponentPath::new("./bar.txt").unwrap();
/// let some_valid_folder = SingleComponentPath::new("foo").unwrap();
/// let some_valid_file = SingleComponentPath::new("bar.txt").unwrap();
/// let with_backreference = SingleComponentPath::new("./bar.txt").unwrap();
/// assert!(SingleComponentPath::new("foo/bar.txt").is_none());
/// assert!(SingleComponentPath::new("..").is_none());
/// assert!(SingleComponentPath::new("/").is_none());
/// assert!(SingleComponentPath::new("/etc/shadow").is_none());
/// # }
/// ```
Expand Down Expand Up @@ -157,6 +169,138 @@ impl AsRef<Path> for SingleComponentPath<'_> {
}
}

/// A safe wrapper for a `PathBuf`.
/// This prevents path traversal attacks.
///
/// The owned variant of [`MultiComponentPath`].
/// There is [`SingleComponentPathBuf`] when only a single component should be allowed.
///
/// It allows just normal path elements and no parent, root directory or prefix like `C:`.
/// Further allowed are references to the current directory of the path (`.`).
#[derive(Clone, Debug, PartialEq, PartialOrd, Eq, Ord, Hash)]
pub struct MultiComponentPathBuf {
pub(crate) path: PathBuf,
}

impl MultiComponentPathBuf {
/// It creates the wrapped `MultiComponentPathBuf` if it's valid.
/// Otherwise it will return `None`.
///
/// ```
/// use path_ratchet::MultiComponentPathBuf;
///
/// # #[cfg(unix)]
/// # {
/// let some_valid_folder = MultiComponentPathBuf::new("foo").unwrap();
/// let some_valid_file = MultiComponentPathBuf::new("bar.txt").unwrap();
/// let with_backreference = MultiComponentPathBuf::new("./bar.txt").unwrap();
/// let multi = MultiComponentPathBuf::new("foo/bar.txt").unwrap();
/// assert!(MultiComponentPathBuf::new("..").is_none());
/// assert!(MultiComponentPathBuf::new("/").is_none());
/// assert!(MultiComponentPathBuf::new("/etc/shadow").is_none());
/// # }
/// ```
pub fn new<S: Into<PathBuf>>(component: S) -> Option<Self> {
let component = Self {
path: component.into(),
};

MultiComponentPath::from(&component)
.is_valid()
.then_some(component)
}
}

impl std::ops::Deref for MultiComponentPathBuf {
type Target = Path;

fn deref(&self) -> &Self::Target {
&self.path
}
}

impl AsRef<Path> for MultiComponentPathBuf {
fn as_ref(&self) -> &Path {
&self.path
}
}

/// A safe wrapper for a `Path`.
/// This prevents path traversal attacks.
///
/// The borrowed variant of [`MultiComponentPathBuf`].
/// There is [`SingleComponentPath`] when only a single component should be allowed.
///
/// It allows just normal path elements and no parent, root directory or prefix like `C:`.
/// Further allowed are references to the current directory of the path (`.`).
#[derive(Debug, PartialEq, PartialOrd, Eq, Ord, Hash)]
pub struct MultiComponentPath<'p> {
pub(crate) path: &'p Path,
}

impl<'p> MultiComponentPath<'p> {
/// It creates the wrapped `MultiComponentPath` if it's valid.
/// Otherwise it will return `None`.
///
/// ```
/// use path_ratchet::MultiComponentPath;
///
/// # #[cfg(unix)]
/// # {
/// let some_valid_folder = MultiComponentPath::new("foo").unwrap();
/// let some_valid_file = MultiComponentPath::new("bar.txt").unwrap();
/// let with_backreference = MultiComponentPath::new("./bar.txt").unwrap();
/// let multi = MultiComponentPath::new("foo/bar.txt").unwrap();
/// assert!(MultiComponentPath::new("..").is_none());
/// assert!(MultiComponentPath::new("/").is_none());
/// assert!(MultiComponentPath::new("/etc/shadow").is_none());
/// # }
/// ```
pub fn new<P: AsRef<Path> + ?Sized>(component: &'p P) -> Option<Self> {
let component = Self {
path: component.as_ref(),
};

component.is_valid().then_some(component)
}

pub(crate) fn is_valid(&self) -> bool {
use std::path::Component;

self.path
.components()
.all(|component| matches!(component, Component::Normal(_) | Component::CurDir))
}
}

impl<'p> From<&'p MultiComponentPathBuf> for MultiComponentPath<'p> {
fn from(s: &'p MultiComponentPathBuf) -> Self {
Self { path: &s.path }
}
}

impl<'p> From<MultiComponentPath<'p>> for MultiComponentPathBuf {
fn from(s: MultiComponentPath<'p>) -> Self {
Self {
path: s.path.to_path_buf(),
}
}
}

impl std::ops::Deref for MultiComponentPath<'_> {
type Target = Path;

fn deref(&self) -> &Self::Target {
self.path
}
}

impl AsRef<Path> for MultiComponentPath<'_> {
fn as_ref(&self) -> &Path {
self.path
}
}

/// Extension trait for [`PathBuf`] to push only components which don't allow path traversal.
pub trait PushPathComponent {
/// This allows to push just a [`SingleComponentPathBuf`] to a [`std::path::PathBuf`].
Expand All @@ -175,17 +319,38 @@ pub trait PushPathComponent {
/// # }
/// ```
fn push_component<'p>(&mut self, component: impl Into<SingleComponentPath<'p>>);
/// ```
/// use std::path::PathBuf;
/// use path_ratchet::prelude::*;
///
/// # #[cfg(unix)]
/// # {
/// let mut path = PathBuf::new();
/// path.push_components(MultiComponentPath::new("foo/bar.txt").unwrap());
///
/// assert_eq!(path, PathBuf::from("foo/bar.txt"));
/// # }
/// ```
fn push_components<'p>(&mut self, component: impl Into<MultiComponentPath<'p>>);
}

impl PushPathComponent for PathBuf {
fn push_component<'p>(&mut self, component: impl Into<SingleComponentPath<'p>>) {
self.push(component.into());
}

fn push_components<'p>(&mut self, component: impl Into<MultiComponentPath<'p>>) {
self.push(component.into());
}
}

/// All needed defenitions
pub mod prelude {
pub use crate::PushPathComponent;

pub use crate::SingleComponentPath;
pub use crate::SingleComponentPathBuf;

pub use crate::MultiComponentPath;
pub use crate::MultiComponentPathBuf;
}
21 changes: 21 additions & 0 deletions src/tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -9,11 +9,21 @@ fn assert_single_disallow(path: &str) {
assert!(SingleComponentPathBuf::new(path).is_none());
}

fn assert_multi_disallow(path: &str) {
assert!(MultiComponentPathBuf::new(path).is_none());
}

#[test]
fn single_disallow_parent() {
assert_single_disallow("../file");
}

#[test]
fn multi_disallow_parent() {
assert_multi_disallow("../file");
assert_multi_disallow("../folder/file");
}

#[test]
fn single_strip_current_dir() {
let mut path = non_existing_absolute();
Expand All @@ -24,3 +34,14 @@ fn single_strip_current_dir() {

assert_eq!(path, replica_path);
}

#[test]
fn multi_strip_current_dir() {
let mut path = non_existing_absolute();
let mut replica_path = non_existing_absolute();

path.push_components(MultiComponentPath::new("./folder/./file/.").unwrap());
replica_path.push("folder/file");

assert_eq!(path, replica_path);
}

0 comments on commit 1357d93

Please sign in to comment.