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

[PATCH] setup: sanitize absolute and funny paths in get_pathspec()

From
Junio C Hamano <gitster@pobox.com>
Date
Jan 29, 2008, 08:29 UTC
Message-ID
<7vve5dox0o.fsf_-_@gitster.siamese.dyndns.org>
In-Reply-To
<7v7ihtqfm8.fsf@gitster.siamese.dyndns.org>

The prefix_path() function called from get_pathspec() is responsible for translating list of user-supplied pathspecs to list of pathspecs that is relative to the root of the work tree. When working inside a subdirectory, the user-supplied pathspecs are taken to be relative to the current subdirectory.

Among special path components in pathspecs, we used to accept and interpret only "." ("the directory", meaning a no-op) and ".." ("up one level") at the beginning. Everything else was passed through as-is.

For example, if you are in Documentation/ directory of the project, you can name Documentation/howto/maintain-git.txt as:

    howto/maintain-git.txt
    ../Documentation/howto/maitain-git.txt
    ../././Documentation/howto/maitain-git.txt
but not as:
    howto/./maintain-git.txt
    $(pwd)/howto/maintain-git.txt
This patch updates prefix_path() in several ways:
 - If the pathspec is not absolute, prefix (i.e. the current
   subdirectory relative to the root of the work tree, with
   terminating slash, if not empty) and the pathspec is
   concatenated first and used in the next step.  Otherwise,
   that absolute pathspec is used in the next step.
 - Then special path components "." (no-op) and ".." (up one
   level) are interpreted to simplify the path.  It is an error
   to have too many ".." to cause the intermediate result to
   step outside of the input to this step.
 - If the original pathspec was not absolute, the result from
   the previous step is the resulting "sanitized" pathspec.
   Otherwise, the result from the previous step is still
   absolute, and it is an error if it does not begin with the
   directory that corresponds to the root of the work tree.  The
   directory is stripped away from the result and is returned.
 - In any case, the resulting pathspec in the array
   get_pathspec() returns omit the ones that caused errors.
With this patch, the last two examples also behave as expected.
Signed-off-by: Junio C Hamano <gitster@pobox.com>
---
 * This is a squash of the handful patches I sent out with some
   clean-ups, to conclude the day for me.  Perhaps this will
   appear in 'pu' before 1.5.4 if I really am bored but not very
   likely.  I am pretty much bogged down with day-job these days
   during the week...
 builtin-ls-files.c |   11 +++-
 builtin-mv.c       |    4 +-
 setup.c            |  158 ++++++++++++++++++++++++++++++++++++++--------------
 t/t7010-setup.sh   |  117 ++++++++++++++++++++++++++++++++++++++
 4 files changed, 245 insertions(+), 45 deletions(-)
 create mode 100755 t/t7010-setup.sh
diff --git a/builtin-ls-files.c b/builtin-ls-files.c
index 0f0ab2d..3801cf4 100644
--- a/builtin-ls-files.c
+++ b/builtin-ls-files.c
@@ -572,8 +572,17 @@ int cmd_ls_files(int argc, const char **argv, const char *prefix)
 	pathspec = get_pathspec(prefix, argv + i);
 
 	/* Verify that the pathspec matches the prefix */
-	if (pathspec)
+	if (pathspec) {
+		if (argc != i) {
+			int cnt;
+			for (cnt = 0; pathspec[cnt]; cnt++)
+				;
+			if (cnt != (argc - i))
+				exit(1); /* error message already given */
+		}
 		prefix = verify_pathspec(prefix);
+	} else if (argc != i)
+		exit(1); /* error message already given */
 
 	/* Treat unmatching pathspec elements as errors */
 	if (pathspec && error_unmatch) {
diff --git a/builtin-mv.c b/builtin-mv.c
index 990e213..94f6dd2 100644
--- a/builtin-mv.c
+++ b/builtin-mv.c
@@ -164,7 +164,7 @@ int cmd_mv(int argc, const char **argv, const char *prefix)
 				}
 
 				dst = add_slash(dst);
