Skip to content

Commit 8d4bf40

Browse files
committed
fix: prevent very long path from using unbounded time in realpath().
It's possible to inject such paths using urls which can then end up being canonicalized, causing very long runtimes with excessively long paths due to `is_symlink` calls which will be slow. Now the amount of components is limited to 4096/2, which should be a worst-case path at the border of realistic. If this limitation becomes too arbitrary, one could consider making this cut-off value configurable.
1 parent 48e8932 commit 8d4bf40

File tree

3 files changed

+36
-2
lines changed

3 files changed

+36
-2
lines changed

gix-path/src/realpath.rs

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,8 @@
44
pub enum Error {
55
#[error("The maximum allowed number {} of symlinks in path is exceeded", .max_symlinks)]
66
MaxSymlinksExceeded { max_symlinks: u8 },
7+
#[error("Cannot resolve symlinks in path with more than {max_symlink_checks} components (takes too long)")]
8+
ExcessiveComponentCount { max_symlink_checks: usize },
79
#[error(transparent)]
810
ReadLink(std::io::Error),
911
#[error(transparent)]
@@ -57,6 +59,8 @@ pub(crate) mod function {
5759
let mut num_symlinks = 0;
5860
let mut path_backing: PathBuf;
5961
let mut components = path.components();
62+
const MAX_SYMLINK_CHECKS: usize = 2048;
63+
let mut symlink_checks = 0;
6064
while let Some(component) = components.next() {
6165
match component {
6266
part @ (RootDir | Prefix(_)) => real_path.push(part),
@@ -68,6 +72,7 @@ pub(crate) mod function {
6872
}
6973
Normal(part) => {
7074
real_path.push(part);
75+
symlink_checks += 1;
7176
if real_path.is_symlink() {
7277
num_symlinks += 1;
7378
if num_symlinks > max_symlinks {
@@ -83,6 +88,11 @@ pub(crate) mod function {
8388
path_backing = link_destination;
8489
components = path_backing.components();
8590
}
91+
if symlink_checks > MAX_SYMLINK_CHECKS {
92+
return Err(Error::ExcessiveComponentCount {
93+
max_symlink_checks: MAX_SYMLINK_CHECKS,
94+
});
95+
}
8696
}
8797
}
8898
}

gix-path/tests/fixtures/fuzzed/54k-path-components.path

Lines changed: 1 addition & 0 deletions
Large diffs are not rendered by default.

gix-path/tests/realpath/mod.rs

Lines changed: 25 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,31 @@
1-
use std::path::Path;
1+
use std::path::{Path, PathBuf};
2+
use std::time::Duration;
23

3-
use gix_path::{realpath::Error, realpath_opts};
4+
use bstr::ByteVec;
45
use tempfile::tempdir;
56

7+
use gix_path::{realpath::Error, realpath_opts};
8+
9+
#[test]
10+
fn fuzzed_timeout() -> crate::Result {
11+
let path = PathBuf::from(std::fs::read("tests/fixtures/fuzzed/54k-path-components.path")?.into_string()?);
12+
assert_eq!(path.components().count(), 54862);
13+
let start = std::time::Instant::now();
14+
assert!(matches!(
15+
gix_path::realpath_opts(&path, Path::new("/cwd"), gix_path::realpath::MAX_SYMLINKS).unwrap_err(),
16+
gix_path::realpath::Error::ExcessiveComponentCount {
17+
max_symlink_checks: 2048
18+
}
19+
));
20+
assert!(
21+
start.elapsed() < Duration::from_millis(500),
22+
"took too long: {:.02} , we can't take too much time for this, and should keep the amount of work reasonable\
23+
as paths can be part of URls which sometimes are canonicalized",
24+
start.elapsed().as_secs_f32()
25+
);
26+
Ok(())
27+
}
28+
629
#[test]
730
fn assorted() -> crate::Result {
831
let cwd = tempdir()?;

0 commit comments

Comments
 (0)