git/list[1] front-page[2] threads[3] people[4] search[5] about
 

[PATCH 4/6] attr: do not respect symlinks for in-tree .gitattributes

From
Jeff King <peff@peff.net>
Date
Feb 16, 2021, 14:44 UTC
Message-ID
<YCvaUHnf6eFll++h@coredump.intra.peff.net>
In-Reply-To
<YCvaJg8o882IqNnx@coredump.intra.peff.net>

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>
---
 attr.c                | 19 +++++++++++++++----
 t/t0003-attributes.sh | 36 +++++++++++++++++++++++++++++++++---
 2 files changed, 48 insertions(+), 7 deletions(-)
diff --git a/attr.c b/attr.c
index 7b0cab085f..a28177915b 100644
--- a/attr.c
+++ b/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. */
 #define READ_ATTR_MACRO_OK (1<<0)
+#define READ_ATTR_NOFOLLOW (1<<1)
 
 /*
  * 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)
 {
-	FILE *fp = fopen_or_warn(path, "r");
+	int fd;
+	FILE *fp;
 	struct attr_stack *res;
 	char buf[2048];
 	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;
+	}
+	fp = xfdopen(fd, "r");
+
 	res = xcalloc(1, sizeof(*res));
 	while (fgets(buf, sizeof(buf), fp)) {
 		char *bufp = buf;
@@ -870,7 +881,7 @@ static void bootstrap_attr_stack(const struct index_state *istate,
 	}
 
 	/* 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);
 
 	/* 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_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" */
 		strbuf_setlen(&pathbuf, len);
diff --git a/t/t0003-attributes.sh b/t/t0003-attributes.sh
index b660593c20..1e4c672b84 100755
--- a/t/t0003-attributes.sh
+++ b/t/t0003-attributes.sh
@@ -4,12 +4,16 @@ test_description=gitattributes
 
 . ./test-lib.sh
 
-attr_check () {
+attr_check_basic () {
 	path="$1" expect="$2" git_opts="$3" &&
 
 	git $git_opts check-attr test -- "$path" >actual 2>err &&
 	echo "$path: test: $expect" >expect &&
-	test_cmp expect actual &&
+	test_cmp expect actual
+}
+
+attr_check () {
+	attr_check_basic "$@" &&
 	test_must_be_empty err
 }
 
@@ -331,12 +335,38 @@ test_expect_success 'binary macro expanded by -a' '
 	test_cmp expect actual
 '
 
-
 test_expect_success 'query binary macro directly' '
 	echo "file binary" >.gitattributes &&
 	echo file: binary: set >expect &&
 	git check-attr binary file >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
-- 
2.30.1.986.gd86016a168
Previous: Jeff KingNext: Jeff King
Message 18 of 25 in “Limited local file inclusion with .mailmap symlinks and git-archive”
  1. Blake BurkhartFeb 13, 2021
  2. Jeff KingFeb 15, 2021
  3. 1/2 fsck: make symlinked .gitignore and .gitattributes a warningJeff King, Feb 15, 2021
  4. Ævar Arnfjörð BjarmasonFeb 16, 2021
  5. Jeff KingFeb 16, 2021
  6. Junio C HamanoFeb 16, 2021
  7. Jeff KingFeb 16, 2021
  8. Jeff KingFeb 16, 2021
  9. 0/6 open in-tree files with O_NOFOLLOWJeff King, Feb 16, 2021
  10. 1/6 add open_nofollow() helperJeff King, Feb 16, 2021
  11. Jeff KingFeb 16, 2021
  12. Taylor BlauFeb 16, 2021
  13. Jeff KingFeb 16, 2021
  14. Taylor BlauFeb 16, 2021
  15. Taylor BlauFeb 16, 2021
  16. Jeff KingFeb 16, 2021
  17. 2/6 attr: convert "macro_ok" into a flags fieldJeff King, Feb 16, 2021
  18. 4/6 attr: do not respect symlinks for in-tree .gitattributesJeff King, Feb 16, 2021
  19. 5/6 exclude: do not respect symlinks for in-tree .gitignoreJeff King, Feb 16, 2021
  20. 3/6 exclude: add flags parameter to add_patterns()Jeff King, Feb 16, 2021
  21. 6/6 mailmap: do not respect symlinks for in-tree .mailmapJeff King, Feb 16, 2021
  22. Jeff KingFeb 16, 2021
  23. Junio C HamanoFeb 25, 2021
  24. Jeff KingFeb 26, 2021
  25. 2/2 disallow symlinked .mailmap filesJeff King, Feb 15, 2021

Read the whole thread, see it on lore, or plain text.

$ cat FOOTERMessages come from the public archive at lore.kernel.org/git, fetched every hour. The front page is chosen and written each morning by an AI editor and can be wrong; the threads themselves are the record. About and API. For agents: an MCP server at https://gitlist.dev/mcp, and any thread, story or person page as Markdown by adding .md to its URL (or sending Accept: text/markdown). Details in /llms.txt.