-				dst_len = strlen(dst) - 1;
+				dst_len = strlen(dst);
 
 				for (j = 0; j < last - first; j++) {
 					const char *path =
@@ -172,7 +172,7 @@ int cmd_mv(int argc, const char **argv, const char *prefix)
 					source[argc + j] = path;
 					destination[argc + j] =
 						prefix_path(dst, dst_len,
-							path + length);
+							path + length + 1);
 					modes[argc + j] = INDEX;
 				}
 				argc += last - first;
diff --git a/setup.c b/setup.c
index adede16..23c9a11 100644
--- a/setup.c
+++ b/setup.c
@@ -4,51 +4,118 @@
 static int inside_git_dir = -1;
 static int inside_work_tree = -1;
 
-const char *prefix_path(const char *prefix, int len, const char *path)
+static int sanitary_path_copy(char *dst, const char *src)
 {
-	const char *orig = path;
+	char *dst0 = dst;
+
+	if (*src == '/') {
+		*dst++ = '/';
+		while (*src == '/')
+			src++;
+	}
+
 	for (;;) {
-		char c;
-		if (*path != '.')
-			break;
-		c = path[1];
-		/* "." */
-		if (!c) {
-			path++;
-			break;
+		char c = *src;
+
+		/*
+		 * A path component that begins with . could be
+		 * special:
+		 * (1) "." and ends   -- ignore and terminate.
+		 * (2) "./"           -- ignore them, eat slash and continue.
+		 * (3) ".." and ends  -- strip one and terminate.
+		 * (4) "../"          -- strip one, eat slash and continue.
+		 */
+		if (c == '.') {
+			switch (src[1]) {
+			case '\0':
+				/* (1) */
+				src++;
+				break;
+			case '/':
+				/* (2) */
+				src += 2;
+				while (*src == '/')
+					src++;
+				continue;
+			case '.':
+				switch (src[2]) {
+				case '\0':
+					/* (3) */
+					src += 2;
+					goto up_one;
+				case '/':
+					/* (4) */
+					src += 3;
+					while (*src == '/')
+						src++;
+					goto up_one;
+				}
+			}
 		}
-		/* "./" */
+
+		/* copy up to the next '/', and eat all '/' */
+		while ((c = *src++) != '\0' && c != '/')
+			*dst++ = c;
 		if (c == '/') {
-			path += 2;
-			continue;
-		}
-		if (c != '.')
+			*dst++ = c;
+			while (c == '/')
+				c = *src++;
+			src--;
+		} else if (!c)
 			break;
-		c = path[2];
-		if (!c)
-			path += 2;
-		else if (c == '/')
-			path += 3;
-		else
-			break;
-		/* ".." and "../" */
-		/* Remove last component of the prefix */
-		do {
-			if (!len)
-				die("'%s' is outside repository", orig);
-			len--;
-		} while (len && prefix[len-1] != '/');
 		continue;
+
+	up_one:
+		/*
+		 * dst0..dst is prefix portion, and dst[-1] is '/';
+		 * go up one level.
+		 */
+		dst -= 2; /* go past trailing '/' if any */
+		if (dst < dst0)
+			return -1;
+		while (1) {
+			if (dst <= dst0)
+				break;
+			c = *dst--;
+			if (c == '/') {
+				dst += 2;
+				break;
+			}
+		}
 	}
-	if (len) {
-		int speclen = strlen(path);
-		char *n = xmalloc(speclen + len + 1);
+	*dst = '\0';
+	return 0;
+}
 
-		memcpy(n, prefix, len);
-		memcpy(n + len, path, speclen+1);
-		path = n;
+const char *prefix_path(const char *prefix, int len, const char *path)
+{
+	const char *orig = path;
+	char *sanitized = xmalloc(len + strlen(path) + 1);
+	if (*orig == '/')
+		strcpy(sanitized, path);
+	else {
+		if (len)
+			memcpy(sanitized, prefix, len);
+		strcpy(sanitized + len, path);
 	}
-	return path;
+	if (sanitary_path_copy(sanitized, sanitized))
+		goto error_out;
+	if (*orig == '/') {
+		const char *work_tree = get_git_work_tree();
+		size_t len = strlen(work_tree);
+		size_t total = strlen(sanitized) + 1;
+		if (strncmp(sanitized, work_tree, len) ||
+		    (sanitized[len] != '\0' && sanitized[len] != '/')) {
+		error_out:
+			error("'%s' is outside repository", orig);
+			free(sanitized);
+			return NULL;
+		}
+		if (sanitized[len] == '/')
+			len++;
+		memmove(sanitized, sanitized + len, total - len);
+	}
+	return sanitized;
 }
 
 /*
@@ -114,7 +181,7 @@ void verify_non_filename(const char *prefix, const char *arg)
 const char **get_pathspec(const char *prefix, const char **pathspec)
 {
 	const char *entry = *pathspec;
-	const char **p;
+	const char **src, **dst;
 	int prefixlen;
 
 	if (!prefix && !entry)
@@ -128,12 +195,19 @@ const char **get_pathspec(const char *prefix, const char **pathspec)
 	}
 
 	/* Otherwise we have to re-write the entries.. */
-	p = pathspec;
+	src = pathspec;
+	dst = pathspec;
 	prefixlen = prefix ? strlen(prefix) : 0;
-	do {
-		*p = prefix_path(prefix, prefixlen, entry);
-	} while ((entry = *++p) != NULL);
-	return (const char **) pathspec;
+	while (*src) {
+		const char *p = prefix_path(prefix, prefixlen, *src);
+		if (p)
+			*(dst++) = p;
+		src++;
+	}
+	*dst = NULL;
+	if (!*pathspec)
+		return NULL;
+	return pathspec;
 }
 
 /*
diff --git a/t/t7010-setup.sh b/t/t7010-setup.sh
new file mode 100755
index 0000000..da20ba5
--- /dev/null
+++ b/t/t7010-setup.sh
@@ -0,0 +1,117 @@
+#!/bin/sh
+
+test_description='setup taking and sanitizing funny paths'
+
+. ./test-lib.sh
+
+test_expect_success setup '
+
+	mkdir -p a/b/c a/e &&
+	D=$(pwd) &&
+	>a/b/c/d &&
+	>a/e/f
+
+'
+
+test_expect_success 'git add (absolute)' '
+
+	git add "$D/a/b/c/d" &&
+	git ls-files >current &&
+	echo a/b/c/d >expect &&
+	diff -u expect current
+
+'
+
+
+test_expect_success 'git add (funny relative)' '
+
+	rm -f .git/index &&
+	(
+		cd a/b &&
+		git add "../e/./f"
+	) &&
+	git ls-files >current &&
+	echo a/e/f >expect &&
+	diff -u expect current
+
+'
+
+test_expect_success 'git rm (absolute)' '
+
+	rm -f .git/index &&
+	git add a &&
+	git rm -f --cached "$D/a/b/c/d" &&
+	git ls-files >current &&
+	echo a/e/f >expect &&
+	diff -u expect current
+
+'
+
+test_expect_success 'git rm (funny relative)' '
+
+	rm -f .git/index &&
+	git add a &&
+	(
+		cd a/b &&
+		git rm -f --cached "../e/./f"
+	) &&
+	git ls-files >current &&
+	echo a/b/c/d >expect &&
+	diff -u expect current
+
+'
+
+test_expect_success 'git ls-files (absolute)' '
+
+	rm -f .git/index &&
+	git add a &&
+	git ls-files "$D/a/e/../b" >current &&
+	echo a/b/c/d >expect &&
+	diff -u expect current
+
+'
+
+test_expect_success 'git ls-files (relative #1)' '
+
+	rm -f .git/index &&
+	git add a &&
+	(
+		cd a/b &&
+		git ls-files "../b/c"
+	)  >current &&
+	echo c/d >expect &&
+	diff -u expect current
+
+'
+
+test_expect_success 'git ls-files (relative #2)' '
+
+	rm -f .git/index &&
+	git add a &&
+	(
+		cd a/b &&
+		git ls-files --full-name "../e/f"
+	)  >current &&
+	echo a/e/f >expect &&
+	diff -u expect current
+
+'
+
+test_expect_success 'git ls-files (relative #3)' '
+
+	rm -f .git/index &&
+	git add a &&
+	(
+		cd a/b &&
+		if git ls-files "../e/f"
+		then
+			echo Gaah, should have failed
+			exit 1
+		else
+			: happy
+		fi
+	)
+
+'
+
+test_done
-- 
1.5.4.rc5
Previous: Junio C HamanoNext: Robin Rosenberg
Message 24 of 47 in “git-clean buglet”
  1. Johannes SixtJan 23, 2008
  2. Johannes SixtJan 23, 2008
  3. Johannes SchindelinJan 23, 2008
  4. Johannes SixtJan 23, 2008
  5. Fix off by one error in prep_exclude.Shawn Bohrer, Jan 27, 2008
  6. Johannes SchindelinJan 27, 2008
  7. Shawn BohrerJan 27, 2008
  8. Junio C HamanoJan 27, 2008
  9. Shawn BohrerJan 28, 2008
  10. Fix off by one error in prep_exclude.Shawn Bohrer, Jan 28, 2008
  11. Johannes SchindelinJan 28, 2008
  12. Junio C HamanoJan 28, 2008
  13. Junio C HamanoJan 28, 2008
  14. Johannes SixtJan 28, 2008
  15. Junio C HamanoJan 28, 2008
  16. Johannes SixtJan 28, 2008
  17. Junio C HamanoJan 28, 2008
  18. prefix_path(): disallow absolute pathsJohannes Schindelin, Jan 28, 2008
  19. prefix_path(): disallow absolute pathsJohannes Schindelin, Jan 28, 2008
  20. Junio C HamanoJan 29, 2008
  21. Junio C HamanoJan 29, 2008
  22. Junio C HamanoJan 29, 2008
  23. Junio C HamanoJan 29, 2008
  24. setup: sanitize absolute and funny paths in get_pathspec()Junio C Hamano, Jan 29, 2008
  25. Make blame accept absolute pathsRobin Rosenberg, Feb 1, 2008
  26. More test cases for sanitized path namesRobin Rosenberg, Feb 1, 2008
  27. Junio C HamanoFeb 1, 2008
  28. Robin RosenbergFeb 1, 2008
  29. Junio C HamanoFeb 1, 2008
  30. Junio C HamanoFeb 1, 2008
  31. Junio C HamanoFeb 1, 2008
  32. Robin RosenbergFeb 1, 2008
  33. Junio C HamanoFeb 1, 2008
  34. Karl HasselströmFeb 1, 2008
  35. Sane use of test_expect_failureJunio C Hamano, Feb 1, 2008
  36. Junio C HamanoFeb 2, 2008
  37. Junio C HamanoMar 7, 2008
  38. Robin RosenbergMar 7, 2008
  39. Johannes SchindelinJan 29, 2008
  40. Junio C HamanoJan 29, 2008
  41. Johannes SchindelinJan 29, 2008
  42. Johannes SixtJan 29, 2008
  43. Junio C HamanoJan 29, 2008
  44. Johannes SixtJan 29, 2008
  45. Junio C HamanoJan 29, 2008
  46. しらいしななこJan 29, 2008
  47. Junio C HamanoJan 30, 2008

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.