Skip to content

Commit 8423fd8

Browse files
committed
verify_path: disallow symlinks in .gitmodules, etc
There are a few reasons it's not a good idea to make .git* files symlinks, including: 1. It won't be portable to systems without symlinks. 2. It may behave inconsistently, since Git internally may look at these files from the index or a tree without bothering to resolve any symbolic links. So it may work in some settings (where we read from the filesystem) but not in others). With some clever code, we could make (2) work. And some people may not care about (1) if they only work on one platform. But there are a few security reasons to simply disallow symlinked meta-files: a. A symlinked .gitmodules file may circumvent any fsck checks of the content. b. Git may read and write from the on-disk file without sanity checking the symlink target. So for example, if you link ".gitmodules" to "../oops" and run "git submodule add", we'll write to the file "oops" outside the repository. Again, both of those are problems that _could_ be solved with sufficient code, but given the current inconsistent behavior and unportability, we're better off just outlawing it explicitly. We'll give the same treatment to .gitmodules, .gitignore, and .gitattributes. The latter two cannot be used to write outside the repository (we write them only as part of a checkout, where we are careful not to follow any symlinks). But they can still cause a "git clone && git log" combination to read arbitrary files outside the filesystem. There's _probably_ nothing too harmful you can do with that, but it seems questionable (and anyway, they suffer from the same portability and consistency problems). Note the slightly tricky call to verify_path() in update-index's update_one(). There we may not have a mode if we're not updating from the filesystem (e.g., we might just be removing the file). Passing "0" as the mode there works fine; since it's not a symlink, we'll just skip the extra checks. Signed-off-by: Jeff King <[email protected]>
1 parent 3f97adc commit 8423fd8

File tree

4 files changed

+43
-15
lines changed

4 files changed

+43
-15
lines changed

apply.c

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3867,9 +3867,9 @@ static int check_unsafe_path(struct patch *patch)
38673867
if (!patch->is_delete)
38683868
new_name = patch->new_name;
38693869

3870-
if (old_name && !verify_path(old_name))
3870+
if (old_name && !verify_path(old_name, patch->old_mode))
38713871
return error(_("invalid path '%s'"), old_name);
3872-
if (new_name && !verify_path(new_name))
3872+
if (new_name && !verify_path(new_name, patch->new_mode))
38733873
return error(_("invalid path '%s'"), new_name);
38743874
return 0;
38753875
}

builtin/update-index.c

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -399,7 +399,7 @@ static int add_cacheinfo(unsigned int mode, const struct object_id *oid,
399399
int size, len, option;
400400
struct cache_entry *ce;
401401

402-
if (!verify_path(path))
402+
if (!verify_path(path, mode))
403403
return error("Invalid path '%s'", path);
404404

405405
len = strlen(path);
@@ -452,7 +452,7 @@ static void update_one(const char *path)
452452
stat_errno = errno;
453453
} /* else stat is valid */
454454

455-
if (!verify_path(path)) {
455+
if (!verify_path(path, st.st_mode)) {
456456
fprintf(stderr, "Ignoring path %s\n", path);
457457
return;
458458
}
@@ -543,7 +543,7 @@ static void read_index_info(int nul_term_line)
543543
path_name = uq.buf;
544544
}
545545

