Skip to content

Commit 2ea87f0

Browse files
committed
fix!: State::from_tree() now performs name validation.
Previously, malicious trees could be used to create a index with invalid names, which is one step closer to actually abusing it.
1 parent 2683235 commit 2ea87f0

22 files changed

+184
-29
lines changed

Cargo.lock

Lines changed: 3 additions & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

gix-index/Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ gix-features = { version = "^0.38.1", path = "../gix-features", features = [
2727
gix-hash = { version = "^0.14.2", path = "../gix-hash" }
2828
gix-bitmap = { version = "^0.2.11", path = "../gix-bitmap" }
2929
gix-object = { version = "^0.42.1", path = "../gix-object" }
30+
gix-validate = { version = "^0.8.4", path = "../gix-validate" }
3031
gix-traverse = { version = "^0.39.0", path = "../gix-traverse" }
3132
gix-lock = { version = "^13.0.0", path = "../gix-lock" }
3233
gix-fs = { version = "^0.10.2", path = "../gix-fs" }

gix-index/src/init.rs

Lines changed: 75 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,6 @@
1-
mod from_tree {
1+
#[allow(clippy::empty_docs)]
2+
///
3+
pub mod from_tree {
24
use std::collections::VecDeque;
35

46
use bstr::{BStr, BString, ByteSlice, ByteVec};
@@ -10,6 +12,19 @@ mod from_tree {
1012
Entry, PathStorage, State, Version,
1113
};
1214

15+
/// The error returned by [State::from_tree()].
16+
#[derive(Debug, thiserror::Error)]
17+
#[allow(missing_docs)]
18+
pub enum Error {
19+
#[error("The path \"{path}\" is invalid")]
20+
InvalidComponent {
21+
path: BString,
22+
source: gix_validate::path::component::Error,
23+
},
24+
#[error(transparent)]
25+
Traversal(#[from] gix_traverse::tree::breadthfirst::Error),
26+
}
27+
1328
/// Initialization
1429
impl State {
1530
/// Return a new and empty in-memory index assuming the given `object_hash`.
@@ -32,23 +47,42 @@ mod from_tree {
3247
}
3348
/// Create an index [`State`] by traversing `tree` recursively, accessing sub-trees
3449
/// with `objects`.
50+
/// `validate` is used to determine which validations to perform on every path component we see.
3551
///
3652
/// **No extension data is currently produced**.
37-
pub fn from_tree<Find>(tree: &gix_hash::oid, objects: Find) -> Result<Self, breadthfirst::Error>
53+
pub fn from_tree<Find>(
54+
tree: &gix_hash::oid,
55+
objects: Find,
56+
validate: gix_validate::path::component::Options,
57+
) -> Result<Self, Error>
3858
where
3959
Find: gix_object::Find,
4060
{
4161
let _span = gix_features::trace::coarse!("gix_index::State::from_tree()");
4262
let mut buf = Vec::new();
43-
let root = objects.find_tree_iter(tree, &mut buf)?;
44-
let mut delegate = CollectEntries::new();
45-
breadthfirst(root, breadthfirst::State::default(), &objects, &mut delegate)?;
63+
let root = objects
64+
.find_tree_iter(tree, &mut buf)
65+
.map_err(breadthfirst::Error::from)?;
66+
let mut delegate = CollectEntries::new(validate);
67+
match breadthfirst(root, breadthfirst::State::default(), &objects, &mut delegate) {
68+
Ok(()) => {}
69+
Err(gix_traverse::tree::breadthfirst::Error::Cancelled) => {
70+
let (path, err) = delegate
71+
.invalid_path
72+
.take()
73+
.expect("cancellation only happens on validation error");
74+
return Err(Error::InvalidComponent { path, source: err });
75+
}
76+
Err(err) => return Err(err.into()),
77+
}
4678

4779
let CollectEntries {
4880
mut entries,
4981
path_backing,
5082
path: _,
5183
path_deque: _,
84+
validate: _,
85+
invalid_path: _,
5286
} = delegate;
5387

5488
entries.sort_by(|a, b| Entry::cmp_filepaths(a.path_in(&path_backing), b.path_in(&path_backing)));
@@ -76,15 +110,19 @@ mod from_tree {
76110
path_backing: PathStorage,
77111
path: BString,
78112
path_deque: VecDeque<BString>,
113+
validate: gix_validate::path::component::Options,
114+
invalid_path: Option<(BString, gix_validate::path::component::Error)>,
79115
}
80116

81117
impl CollectEntries {
82-
pub fn new() -> CollectEntries {
118+
pub fn new(validate: gix_validate::path::component::Options) -> CollectEntries {
83119
CollectEntries {
84120
entries: Vec::new(),
85121
path_backing: Vec::new(),
86122
path: BString::default(),
87123
path_deque: VecDeque::new(),
124+
validate,
125+
invalid_path: None,
88126
}
89127
}
90128

@@ -93,6 +131,11 @@ mod from_tree {
93131
self.path.push(b'/');
94132
}
95133
self.path.push_str(name);
134+
if self.invalid_path.is_none() {
135+
if let Err(err) = gix_validate::path::component(name, None, self.validate) {
136+
self.invalid_path = Some((self.path.clone(), err))
137+
}
138+
}
96139
}
97140

98141
pub fn add_entry(&mut self, entry: &tree::EntryRef<'_>) {
@@ -103,6 +146,18 @@ mod from_tree {
103146
EntryKind::Link => Mode::SYMLINK,
104147
EntryKind::Commit => Mode::COMMIT,
105148
};
149+
// There are leaf-names that require special validation, specific to their mode.
150+
// Double-validate just for this case, as the previous validation didn't know the mode yet.
151+
if self.invalid_path.is_none() {
152+
let start = self.path.rfind_byte(b'/').map(|pos| pos + 1).unwrap_or_default();
153+
if let Err(err) = gix_validate::path::component(
154+
self.path[start..].as_ref(),
155+
(entry.mode.kind() == EntryKind::Link).then_some(gix_validate::path::component::Mode::Symlink),
156+
self.validate,
157+
) {
158+
self.invalid_path = Some((self.path.clone(), err));
159+
}
160+
}
106161

107162
let path_start = self.path_backing.len();
108163
self.path_backing.extend_from_slice(&self.path);
@@ -117,6 +172,14 @@ mod from_tree {
117172

118173
self.entries.push(new_entry);
119174
}
175+
176+
fn determine_action(&self) -> Action {
177+
if self.invalid_path.is_none() {
178+
Action::Continue
179+
} else {
180+
Action::Cancel
181+
}
182+
}
120183
}
121184

122185
impl Visit for CollectEntries {
@@ -127,12 +190,12 @@ mod from_tree {
127190
.expect("every call is matched with push_tracked_path_component");
128191
}
129192

130-
fn push_back_tracked_path_component(&mut self, component: &bstr::BStr) {
193+
fn push_back_tracked_path_component(&mut self, component: &BStr) {
131194
self.push_element(component);
132195
self.path_deque.push_back(self.path.clone());
133196
}
134197

135-
fn push_path_component(&mut self, component: &bstr::BStr) {
198+
fn push_path_component(&mut self, component: &BStr) {
136199
self.push_element(component);
137200
}
138201

@@ -144,13 +207,13 @@ mod from_tree {
144207
}
145208
}
146209

147-
fn visit_tree(&mut self, _entry: &gix_object::tree::EntryRef<'_>) -> gix_traverse::tree::visit::Action {
148-
Action::Continue
210+
fn visit_tree(&mut self, _entry: &gix_object::tree::EntryRef<'_>) -> Action {
211+
self.determine_action()
149212
}
150213

151-
fn visit_nontree(&mut self, entry: &gix_object::tree::EntryRef<'_>) -> gix_traverse::tree::visit::Action {
214+
fn visit_nontree(&mut self, entry: &gix_object::tree::EntryRef<'_>) -> Action {
152215
self.add_entry(entry);
153-
Action::Continue
216+
self.determine_action()
154217
}
155218
}
156219
}

gix-index/src/lib.rs

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,9 @@ pub mod entry;
2626

2727
mod access;
2828

29-
mod init;
29+
///
30+
#[allow(clippy::empty_docs)]
31+
pub mod init;
3032

3133
///
3234
#[allow(clippy::empty_docs)]
@@ -116,8 +118,8 @@ pub struct AccelerateLookup<'a> {
116118
///
117119
/// # A note on safety
118120
///
119-
/// An index (i.e. [`State`]) created [from a tree](State::from_tree()) is not guaranteed to have valid entry paths as those
120-
/// depend on the names contained in trees entirely, without applying any level of validation.
121+
/// An index (i.e. [`State`]) created by hand is not guaranteed to have valid entry paths as they are entirely controlled
122+
/// by the caller, without applying any level of validation.
121123
///
122124
/// This means that before using these paths to recreate files on disk, *they must be validated*.
123125
///

gix-index/tests/Cargo.toml

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -19,8 +19,9 @@ gix-features-parallel = ["gix-features/parallel"]
1919
[dev-dependencies]
2020
gix-index = { path = ".." }
2121
gix-features = { path = "../../gix-features", features = ["rustsha1", "progress"] }
22-
gix-testtools = { path = "../../tests/tools"}
23-
gix = { path = "../../gix", default-features = false, features = ["index"] }
24-
gix-hash = { path = "../../gix-hash"}
22+
gix-testtools = { path = "../../tests/tools" }
23+
gix-odb = { path = "../../gix-odb" }
24+
gix-object = { path = "../../gix-object" }
25+
gix-hash = { path = "../../gix-hash" }
2526
filetime = "0.2.15"
2627
bstr = { version = "1.3.0", default-features = false }
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.

0 commit comments

Comments
 (0)