684dd4c2b4
Before checking out a file, we have to confirm that all of its leading components are real existing directories. And to reduce the number of lstat() calls in this process, we cache the last leading path known to contain only directories. However, when a path collision occurs (e.g. when checking out case-sensitive files in case-insensitive file systems), a cached path might have its file type changed on disk, leaving the cache on an invalid state. Normally, this doesn't bring any bad consequences as we usually check out files in index order, and therefore, by the time the cached path becomes outdated, we no longer need it anyway (because all files in that directory would have already been written). But, there are some users of the checkout machinery that do not always follow the index order. In particular: checkout-index writes the paths in the same order that they appear on the CLI (or stdin); and the delayed checkout feature -- used when a long-running filter process replies with "status=delayed" -- postpones the checkout of some entries, thus modifying the checkout order. When we have to check out an out-of-order entry and the lstat() cache is invalid (due to a previous path collision), checkout_entry() may end up using the invalid data and thrusting that the leading components are real directories when, in reality, they are not. In the best case scenario, where the directory was replaced by a regular file, the user will get an error: "fatal: unable to create file 'foo/bar': Not a directory". But if the directory was replaced by a symlink, checkout could actually end up following the symlink and writing the file at a wrong place, even outside the repository. Since delayed checkout is affected by this bug, it could be used by an attacker to write arbitrary files during the clone of a maliciously crafted repository. Some candidate solutions considered were to disable the lstat() cache during unordered checkouts or sort the entries before passing them to the checkout machinery. But both ideas include some performance penalty and they don't future-proof the code against new unordered use cases. Instead, we now manually reset the lstat cache whenever we successfully remove a directory. Note: We are not even checking whether the directory was the same as the lstat cache points to because we might face a scenario where the paths refer to the same location but differ due to case folding, precomposed UTF-8 issues, or the presence of `..` components in the path. Two regression tests, with case-collisions and utf8-collisions, are also added for both checkout-index and delayed checkout. Note: to make the previously mentioned clone attack unfeasible, it would be sufficient to reset the lstat cache only after the remove_subtree() call inside checkout_entry(). This is the place where we would remove a directory whose path collides with the path of another entry that we are currently trying to check out (possibly a symlink). However, in the interest of a thorough fix that does not leave Git open to similar-but-not-identical attack vectors, we decided to intercept all `rmdir()` calls in one fell swoop. This addresses CVE-2021-21300. Co-authored-by: Johannes Schindelin <johannes.schindelin@gmx.de> Signed-off-by: Matheus Tavares <matheus.bernardino@usp.br>
71 lines
1.8 KiB
Bash
Executable File
71 lines
1.8 KiB
Bash
Executable File
#!/bin/sh
|
|
|
|
test_description='basic checkout-index tests
|
|
'
|
|
|
|
. ./test-lib.sh
|
|
|
|
test_expect_success 'checkout-index --gobbledegook' '
|
|
test_expect_code 129 git checkout-index --gobbledegook 2>err &&
|
|
test_i18ngrep "[Uu]sage" err
|
|
'
|
|
|
|
test_expect_success 'checkout-index -h in broken repository' '
|
|
mkdir broken &&
|
|
(
|
|
cd broken &&
|
|
git init &&
|
|
>.git/index &&
|
|
test_expect_code 129 git checkout-index -h >usage 2>&1
|
|
) &&
|
|
test_i18ngrep "[Uu]sage" broken/usage
|
|
'
|
|
|
|
for mode in 'case' 'utf-8'
|
|
do
|
|
case "$mode" in
|
|
case) dir='A' symlink='a' mode_prereq='CASE_INSENSITIVE_FS' ;;
|
|
utf-8)
|
|
dir=$(printf "\141\314\210") symlink=$(printf "\303\244")
|
|
mode_prereq='UTF8_NFD_TO_NFC' ;;
|
|
esac
|
|
|
|
test_expect_success SYMLINKS,$mode_prereq \
|
|
"checkout-index with $mode-collision don't write to the wrong place" '
|
|
git init $mode-collision &&
|
|
(
|
|
cd $mode-collision &&
|
|
mkdir target-dir &&
|
|
|
|
empty_obj_hex=$(git hash-object -w --stdin </dev/null) &&
|
|
symlink_hex=$(printf "%s" "$PWD/target-dir" | git hash-object -w --stdin) &&
|
|
|
|
cat >objs <<-EOF &&
|
|
100644 blob ${empty_obj_hex} ${dir}/x
|
|
100644 blob ${empty_obj_hex} ${dir}/y
|
|
100644 blob ${empty_obj_hex} ${dir}/z
|
|
120000 blob ${symlink_hex} ${symlink}
|
|
EOF
|
|
|
|
git update-index --index-info <objs &&
|
|
|
|
# Note: the order is important here to exercise the
|
|
# case where the file at ${dir} has its type changed by
|
|
# the time Git tries to check out ${dir}/z.
|
|
#
|
|
# Also, we use core.precomposeUnicode=false because we
|
|
# want Git to treat the UTF-8 paths transparently on
|
|
# Mac OS, matching what is in the index.
|
|
#
|
|
git -c core.precomposeUnicode=false checkout-index -f \
|
|
${dir}/x ${dir}/y ${symlink} ${dir}/z &&
|
|
|
|
# Should not create ${dir}/z at ${symlink}/z
|
|
test_path_is_missing target-dir/z
|
|
|
|
)
|
|
'
|
|
done
|
|
|
|
test_done
|