546-
if (!verify_path(path_name)) {
546+
if (!verify_path(path_name, mode)) {
547547
fprintf(stderr, "Ignoring path %s\n", path_name);
548548
continue;
549549
}

cache.h

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -598,7 +598,7 @@ extern int read_index_unmerged(struct index_state *);
598598
extern int write_locked_index(struct index_state *, struct lock_file *lock, unsigned flags);
599599
extern int discard_index(struct index_state *);
600600
extern int unmerged_index(const struct index_state *);
601-
extern int verify_path(const char *path);
601+
extern int verify_path(const char *path, unsigned mode);
602602
extern int strcmp_offset(const char *s1, const char *s2, size_t *first_change);
603603
extern int index_dir_exists(struct index_state *istate, const char *name, int namelen);
604604
extern void adjust_dirname_case(struct index_state *istate, char *name);

read-cache.c

Lines changed: 37 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -732,7 +732,7 @@ struct cache_entry *make_cache_entry(unsigned int mode,
732732
int size, len;
733733
struct cache_entry *ce, *ret;
734734

735-
if (!verify_path(path)) {
735+
if (!verify_path(path, mode)) {
736736
error("Invalid path '%s'", path);
737737
return NULL;
738738
}
@@ -796,7 +796,7 @@ int ce_same_name(const struct cache_entry *a, const struct cache_entry *b)
796796
* Also, we don't want double slashes or slashes at the
797797
* end that can make pathnames ambiguous.
798798
*/
799-
static int verify_dotfile(const char *rest)
799+
static int verify_dotfile(const char *rest, unsigned mode)
800800
{
801801
/*
802802
* The first character was '.', but that
@@ -814,6 +814,9 @@ static int verify_dotfile(const char *rest)
814814
* case-insensitively here, even if ignore_case is not set.
815815
* This outlaws ".GIT" everywhere out of an abundance of caution,
816816
* since there's really no good reason to allow it.
817+
*
818+
* Once we've seen ".git", we can also find ".gitmodules", etc (also
819+
* case-insensitively).
817820
*/
818821
case 'g':
819822
case 'G':
@@ -823,6 +826,14 @@ static int verify_dotfile(const char *rest)
823826
break;
824827
if (rest[3] == '\0' || is_dir_sep(rest[3]))
825828
return 0;
829+
if (S_ISLNK(mode)) {
830+
rest += 3;
831+
if ((skip_iprefix(rest, "modules", &rest) ||
832+
skip_iprefix(rest, "ignore", &rest) ||
833+
skip_iprefix(rest, "attributes", &rest)) &&
834+
(*rest == '\0' || is_dir_sep(*rest)))
835+
return 0;
836+
}
826837
break;
827838
case '.':
828839
if (rest[1] == '\0' || is_dir_sep(rest[1]))
@@ -831,7 +842,7 @@ static int verify_dotfile(const char *rest)
831842
return 1;
832843
}
833844

834-
int verify_path(const char *path)
845+
int verify_path(const char *path, unsigned mode)
835846
{
836847
char c;
837848

@@ -844,12 +855,29 @@ int verify_path(const char *path)
844855
return 1;
845856
if (is_dir_sep(c)) {
846857
inside:
847-
if (protect_hfs && is_hfs_dotgit(path))
848-
return 0;
849-
if (protect_ntfs && is_ntfs_dotgit(path))
850-
return 0;
858+
if (protect_hfs) {
859+
if (is_hfs_dotgit(path))
860+
return 0;
861+
if (S_ISLNK(mode)) {
862+
if (is_hfs_dotgitmodules(path) ||
863+
is_hfs_dotgitignore(path) ||
864+
is_hfs_dotgitattributes(path))
865+
return 0;
866+
}
867+
}
868+
if (protect_ntfs) {
869+
if (is_ntfs_dotgit(path))
870+
return 0;
871+
if (S_ISLNK(mode)) {
872+
if (is_ntfs_dotgitmodules(path) ||
873+
is_ntfs_dotgitignore(path) ||
874+
is_ntfs_dotgitattributes(path))
875+
return 0;
876+
}
877+
}
878+
851879
c = *path++;
852-
if ((c == '.' && !verify_dotfile(path)) ||
880+
if ((c == '.' && !verify_dotfile(path, mode)) ||
853881
is_dir_sep(c) || c == '\0')
854882
return 0;
855883
}
@@ -1166,7 +1194,7 @@ static int add_index_entry_with_check(struct index_state *istate, struct cache_e
11661194

11671195
if (!ok_to_add)
11681196
return -1;
1169-
if (!verify_path(ce->name))
1197+
if (!verify_path(ce->name, ce->ce_mode))
11701198
return error("Invalid path '%s'", ce->name);
11711199

11721200
if (!skip_df_check &&

0 commit comments

Comments
 (0)