attr: do not respect symlinks for in-tree .gitattributes

The attributes system may sometimes read in-tree files from the
filesystem, and sometimes from the index. In the latter case, we do not
resolve symbolic links (and are not likely to ever start doing so).
Let's open filesystem links with O_NOFOLLOW so that the two cases behave
consistently.

As a bonus, this means that git will not follow such symlinks to read
and parse out-of-tree paths. In some cases this could have security
implications, as a malicious repository can cause Git to open and read
arbitrary files. It could already feed arbitrary content to the parser,
but in certain setups it might be able to exfiltrate data from those
paths (e.g., if an automated service operating on the malicious repo
reveals its stderr to an attacker).

Note that O_NOFOLLOW only prevents following links for the path itself,
not intermediate directories in the path.  At first glance, it seems
like

  ln -s /some/path in-repo

might still look at "in-repo/.gitattributes", following the symlink to
"/some/path/.gitattributes". However, if "in-repo" is a symbolic link,
then we know that it has no git paths below it, and will never look at
its .gitattributes file.

We will continue to support out-of-tree symbolic links (e.g., in
$GIT_DIR/info/attributes); this just affects in-tree links. When a
symbolic link is encountered, the contents are ignored and a warning is
printed. POSIX specifies ELOOP in this case, so the user would generally
see something like:

  warning: unable to access '.gitattributes': Too many levels of symbolic links

Signed-off-by: Jeff King <peff@peff.net>
Signed-off-by: Junio C Hamano <gitster@pobox.com>
This commit is contained in:
Jeff King 2021-02-16 09:44:32 -05:00 коммит произвёл Junio C Hamano
Родитель 1679d60bfc
Коммит 2ef579e261
2 изменённых файлов: 48 добавлений и 7 удалений

19
attr.c
Просмотреть файл

@ -280,6 +280,7 @@ static const char blank[] = " \t\r\n";
/* Flags usable in read_attr() and parse_attr_line() family of functions. */ /* Flags usable in read_attr() and parse_attr_line() family of functions. */
#define READ_ATTR_MACRO_OK (1<<0) #define READ_ATTR_MACRO_OK (1<<0)
#define READ_ATTR_NOFOLLOW (1<<1)
/* /*
* Parse a whitespace-delimited attribute state (i.e., "attr", * Parse a whitespace-delimited attribute state (i.e., "attr",
@ -704,13 +705,23 @@ void git_attr_set_direction(enum git_attr_direction new_direction)
static struct attr_stack *read_attr_from_file(const char *path, unsigned flags) static struct attr_stack *read_attr_from_file(const char *path, unsigned flags)
{ {
FILE *fp = fopen_or_warn(path, "r"); int fd;
FILE *fp;
struct attr_stack *res; struct attr_stack *res;
char buf[2048]; char buf[2048];
int lineno = 0; int lineno = 0;
if (!fp) if (flags & READ_ATTR_NOFOLLOW)
fd = open_nofollow(path, O_RDONLY);
else
fd = open(path, O_RDONLY);
if (fd < 0) {
warn_on_fopen_errors(path);
return NULL; return NULL;
}
fp = xfdopen(fd, "r");
res = xcalloc(1, sizeof(*res)); res = xcalloc(1, sizeof(*res));
while (fgets(buf, sizeof(buf), fp)) { while (fgets(buf, sizeof(buf), fp)) {
char *bufp = buf; char *bufp = buf;
@ -870,7 +881,7 @@ static void bootstrap_attr_stack(const struct index_state *istate,
} }
/* root directory */ /* root directory */
e = read_attr(istate, GITATTRIBUTES_FILE, flags); e = read_attr(istate, GITATTRIBUTES_FILE, flags | READ_ATTR_NOFOLLOW);
push_stack(stack, e, xstrdup(""), 0); push_stack(stack, e, xstrdup(""), 0);
/* info frame */ /* info frame */
@ -961,7 +972,7 @@ static void prepare_attr_stack(const struct index_state *istate,
strbuf_add(&pathbuf, path + pathbuf.len, (len - pathbuf.len)); strbuf_add(&pathbuf, path + pathbuf.len, (len - pathbuf.len));
strbuf_addf(&pathbuf, "/%s", GITATTRIBUTES_FILE); strbuf_addf(&pathbuf, "/%s", GITATTRIBUTES_FILE);
next = read_attr(istate, pathbuf.buf, 0); next = read_attr(istate, pathbuf.buf, READ_ATTR_NOFOLLOW);
/* reset the pathbuf to not include "/.gitattributes" */ /* reset the pathbuf to not include "/.gitattributes" */
strbuf_setlen(&pathbuf, len); strbuf_setlen(&pathbuf, len);

Просмотреть файл

@ -4,12 +4,16 @@ test_description=gitattributes
. ./test-lib.sh . ./test-lib.sh
attr_check () { attr_check_basic () {
path="$1" expect="$2" git_opts="$3" && path="$1" expect="$2" git_opts="$3" &&
git $git_opts check-attr test -- "$path" >actual 2>err && git $git_opts check-attr test -- "$path" >actual 2>err &&
echo "$path: test: $expect" >expect && echo "$path: test: $expect" >expect &&
test_cmp expect actual && test_cmp expect actual
}
attr_check () {
attr_check_basic "$@" &&
test_must_be_empty err test_must_be_empty err
} }
@ -331,7 +335,6 @@ test_expect_success 'binary macro expanded by -a' '
test_cmp expect actual test_cmp expect actual
' '
test_expect_success 'query binary macro directly' ' test_expect_success 'query binary macro directly' '
echo "file binary" >.gitattributes && echo "file binary" >.gitattributes &&
echo file: binary: set >expect && echo file: binary: set >expect &&
@ -339,4 +342,31 @@ test_expect_success 'query binary macro directly' '
test_cmp expect actual test_cmp expect actual
' '
test_expect_success SYMLINKS 'set up symlink tests' '
echo "* test" >attr &&
rm -f .gitattributes
'
test_expect_success SYMLINKS 'symlinks respected in core.attributesFile' '
test_when_finished "rm symlink" &&
ln -s attr symlink &&
test_config core.attributesFile "$(pwd)/symlink" &&
attr_check file set
'
test_expect_success SYMLINKS 'symlinks respected in info/attributes' '
test_when_finished "rm .git/info/attributes" &&
ln -s ../../attr .git/info/attributes &&
attr_check file set
'
test_expect_success SYMLINKS 'symlinks not respected in-tree' '
test_when_finished "rm -rf .gitattributes subdir" &&
ln -s attr .gitattributes &&
mkdir subdir &&
ln -s ../attr subdir/.gitattributes &&
attr_check_basic subdir/file unspecified &&
test_i18ngrep "unable to access.*gitattributes" err
'
test_done test_done