summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--Documentation/gitattributes.txt6
-rw-r--r--Documentation/gitignore.txt4
-rw-r--r--Documentation/gitmailmap.txt7
-rw-r--r--Documentation/gitmodules.txt8
-rw-r--r--cache.h1
-rw-r--r--fsck.c84
-rw-r--r--fsck.h3
-rw-r--r--path.c5
-rw-r--r--t/helper/test-path-utils.c46
-rwxr-xr-xt/t0060-path-utils.sh30
-rwxr-xr-xt/t7450-bad-git-dotfiles.sh (renamed from t/t7415-submodule-names.sh)116
-rw-r--r--utf8.c5
-rw-r--r--utf8.h1
13 files changed, 255 insertions, 61 deletions
diff --git a/Documentation/gitattributes.txt b/Documentation/gitattributes.txt
index cfcfa800c2..83fd4e19a4 100644
--- a/Documentation/gitattributes.txt
+++ b/Documentation/gitattributes.txt
@@ -1247,6 +1247,12 @@ to:
[attr]binary -diff -merge -text
------------
+NOTES
+-----
+
+Git does not follow symbolic links when accessing a `.gitattributes`
+file in the working tree. This keeps behavior consistent when the file
+is accessed from the index or a tree versus from the filesystem.
EXAMPLES
--------
diff --git a/Documentation/gitignore.txt b/Documentation/gitignore.txt
index 5751603b13..53e7d5c914 100644
--- a/Documentation/gitignore.txt
+++ b/Documentation/gitignore.txt
@@ -149,6 +149,10 @@ not tracked by Git remain untracked.
To stop tracking a file that is currently tracked, use
'git rm --cached'.
+Git does not follow symbolic links when accessing a `.gitignore` file in
+the working tree. This keeps behavior consistent when the file is
+accessed from the index or a tree versus from the filesystem.
+
EXAMPLES
--------
diff --git a/Documentation/gitmailmap.txt b/Documentation/gitmailmap.txt
index 3fb39f801f..06f4af93fe 100644
--- a/Documentation/gitmailmap.txt
+++ b/Documentation/gitmailmap.txt
@@ -55,6 +55,13 @@ this would also match the 'Commit Name <commit&#64;email.xx>' above:
Proper Name <proper@email.xx> CoMmIt NaMe <CoMmIt@EmAiL.xX>
--
+NOTES
+-----
+
+Git does not follow symbolic links when accessing a `.mailmap` file in
+the working tree. This keeps behavior consistent when the file is
+accessed from the index or a tree versus from the filesystem.
+
EXAMPLES
--------
diff --git a/Documentation/gitmodules.txt b/Documentation/gitmodules.txt
index 8e333dde1b..dcee09b500 100644
--- a/Documentation/gitmodules.txt
+++ b/Documentation/gitmodules.txt
@@ -98,6 +98,14 @@ submodule.<name>.shallow::
shallow clone (with a history depth of 1) unless the user explicitly
asks for a non-shallow clone.
+NOTES
+-----
+
+Git does not allow the `.gitmodules` file within a working tree to be a
+symbolic link, and will refuse to check out such a tree entry. This
+keeps behavior consistent when the file is accessed from the index or a
+tree versus from the filesystem, and helps Git reliably enforce security
+checks of the file contents.
EXAMPLES
--------
diff --git a/cache.h b/cache.h
index 1666c38edb..abeaec2b2b 100644
--- a/cache.h
+++ b/cache.h
@@ -1272,6 +1272,7 @@ int is_ntfs_dotgit(const char *name);
int is_ntfs_dotgitmodules(const char *name);
int is_ntfs_dotgitignore(const char *name);
int is_ntfs_dotgitattributes(const char *name);
+int is_ntfs_dotmailmap(const char *name);
/*
* Returns true iff "str" could be confused as a command-line option when
diff --git a/fsck.c b/fsck.c
index f5ed6a2635..3ec500d707 100644
--- a/fsck.c
+++ b/fsck.c
@@ -558,7 +558,7 @@ static int verify_ordered(unsigned mode1, const char *name1,
return c1 < c2 ? 0 : TREE_UNORDERED;
}
-static int fsck_tree(const struct object_id *oid,
+static int fsck_tree(const struct object_id *tree_oid,
const char *buffer, unsigned long size,
struct fsck_options *options)
{
@@ -579,7 +579,9 @@ static int fsck_tree(const struct object_id *oid,
struct name_stack df_dup_candidates = { NULL };
if (init_tree_desc_gently(&desc, buffer, size)) {
- retval += report(options, oid, OBJ_TREE, FSCK_MSG_BAD_TREE, "cannot be parsed as a tree");
+ retval += report(options, tree_oid, OBJ_TREE,
+ FSCK_MSG_BAD_TREE,
+ "cannot be parsed as a tree");
return retval;
}
@@ -589,11 +591,11 @@ static int fsck_tree(const struct object_id *oid,
while (desc.size) {
unsigned short mode;
const char *name, *backslash;
- const struct object_id *oid;
+ const struct object_id *entry_oid;
- oid = tree_entry_extract(&desc, &name, &mode);
+ entry_oid = tree_entry_extract(&desc, &name, &mode);
- has_null_sha1 |= is_null_oid(oid);
+ has_null_sha1 |= is_null_oid(entry_oid);
has_full_path |= !!strchr(name, '/');
has_empty_name |= !*name;
has_dot |= !strcmp(name, ".");
@@ -603,23 +605,43 @@ static int fsck_tree(const struct object_id *oid,
if (is_hfs_dotgitmodules(name) || is_ntfs_dotgitmodules(name)) {
if (!S_ISLNK(mode))
- oidset_insert(&options->gitmodules_found, oid);
+ oidset_insert(&options->gitmodules_found,
+ entry_oid);
else
retval += report(options,
- oid, OBJ_TREE,
+ tree_oid, OBJ_TREE,
FSCK_MSG_GITMODULES_SYMLINK,
".gitmodules is a symbolic link");
}
+ if (S_ISLNK(mode)) {
+ if (is_hfs_dotgitignore(name) ||
+ is_ntfs_dotgitignore(name))
+ retval += report(options, tree_oid, OBJ_TREE,
+ FSCK_MSG_GITIGNORE_SYMLINK,
+ ".gitignore is a symlink");
+ if (is_hfs_dotgitattributes(name) ||
+ is_ntfs_dotgitattributes(name))
+ retval += report(options, tree_oid, OBJ_TREE,
+ FSCK_MSG_GITATTRIBUTES_SYMLINK,
+ ".gitattributes is a symlink");
+ if (is_hfs_dotmailmap(name) ||
+ is_ntfs_dotmailmap(name))
+ retval += report(options, tree_oid, OBJ_TREE,
+ FSCK_MSG_MAILMAP_SYMLINK,
+ ".mailmap is a symlink");
+ }
+
if ((backslash = strchr(name, '\\'))) {
while (backslash) {
backslash++;
has_dotgit |= is_ntfs_dotgit(backslash);
if (is_ntfs_dotgitmodules(backslash)) {
if (!S_ISLNK(mode))
- oidset_insert(&options->gitmodules_found, oid);
+ oidset_insert(&options->gitmodules_found,
+ entry_oid);
else
- retval += report(options, oid, OBJ_TREE,
+ retval += report(options, tree_oid, OBJ_TREE,
FSCK_MSG_GITMODULES_SYMLINK,
".gitmodules is a symbolic link");
}
@@ -628,7 +650,9 @@ static int fsck_tree(const struct object_id *oid,
}
if (update_tree_entry_gently(&desc)) {
- retval += report(options, oid, OBJ_TREE, FSCK_MSG_BAD_TREE, "cannot be parsed as a tree");
+ retval += report(options, tree_oid, OBJ_TREE,
+ FSCK_MSG_BAD_TREE,
+ "cannot be parsed as a tree");
break;
}
@@ -676,25 +700,45 @@ static int fsck_tree(const struct object_id *oid,
name_stack_clear(&df_dup_candidates);
if (has_null_sha1)
- retval += report(options, oid, OBJ_TREE, FSCK_MSG_NULL_SHA1, "contains entries pointing to null sha1");
+ retval += report(options, tree_oid, OBJ_TREE,
+ FSCK_MSG_NULL_SHA1,
+ "contains entries pointing to null sha1");
if (has_full_path)
- retval += report(options, oid, OBJ_TREE, FSCK_MSG_FULL_PATHNAME, "contains full pathnames");
+ retval += report(options, tree_oid, OBJ_TREE,
+ FSCK_MSG_FULL_PATHNAME,
+ "contains full pathnames");
if (has_empty_name)
- retval += report(options, oid, OBJ_TREE, FSCK_MSG_EMPTY_NAME, "contains empty pathname");
+ retval += report(options, tree_oid, OBJ_TREE,
+ FSCK_MSG_EMPTY_NAME,
+ "contains empty pathname");
if (has_dot)
- retval += report(options, oid, OBJ_TREE, FSCK_MSG_HAS_DOT, "contains '.'");
+ retval += report(options, tree_oid, OBJ_TREE,
+ FSCK_MSG_HAS_DOT,
+ "contains '.'");
if (has_dotdot)
- retval += report(options, oid, OBJ_TREE, FSCK_MSG_HAS_DOTDOT, "contains '..'");
+ retval += report(options, tree_oid, OBJ_TREE,
+ FSCK_MSG_HAS_DOTDOT,
+ "contains '..'");
if (has_dotgit)
- retval += report(options, oid, OBJ_TREE, FSCK_MSG_HAS_DOTGIT, "contains '.git'");
+ retval += report(options, tree_oid, OBJ_TREE,
+ FSCK_MSG_HAS_DOTGIT,
+ "contains '.git'");
if (has_zero_pad)
- retval += report(options, oid, OBJ_TREE, FSCK_MSG_ZERO_PADDED_FILEMODE, "contains zero-padded file modes");
+ retval += report(options, tree_oid, OBJ_TREE,
+ FSCK_MSG_ZERO_PADDED_FILEMODE,
+ "contains zero-padded file modes");
if (has_bad_modes)
- retval += report(options, oid, OBJ_TREE, FSCK_MSG_BAD_FILEMODE, "contains bad file modes");
+ retval += report(options, tree_oid, OBJ_TREE,
+ FSCK_MSG_BAD_FILEMODE,
+ "contains bad file modes");
if (has_dup_entries)
- retval += report(options, oid, OBJ_TREE, FSCK_MSG_DUPLICATE_ENTRIES, "contains duplicate file entries");
+ retval += report(options, tree_oid, OBJ_TREE,
+ FSCK_MSG_DUPLICATE_ENTRIES,
+ "contains duplicate file entries");
if (not_properly_sorted)
- retval += report(options, oid, OBJ_TREE, FSCK_MSG_TREE_NOT_SORTED, "not properly sorted");
+ retval += report(options, tree_oid, OBJ_TREE,
+ FSCK_MSG_TREE_NOT_SORTED,
+ "not properly sorted");
return retval;
}
diff --git a/fsck.h b/fsck.h
index 7202c3c87e..d07f7a2459 100644
--- a/fsck.h
+++ b/fsck.h
@@ -67,6 +67,9 @@ enum fsck_msg_type {
FUNC(NUL_IN_COMMIT, WARN) \
/* infos (reported as warnings, but ignored by default) */ \
FUNC(GITMODULES_PARSE, INFO) \
+ FUNC(GITIGNORE_SYMLINK, INFO) \
+ FUNC(GITATTRIBUTES_SYMLINK, INFO) \
+ FUNC(MAILMAP_SYMLINK, INFO) \
FUNC(BAD_TAG_NAME, INFO) \
FUNC(MISSING_TAGGER_ENTRY, INFO) \
/* ignored (elevated when requested) */ \
diff --git a/path.c b/path.c
index 9e883eb524..7bccd830e9 100644
--- a/path.c
+++ b/path.c
@@ -1493,6 +1493,11 @@ int is_ntfs_dotgitattributes(const char *name)
return is_ntfs_dot_str(name, "gitattributes", "gi7d29");
}
+int is_ntfs_dotmailmap(const char *name)
+{
+ return is_ntfs_dot_str(name, "mailmap", "maba30");
+}
+
int looks_like_command_line_option(const char *str)
{
return str && str[0] == '-';
diff --git a/t/helper/test-path-utils.c b/t/helper/test-path-utils.c
index 313a153209..229ed416b0 100644
--- a/t/helper/test-path-utils.c
+++ b/t/helper/test-path-utils.c
@@ -172,9 +172,22 @@ static struct test_data dirname_data[] = {
{ NULL, NULL }
};
-static int is_dotgitmodules(const char *path)
+static int check_dotfile(const char *x, const char **argv,
+ int (*is_hfs)(const char *),
+ int (*is_ntfs)(const char *))
{
- return is_hfs_dotgitmodules(path) || is_ntfs_dotgitmodules(path);
+ int res = 0, expect = 1;
+ for (; *argv; argv++) {
+ if (!strcmp("--not", *argv))
+ expect = !expect;
+ else if (expect != (is_hfs(*argv) || is_ntfs(*argv)))
+ res = error("'%s' is %s.git%s", *argv,
+ expect ? "not " : "", x);
+ else
+ fprintf(stderr, "ok: '%s' is %s.git%s\n",
+ *argv, expect ? "" : "not ", x);
+ }
+ return !!res;
}
static int cmp_by_st_size(const void *a, const void *b)
@@ -382,17 +395,24 @@ int cmd__path_utils(int argc, const char **argv)
return test_function(dirname_data, posix_dirname, argv[1]);
if (argc > 2 && !strcmp(argv[1], "is_dotgitmodules")) {
- int res = 0, expect = 1, i;
- for (i = 2; i < argc; i++)
- if (!strcmp("--not", argv[i]))
- expect = !expect;
- else if (expect != is_dotgitmodules(argv[i]))
- res = error("'%s' is %s.gitmodules", argv[i],
- expect ? "not " : "");
- else
- fprintf(stderr, "ok: '%s' is %s.gitmodules\n",
- argv[i], expect ? "" : "not ");
- return !!res;
+ return check_dotfile("modules", argv + 2,
+ is_hfs_dotgitmodules,
+ is_ntfs_dotgitmodules);
+ }
+ if (argc > 2 && !strcmp(argv[1], "is_dotgitignore")) {
+ return check_dotfile("ignore", argv + 2,
+ is_hfs_dotgitignore,
+ is_ntfs_dotgitignore);
+ }
+ if (argc > 2 && !strcmp(argv[1], "is_dotgitattributes")) {
+ return check_dotfile("attributes", argv + 2,
+ is_hfs_dotgitattributes,
+ is_ntfs_dotgitattributes);
+ }
+ if (argc > 2 && !strcmp(argv[1], "is_dotmailmap")) {
+ return check_dotfile("mailmap", argv + 2,
+ is_hfs_dotmailmap,
+ is_ntfs_dotmailmap);
}
if (argc > 2 && !strcmp(argv[1], "file-size")) {
diff --git a/t/t0060-path-utils.sh b/t/t0060-path-utils.sh
index 0ff06b5d1b..de4960783f 100755
--- a/t/t0060-path-utils.sh
+++ b/t/t0060-path-utils.sh
@@ -468,6 +468,36 @@ test_expect_success 'match .gitmodules' '
.gitmodules,:\$DATA
'
+test_expect_success 'match .gitattributes' '
+ test-tool path-utils is_dotgitattributes \
+ .gitattributes \
+ .git${u200c}attributes \
+ .Gitattributes \
+ .gitattributeS \
+ GITATT~1 \
+ GI7D29~1
+'
+
+test_expect_success 'match .gitignore' '
+ test-tool path-utils is_dotgitignore \
+ .gitignore \
+ .git${u200c}ignore \
+ .Gitignore \
+ .gitignorE \
+ GITIGN~1 \
+ GI250A~1
+'
+
+test_expect_success 'match .mailmap' '
+ test-tool path-utils is_dotmailmap \
+ .mailmap \
+ .mail${u200c}map \
+ .Mailmap \
+ .mailmaP \
+ MAILMA~1 \
+ MABA30~1
+'
+
test_expect_success MINGW 'is_valid_path() on Windows' '
test-tool path-utils is_valid_path \
win32 \
diff --git a/t/t7415-submodule-names.sh b/t/t7450-bad-git-dotfiles.sh
index 6bf098a6be..41706c1c9f 100755
--- a/t/t7415-submodule-names.sh
+++ b/t/t7450-bad-git-dotfiles.sh
@@ -1,9 +1,16 @@
#!/bin/sh
-test_description='check handling of .. in submodule names
+test_description='check broken or malicious patterns in .git* files
-Exercise the name-checking function on a variety of names, and then give a
-real-world setup that confirms we catch this in practice.
+Such as:
+
+ - presence of .. in submodule names;
+ Exercise the name-checking function on a variety of names, and then give a
+ real-world setup that confirms we catch this in practice.
+
+ - nested submodule names
+
+ - symlinked .gitmodules, etc
'
. ./test-lib.sh
. "$TEST_DIRECTORY"/lib-pack.sh
@@ -132,31 +139,84 @@ test_expect_success 'index-pack --strict works for non-repo pack' '
grep gitmodulesName output
'
-test_expect_success 'fsck detects symlinked .gitmodules file' '
- git init symlink &&
- (
- cd symlink &&
-
- # Make the tree directly to avoid index restrictions.
- #
- # Because symlinks store the target as a blob, choose
- # a pathname that could be parsed as a .gitmodules file
- # to trick naive non-symlink-aware checking.
- tricky="[foo]bar=true" &&
- content=$(git hash-object -w ../.gitmodules) &&
- target=$(printf "$tricky" | git hash-object -w --stdin) &&
- {
- printf "100644 blob $content\t$tricky\n" &&
- printf "120000 blob $target\t.gitmodules\n"
- } | git mktree &&
-
- # Check not only that we fail, but that it is due to the
- # symlink detector; this grep string comes from the config
- # variable name and will not be translated.
- test_must_fail git fsck 2>output &&
- test_i18ngrep gitmodulesSymlink output
- )
-'
+check_dotx_symlink () {
+ fsck_must_fail=test_must_fail
+ fsck_prefix=error
+ refuse_index=t
+ case "$1" in
+ --warning)
+ fsck_must_fail=
+ fsck_prefix=warning
+ refuse_index=
+ shift
+ ;;
+ esac
+
+ name=$1
+ type=$2
+ path=$3
+ dir=symlink-$name-$type
+
+ test_expect_success "set up repo with symlinked $name ($type)" '
+ git init $dir &&
+ (
+ cd $dir &&
+
+ # Make the tree directly to avoid index restrictions.
+ #
+ # Because symlinks store the target as a blob, choose
+ # a pathname that could be parsed as a .gitmodules file
+ # to trick naive non-symlink-aware checking.
+ tricky="[foo]bar=true" &&
+ content=$(git hash-object -w ../.gitmodules) &&
+ target=$(printf "$tricky" | git hash-object -w --stdin) &&
+ {
+ printf "100644 blob $content\t$tricky\n" &&
+ printf "120000 blob $target\t$path\n"
+ } >bad-tree
+ ) &&
+ tree=$(git -C $dir mktree <$dir/bad-tree)
+ '
+
+ test_expect_success "fsck detects symlinked $name ($type)" '
+ (
+ cd $dir &&
+
+ # Check not only that we fail, but that it is due to the
+ # symlink detector
+ $fsck_must_fail git fsck 2>output &&
+ grep "$fsck_prefix.*tree $tree: ${name}Symlink" output
+ )
+ '
+
+ test -n "$refuse_index" &&
+ test_expect_success "refuse to load symlinked $name into index ($type)" '
+ test_must_fail \
+ git -C $dir \
+ -c core.protectntfs \
+ -c core.protecthfs \
+ read-tree $tree 2>err &&
+ grep "invalid path.*$name" err &&
+ git -C $dir ls-files -s >out &&
+ test_must_be_empty out
+ '
+}
+
+check_dotx_symlink gitmodules vanilla .gitmodules
+check_dotx_symlink gitmodules ntfs ".gitmodules ."
+check_dotx_symlink gitmodules hfs ".${u200c}gitmodules"
+
+check_dotx_symlink --warning gitattributes vanilla .gitattributes
+check_dotx_symlink --warning gitattributes ntfs ".gitattributes ."
+check_dotx_symlink --warning gitattributes hfs ".${u200c}gitattributes"
+
+check_dotx_symlink --warning gitignore vanilla .gitignore
+check_dotx_symlink --warning gitignore ntfs ".gitignore ."
+check_dotx_symlink --warning gitignore hfs ".${u200c}gitignore"
+
+check_dotx_symlink --warning mailmap vanilla .mailmap
+check_dotx_symlink --warning mailmap ntfs ".mailmap ."
+check_dotx_symlink --warning mailmap hfs ".${u200c}mailmap"
test_expect_success 'fsck detects non-blob .gitmodules' '
git init non-blob &&
diff --git a/utf8.c b/utf8.c
index 5b39361ada..de4ce5c0e6 100644
--- a/utf8.c
+++ b/utf8.c
@@ -777,6 +777,11 @@ int is_hfs_dotgitattributes(const char *path)
return is_hfs_dot_str(path, "gitattributes");
}
+int is_hfs_dotmailmap(const char *path)
+{
+ return is_hfs_dot_str(path, "mailmap");
+}
+
const char utf8_bom[] = "\357\273\277";
int skip_utf8_bom(char **text, size_t len)
diff --git a/utf8.h b/utf8.h
index fcd5167baf..9a16c8679d 100644
--- a/utf8.h
+++ b/utf8.h
@@ -61,6 +61,7 @@ int is_hfs_dotgit(const char *path);
int is_hfs_dotgitmodules(const char *path);
int is_hfs_dotgitignore(const char *path);
int is_hfs_dotgitattributes(const char *path);
+int is_hfs_dotmailmap(const char *path);
typedef enum {
ALIGN_LEFT,