diff --git a/Documentation/RelNotes/2.30.3.txt b/Documentation/RelNotes/2.30.3.txt new file mode 100644 index 0000000000..31b2a4daa6 --- /dev/null +++ b/Documentation/RelNotes/2.30.3.txt @@ -0,0 +1,24 @@ +Git v2.30.2 Release Notes +========================= + +This release addresses the security issue CVE-2022-24765. + +Fixes since v2.30.2 +------------------- + + * Build fix on Windows. + + * Fix `GIT_CEILING_DIRECTORIES` with Windows-style root directories. + + * CVE-2022-24765: + On multi-user machines, Git users might find themselves + unexpectedly in a Git worktree, e.g. when another user created a + repository in `C:\.git`, in a mounted network drive or in a + scratch space. Merely having a Git-aware prompt that runs `git + status` (or `git diff`) and navigating to a directory which is + supposedly not a Git worktree, or opening such a directory in an + editor or IDE such as VS Code or Atom, will potentially run + commands defined by that other user. + +Credit for finding this vulnerability goes to 俞晨东; The fix was +authored by Johannes Schindelin. diff --git a/Documentation/config.txt b/Documentation/config.txt index bf82766a6a..a1efd744cd 100644 --- a/Documentation/config.txt +++ b/Documentation/config.txt @@ -440,6 +440,8 @@ include::config/rerere.txt[] include::config/reset.txt[] +include::config/safe.txt[] + include::config/sendemail.txt[] include::config/sequencer.txt[] diff --git a/Documentation/config/safe.txt b/Documentation/config/safe.txt new file mode 100644 index 0000000000..63597b2df8 --- /dev/null +++ b/Documentation/config/safe.txt @@ -0,0 +1,21 @@ +safe.directory:: + These config entries specify Git-tracked directories that are + considered safe even if they are owned by someone other than the + current user. By default, Git will refuse to even parse a Git + config of a repository owned by someone else, let alone run its + hooks, and this config setting allows users to specify exceptions, + e.g. for intentionally shared repositories (see the `--shared` + option in linkgit:git-init[1]). ++ +This is a multi-valued setting, i.e. you can add more than one directory +via `git config --add`. To reset the list of safe directories (e.g. to +override any such directories specified in the system config), add a +`safe.directory` entry with an empty value. ++ +This config setting is only respected when specified in a system or global +config, not when it is specified in a repository config or via the command +line option `-c safe.directory=`. ++ +The value of this setting is interpolated, i.e. `~/` expands to a +path relative to the home directory and `%(prefix)/` expands to a +path relative to Git's (runtime) prefix. diff --git a/compat/mingw.c b/compat/mingw.c index a43599841c..38ac35913d 100644 --- a/compat/mingw.c +++ b/compat/mingw.c @@ -1,5 +1,6 @@ #include "../git-compat-util.h" #include "win32.h" +#include #include #include #include "../strbuf.h" @@ -1060,6 +1061,7 @@ int pipe(int filedes[2]) return 0; } +#ifndef __MINGW64__ struct tm *gmtime_r(const time_t *timep, struct tm *result) { if (gmtime_s(result, timep) == 0) @@ -1073,6 +1075,7 @@ struct tm *localtime_r(const time_t *timep, struct tm *result) return result; return NULL; } +#endif char *mingw_getcwd(char *pointer, int len) { @@ -2599,6 +2602,92 @@ static void setup_windows_environment(void) } } +static PSID get_current_user_sid(void) +{ + HANDLE token; + DWORD len = 0; + PSID result = NULL; + + if (!OpenProcessToken(GetCurrentProcess(), TOKEN_QUERY, &token)) + return NULL; + + if (!GetTokenInformation(token, TokenUser, NULL, 0, &len)) { + TOKEN_USER *info = xmalloc((size_t)len); + if (GetTokenInformation(token, TokenUser, info, len, &len)) { + len = GetLengthSid(info->User.Sid); + result = xmalloc(len); + if (!CopySid(len, result, info->User.Sid)) { + error(_("failed to copy SID (%ld)"), + GetLastError()); + FREE_AND_NULL(result); + } + } + FREE_AND_NULL(info); + } + CloseHandle(token); + + return result; +} + +int is_path_owned_by_current_sid(const char *path) +{ + WCHAR wpath[MAX_PATH]; + PSID sid = NULL; + PSECURITY_DESCRIPTOR descriptor = NULL; + DWORD err; + + static wchar_t home[MAX_PATH]; + + int result = 0; + + if (xutftowcs_path(wpath, path) < 0) + return 0; + + /* + * On Windows, the home directory is owned by the administrator, but for + * all practical purposes, it belongs to the user. Do pretend that it is + * owned by the user. + */ + if (!*home) { + DWORD size = ARRAY_SIZE(home); + DWORD len = GetEnvironmentVariableW(L"HOME", home, size); + if (!len || len > size) + wcscpy(home, L"::N/A::"); + } + if (!wcsicmp(wpath, home)) + return 1; + + /* Get the owner SID */ + err = GetNamedSecurityInfoW(wpath, SE_FILE_OBJECT, + OWNER_SECURITY_INFORMATION | + DACL_SECURITY_INFORMATION, + &sid, NULL, NULL, NULL, &descriptor); + + if (err != ERROR_SUCCESS) + error(_("failed to get owner for '%s' (%ld)"), path, err); + else if (sid && IsValidSid(sid)) { + /* Now, verify that the SID matches the current user's */ + static PSID current_user_sid; + + if (!current_user_sid) + current_user_sid = get_current_user_sid(); + + if (current_user_sid && + IsValidSid(current_user_sid) && + EqualSid(sid, current_user_sid)) + result = 1; + } + + /* + * We can release the security descriptor struct only now because `sid` + * actually points into this struct. + */ + if (descriptor) + LocalFree(descriptor); + + return result; +} + int is_valid_win32_path(const char *path, int allow_literal_nul) { const char *p = path; diff --git a/compat/mingw.h b/compat/mingw.h index c9a52ad64a..ffa53a44b0 100644 --- a/compat/mingw.h +++ b/compat/mingw.h @@ -453,6 +453,13 @@ char *mingw_query_user_email(void); #include #endif +/** + * Verifies that the specified path is owned by the user running the + * current process. + */ +int is_path_owned_by_current_sid(const char *path); +#define is_path_owned_by_current_user is_path_owned_by_current_sid + /** * Verifies that the given path is a valid one on Windows. * diff --git a/git-compat-util.h b/git-compat-util.h index dcc786edaa..261516231e 100644 --- a/git-compat-util.h +++ b/git-compat-util.h @@ -127,7 +127,9 @@ /* Approximation of the length of the decimal representation of this type. */ #define decimal_length(x) ((int)(sizeof(x) * 2.56 + 0.5) + 1) -#if defined(__sun__) +#ifdef __MINGW64__ +#define _POSIX_C_SOURCE 1 +#elif defined(__sun__) /* * On Solaris, when _XOPEN_EXTENDED is set, its header file * forces the programs to be XPG4v2, defeating any _XOPEN_SOURCE @@ -390,6 +392,18 @@ static inline int git_offset_1st_component(const char *path) #define is_valid_path(path) 1 #endif +#ifndef is_path_owned_by_current_user +static inline int is_path_owned_by_current_uid(const char *path) +{ + struct stat st; + if (lstat(path, &st)) + return 0; + return st.st_uid == geteuid(); +} + +#define is_path_owned_by_current_user is_path_owned_by_current_uid +#endif + #ifndef find_last_dir_sep static inline char *git_find_last_dir_sep(const char *path) { diff --git a/path.c b/path.c index 7b385e5eb2..853e7165c8 100644 --- a/path.c +++ b/path.c @@ -1218,11 +1218,15 @@ int longest_ancestor_length(const char *path, struct string_list *prefixes) const char *ceil = prefixes->items[i].string; int len = strlen(ceil); - if (len == 1 && ceil[0] == '/') - len = 0; /* root matches anything, with length 0 */ - else if (!strncmp(path, ceil, len) && path[len] == '/') - ; /* match of length len */ - else + /* + * For root directories (`/`, `C:/`, `//server/share/`) + * adjust the length to exclude the trailing slash. + */ + if (len > 0 && ceil[len - 1] == '/') + len--; + + if (strncmp(path, ceil, len) || + path[len] != '/' || !path[len + 1]) continue; /* no match */ if (len > max_len) diff --git a/setup.c b/setup.c index c04cd25a30..95d5b00940 100644 --- a/setup.c +++ b/setup.c @@ -5,6 +5,7 @@ #include "string-list.h" #include "chdir-notify.h" #include "promisor-remote.h" +#include "quote.h" static int inside_git_dir = -1; static int inside_work_tree = -1; @@ -1024,6 +1025,42 @@ static int canonicalize_ceiling_entry(struct string_list_item *item, } } +struct safe_directory_data { + const char *path; + int is_safe; +}; + +static int safe_directory_cb(const char *key, const char *value, void *d) +{ + struct safe_directory_data *data = d; + + if (!value || !*value) + data->is_safe = 0; + else { + const char *interpolated = NULL; + + if (!git_config_pathname(&interpolated, key, value) && + !fspathcmp(data->path, interpolated ? interpolated : value)) + data->is_safe = 1; + + free((char *)interpolated); + } + + return 0; +} + +static int ensure_valid_ownership(const char *path) +{ + struct safe_directory_data data = { .path = path }; + + if (is_path_owned_by_current_user(path)) + return 1; + + read_very_early_config(safe_directory_cb, &data); + + return data.is_safe; +} + enum discovery_result { GIT_DIR_NONE = 0, GIT_DIR_EXPLICIT, @@ -1032,7 +1069,8 @@ enum discovery_result { /* these are errors */ GIT_DIR_HIT_CEILING = -1, GIT_DIR_HIT_MOUNT_POINT = -2, - GIT_DIR_INVALID_GITFILE = -3 + GIT_DIR_INVALID_GITFILE = -3, + GIT_DIR_INVALID_OWNERSHIP = -4 }; /* @@ -1122,11 +1160,15 @@ static enum discovery_result setup_git_directory_gently_1(struct strbuf *dir, } strbuf_setlen(dir, offset); if (gitdirenv) { + if (!ensure_valid_ownership(dir->buf)) + return GIT_DIR_INVALID_OWNERSHIP; strbuf_addstr(gitdir, gitdirenv); return GIT_DIR_DISCOVERED; } if (is_git_directory(dir->buf)) { + if (!ensure_valid_ownership(dir->buf)) + return GIT_DIR_INVALID_OWNERSHIP; strbuf_addstr(gitdir, "."); return GIT_DIR_BARE; } @@ -1253,6 +1295,19 @@ const char *setup_git_directory_gently(int *nongit_ok) dir.buf); *nongit_ok = 1; break; + case GIT_DIR_INVALID_OWNERSHIP: + if (!nongit_ok) { + struct strbuf quoted = STRBUF_INIT; + + sq_quote_buf_pretty("ed, dir.buf); + die(_("unsafe repository ('%s' is owned by someone else)\n" + "To add an exception for this directory, call:\n" + "\n" + "\tgit config --global --add safe.directory %s"), + dir.buf, quoted.buf); + } + *nongit_ok = 1; + break; case GIT_DIR_NONE: /* * As a safeguard against setup_git_directory_gently_1 returning diff --git a/t/t0060-path-utils.sh b/t/t0060-path-utils.sh index 0ff06b5d1b..a2a214f982 100755 --- a/t/t0060-path-utils.sh +++ b/t/t0060-path-utils.sh @@ -55,12 +55,15 @@ fi ancestor() { # We do some math with the expected ancestor length. expected=$3 - if test -n "$rootoff" && test "x$expected" != x-1; then - expected=$(($expected-$rootslash)) - test $expected -lt 0 || - expected=$(($expected+$rootoff)) - fi - test_expect_success "longest ancestor: $1 $2 => $expected" \ + case "$rootoff,$expected,$2" in + *,*,//*) ;; # leave UNC paths alone + [0-9]*,[0-9]*,/*) + # On Windows, expect MSYS2 pseudo root translation for + # Unix-style absolute paths + expected=$(($expected-$rootslash+$rootoff)) + ;; + esac + test_expect_success $4 "longest ancestor: $1 $2 => $expected" \ "actual=\$(test-tool path-utils longest_ancestor_length '$1' '$2') && test \"\$actual\" = '$expected'" } @@ -156,6 +159,11 @@ ancestor /foo/bar /foo 4 ancestor /foo/bar /foo:/bar 4 ancestor /foo/bar /bar -1 +# Windows-specific: DOS drives, network shares +ancestor C:/Users/me C:/ 2 MINGW +ancestor D:/Users/me C:/ -1 MINGW +ancestor //server/share/my-directory //server/share/ 14 MINGW + test_expect_success 'strip_path_suffix' ' test c:/msysgit = $(test-tool path-utils strip_path_suffix \ c:/msysgit/libexec//git-core libexec/git-core)