Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
Show all changes
51 commits
Select commit Hold shift + click to select a range
f3d5a69
mark safety-related core-flags as planned
Byron May 14, 2024
0d78db2
add validation for path components and tree-names
Byron May 14, 2024
eff4c00
feat: add validation for path components
Byron May 15, 2024
874cfd6
fix!: validate all components pushed onto the stack when creating lea…
Byron May 16, 2024
595fe87
feat!: `Stack::at_path()` replaces `is_dir` parameter with `mode`.
Byron May 17, 2024
9564699
feat: add `From<gix_object::tree::Mode> for gix_index::entry::Mode`.
Byron May 17, 2024
1ca6a3c
adapt to changes in `gix-worktree`
Byron May 17, 2024
886d6b5
feat: checkout respects options for `core.protectHFS` and `core.prote…
Byron May 17, 2024
b6a67d7
doc: make clear that indices can contain invalid or dangerous paths.
Byron May 18, 2024
a67d82d
feat: defend against `CON` device names and more if `gitoxide.core.pr…
Byron May 18, 2024
1076375
thanks clippy
Byron May 19, 2024
7fa0185
Start on demo script making repo with .. trees, deploying above repo
EliahKagan Apr 20, 2024
bf49d73
Hard-code target to fix remaining replacement bugs
EliahKagan Apr 20, 2024
4e3b77d
Add missing executable bit to payloads
EliahKagan Apr 20, 2024
474bf0d
Make the script more robust, and don't require `ex`
EliahKagan Apr 20, 2024
9180dde
Set LC_ALL=C when using sed on a binary file
EliahKagan Apr 21, 2024
0d15e5c
No need to actually create the directories
EliahKagan Apr 23, 2024
845c6bc
Don't bother running `git show --stat`
EliahKagan Apr 23, 2024
0581966
Don't require the filesystem that makes the repo to support +x
EliahKagan Apr 23, 2024
a59c05a
Stage and set mode in one step instead of two
EliahKagan Apr 23, 2024
49eb14c
Start on demo script making repo with NTFS stream
EliahKagan Apr 28, 2024
7041e73
Use .git::$INDEX_ALLOCATION instead of .git:$I30
EliahKagan May 1, 2024
7daca49
Start on demo script making repo with .git/… filename
EliahKagan May 1, 2024
981cf5b
Show the new commit, once made and on the branch
EliahKagan May 1, 2024
9436f3f
Split into commented sections
EliahKagan May 1, 2024
89ee180
Reword to be more portable and self-documenting
EliahKagan May 1, 2024
6846c90
Pass --literally to hash-object when making tree
EliahKagan May 1, 2024
4c684ca
Start on demo script making repo with ../… filename
EliahKagan May 1, 2024
bad9a79
Apply suggestions from code review
Byron May 20, 2024
fcc3b69
address review comments
Byron May 19, 2024
ccbc119
Apply suggestions from code review
Byron May 21, 2024
fe8c2c9
Adjust make_traverse_dotdot_slashes.sh for environment
EliahKagan May 21, 2024
7e9c769
Combine "slashes" scripts and make it a fixture
EliahKagan May 21, 2024
6f44aca
Combine non-"slashes" (i.e. trees) scripts and make it a fixture
EliahKagan May 21, 2024
f3edaa3
Make more test repos with traversal-attempting blob names
EliahKagan May 21, 2024
4791e31
further testing of `.git` path variants
Byron May 21, 2024
00a1c47
better detection of pre-requisites for symlink test (#1373)
Byron May 21, 2024
bec648d
fix: multi-process safe parallel filesystem capabilities probing (#1373)
Byron May 21, 2024
a6710c5
add tests for actual worktree checkouts to assure validations kick in
Byron May 21, 2024
f961687
fix compile warnings
Byron May 21, 2024
2683235
fix: assure high-speed SHA1 assembly is only used in not on Windows (…
Byron May 21, 2024
2ea87f0
fix!: `State::from_tree()` now performs name validation.
Byron May 21, 2024
5f86e6b
adapt to changes in `gix-index`
Byron May 21, 2024
f1f0ba5
feat: add `path::component_is_windows_device()`
Byron May 21, 2024
9555efe
fix!: assure that special device names on Windows aren't allowed.
Byron May 21, 2024
d2ae9d5
adapt to changes in `gix-ref`
Byron May 21, 2024
1242151
Apply suggestions from code review
Byron May 22, 2024
79dce79
Merge pull request from GHSA-7w47-3wg8-547c
Byron May 22, 2024
6f55f2a
fix-CI
Byron May 22, 2024
cd4de83
update dependencies
Byron May 22, 2024
e955770
fix: symlink support for `zip` archives
Byron May 22, 2024
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Prev Previous commit
Next Next commit
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.
  • Loading branch information
Byron committed May 22, 2024
commit 2ea87f0060fd796961a2173569f16f362ed61617
4 changes: 3 additions & 1 deletion Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions gix-index/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ gix-features = { version = "^0.38.1", path = "../gix-features", features = [
gix-hash = { version = "^0.14.2", path = "../gix-hash" }
gix-bitmap = { version = "^0.2.11", path = "../gix-bitmap" }
gix-object = { version = "^0.42.1", path = "../gix-object" }
gix-validate = { version = "^0.8.4", path = "../gix-validate" }
gix-traverse = { version = "^0.39.0", path = "../gix-traverse" }
gix-lock = { version = "^13.0.0", path = "../gix-lock" }
gix-fs = { version = "^0.10.2", path = "../gix-fs" }
Expand Down
87 changes: 75 additions & 12 deletions gix-index/src/init.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
mod from_tree {
#[allow(clippy::empty_docs)]
///
pub mod from_tree {
use std::collections::VecDeque;

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

/// The error returned by [State::from_tree()].
#[derive(Debug, thiserror::Error)]
#[allow(missing_docs)]
pub enum Error {
#[error("The path \"{path}\" is invalid")]
InvalidComponent {
path: BString,
source: gix_validate::path::component::Error,
},
#[error(transparent)]
Traversal(#[from] gix_traverse::tree::breadthfirst::Error),
}

/// Initialization
impl State {
/// Return a new and empty in-memory index assuming the given `object_hash`.
Expand All @@ -32,23 +47,42 @@ mod from_tree {
}
/// Create an index [`State`] by traversing `tree` recursively, accessing sub-trees
/// with `objects`.
/// `validate` is used to determine which validations to perform on every path component we see.
///
/// **No extension data is currently produced**.
pub fn from_tree<Find>(tree: &gix_hash::oid, objects: Find) -> Result<Self, breadthfirst::Error>
pub fn from_tree<Find>(
tree: &gix_hash::oid,
objects: Find,
validate: gix_validate::path::component::Options,
) -> Result<Self, Error>
where
Find: gix_object::Find,
{
let _span = gix_features::trace::coarse!("gix_index::State::from_tree()");
let mut buf = Vec::new();
let root = objects.find_tree_iter(tree, &mut buf)?;
let mut delegate = CollectEntries::new();
breadthfirst(root, breadthfirst::State::default(), &objects, &mut delegate)?;
let root = objects
.find_tree_iter(tree, &mut buf)
.map_err(breadthfirst::Error::from)?;
let mut delegate = CollectEntries::new(validate);
match breadthfirst(root, breadthfirst::State::default(), &objects, &mut delegate) {
Ok(()) => {}
Err(gix_traverse::tree::breadthfirst::Error::Cancelled) => {
let (path, err) = delegate
.invalid_path
.take()
.expect("cancellation only happens on validation error");
return Err(Error::InvalidComponent { path, source: err });
}
Err(err) => return Err(err.into()),
}

let CollectEntries {
mut entries,
path_backing,
path: _,
path_deque: _,
validate: _,
invalid_path: _,
} = delegate;

entries.sort_by(|a, b| Entry::cmp_filepaths(a.path_in(&path_backing), b.path_in(&path_backing)));
Expand Down Expand Up @@ -76,15 +110,19 @@ mod from_tree {
path_backing: PathStorage,
path: BString,
path_deque: VecDeque<BString>,
validate: gix_validate::path::component::Options,
invalid_path: Option<(BString, gix_validate::path::component::Error)>,
}

impl CollectEntries {
pub fn new() -> CollectEntries {
pub fn new(validate: gix_validate::path::component::Options) -> CollectEntries {
CollectEntries {
entries: Vec::new(),
path_backing: Vec::new(),
path: BString::default(),
path_deque: VecDeque::new(),
validate,
invalid_path: None,
}
}

Expand All @@ -93,6 +131,11 @@ mod from_tree {
self.path.push(b'/');
}
self.path.push_str(name);
if self.invalid_path.is_none() {
if let Err(err) = gix_validate::path::component(name, None, self.validate) {
self.invalid_path = Some((self.path.clone(), err))
}
}
}

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

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

self.entries.push(new_entry);
}

fn determine_action(&self) -> Action {
if self.invalid_path.is_none() {
Action::Continue
} else {
Action::Cancel
}
}
}

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

fn push_back_tracked_path_component(&mut self, component: &bstr::BStr) {
fn push_back_tracked_path_component(&mut self, component: &BStr) {
self.push_element(component);
self.path_deque.push_back(self.path.clone());
}

fn push_path_component(&mut self, component: &bstr::BStr) {
fn push_path_component(&mut self, component: &BStr) {
self.push_element(component);
}

Expand All @@ -144,13 +207,13 @@ mod from_tree {
}
}

fn visit_tree(&mut self, _entry: &gix_object::tree::EntryRef<'_>) -> gix_traverse::tree::visit::Action {
Action::Continue
fn visit_tree(&mut self, _entry: &gix_object::tree::EntryRef<'_>) -> Action {
self.determine_action()
}

fn visit_nontree(&mut self, entry: &gix_object::tree::EntryRef<'_>) -> gix_traverse::tree::visit::Action {
fn visit_nontree(&mut self, entry: &gix_object::tree::EntryRef<'_>) -> Action {
self.add_entry(entry);
Action::Continue
self.determine_action()
}
}
}
8 changes: 5 additions & 3 deletions gix-index/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,9 @@ pub mod entry;

mod access;

mod init;
///
#[allow(clippy::empty_docs)]
pub mod init;

///
#[allow(clippy::empty_docs)]
Expand Down Expand Up @@ -116,8 +118,8 @@ pub struct AccelerateLookup<'a> {
///
/// # A note on safety
///
/// An index (i.e. [`State`]) created [from a tree](State::from_tree()) is not guaranteed to have valid entry paths as those
/// depend on the names contained in trees entirely, without applying any level of validation.
/// An index (i.e. [`State`]) created by hand is not guaranteed to have valid entry paths as they are entirely controlled
/// by the caller, without applying any level of validation.
///
/// This means that before using these paths to recreate files on disk, *they must be validated*.
///
Expand Down
7 changes: 4 additions & 3 deletions gix-index/tests/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -19,8 +19,9 @@ gix-features-parallel = ["gix-features/parallel"]
[dev-dependencies]
gix-index = { path = ".." }
gix-features = { path = "../../gix-features", features = ["rustsha1", "progress"] }
gix-testtools = { path = "../../tests/tools"}
gix = { path = "../../gix", default-features = false, features = ["index"] }
gix-hash = { path = "../../gix-hash"}
gix-testtools = { path = "../../tests/tools" }
gix-odb = { path = "../../gix-odb" }
gix-object = { path = "../../gix-object" }
gix-hash = { path = "../../gix-hash" }
filetime = "0.2.15"
bstr = { version = "1.3.0", default-features = false }
Binary file not shown.
Binary file modified gix-index/tests/fixtures/generated-archives/v2.tar.xz
Binary file not shown.
Binary file not shown.
Binary file modified gix-index/tests/fixtures/generated-archives/v2_more_files.tar.xz
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
2 changes: 2 additions & 0 deletions gix-index/tests/fixtures/make_index/v2.sh
Original file line number Diff line number Diff line change
Expand Up @@ -8,3 +8,5 @@ git config index.threads 2
touch a
git add a
git commit -m "empty"

git rev-parse @^{tree} > head.tree
2 changes: 2 additions & 0 deletions gix-index/tests/fixtures/make_index/v2_all_file_kinds.sh
Original file line number Diff line number Diff line change
Expand Up @@ -22,3 +22,5 @@ mkdir d

git add .
git commit -m "init"

git rev-parse @^{tree} > head.tree
2 changes: 2 additions & 0 deletions gix-index/tests/fixtures/make_index/v2_more_files.sh
Original file line number Diff line number Diff line change
Expand Up @@ -11,3 +11,5 @@ mkdir d

git add .
git commit -m "empty"

git rev-parse @^{tree} > head.tree
Original file line number Diff line number Diff line change
Expand Up @@ -17,4 +17,4 @@ git config --worktree index.sparse true
echo "/*" > .git/info/sparse-checkout &&
echo "!/*/" >> .git/info/sparse-checkout

git checkout main
git checkout main
Original file line number Diff line number Diff line change
Expand Up @@ -13,4 +13,4 @@ mkdir d
git add .
git commit -m "init"

git sparse-checkout set c1/c2 --no-cone
git sparse-checkout set c1/c2 --no-cone
2 changes: 2 additions & 0 deletions gix-index/tests/fixtures/make_index/v4_more_files_IEOT.sh
Original file line number Diff line number Diff line change
Expand Up @@ -12,3 +12,5 @@ touch x

git add .
git commit -m "empty"

git rev-parse @^{tree} > head.tree
44 changes: 44 additions & 0 deletions gix-index/tests/fixtures/make_traverse_literal_separators.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
#!/bin/bash
set -eu -o pipefail

# Makes a repo carrying a literally named file, which may even contain "/".
# File content is from stdin. Arguments are repo name, file name, and file mode.
function make_repo() (
local repo="$1" file="$2" mode="$3"
local blob_hash_escaped tree_hash commit_hash branch

git init -- "$repo"
cd -- "$repo" # Temporary, as the function body is a ( ) subshell.

blob_hash_escaped="$(git hash-object -w --stdin | sed 's/../\\x&/g')"

tree_hash="$(
printf "%s %s\\0$blob_hash_escaped" "$mode" "$file" |
git hash-object -t tree -w --stdin --literally
)"

commit_hash="$(git commit-tree -m 'Initial commit' "$tree_hash")"

branch="$(git symbolic-ref --short HEAD)"
git branch -f -- "$branch" "$commit_hash"
test -z "${DEBUG_FIXTURE-}" || git show # TODO: How should verbosity be controlled?
git rev-parse @^{tree} > head.tree
)

make_repo traverse_dotdot_slashes ../outside 100644 \
<<<'A file outside the working tree, somehow.'

make_repo traverse_dotgit_slashes .git/hooks/pre-commit 100755 <<'EOF'
#!/bin/sh
printf 'Vulnerable!\n'
date >vulnerable
EOF

make_repo traverse_dotdot_backslashes '..\outside' 100644 \
<<<'A file outside the working tree, somehow.'

make_repo traverse_dotgit_backslashes '.git\hooks\pre-commit' 100755 <<'EOF'
#!/bin/sh
printf 'Vulnerable!\n'
date >vulnerable
EOF
2 changes: 1 addition & 1 deletion gix-index/tests/index/file/read.rs
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ use crate::{hex_to_id, index::Fixture, loose_file_path};
fn verify(index: gix_index::File) -> gix_index::File {
index.verify_integrity().unwrap();
index.verify_entries().unwrap();
index.verify_extensions(false, gix::objs::find::Never).unwrap();
index.verify_extensions(false, gix_object::find::Never).unwrap();
index
}

Expand Down
2 changes: 1 addition & 1 deletion gix-index/tests/index/file/write.rs
Original file line number Diff line number Diff line change
Expand Up @@ -228,7 +228,7 @@ fn compare_states_against_baseline(

fn compare_states(actual: &State, actual_version: Version, expected: &State, options: Options, fixture: &str) {
actual.verify_entries().expect("valid");
actual.verify_extensions(false, gix::objs::find::Never).expect("valid");
actual.verify_extensions(false, gix_object::find::Never).expect("valid");

assert_eq!(
actual.version(),
Expand Down
Loading