diff options
author | Edward Thomson <ethomson@edwardthomson.com> | 2022-04-12 14:11:54 -0400 |
---|---|---|
committer | GitHub <noreply@github.com> | 2022-04-12 14:11:54 -0400 |
commit | 63970244fb2d49794e7b5d268a2defe4299fd3ad (patch) | |
tree | e392dbbef54ade88914f7bf1550d2cf8340704a6 | |
parent | 7e8d9be0cc168c22cbaf1116bd5b27c0a6076565 (diff) | |
parent | 4161ebdd50dc29205dcc94cba5320c6d0d2537a7 (diff) | |
download | libgit2-63970244fb2d49794e7b5d268a2defe4299fd3ad.tar.gz |
Merge pull request #6266 from libgit2/ethomson/ownership
Validate repository directory ownership
-rw-r--r-- | include/git2/common.h | 12 | ||||
-rw-r--r-- | include/git2/errors.h | 3 | ||||
-rw-r--r-- | src/libgit2/config.c | 14 | ||||
-rw-r--r-- | src/libgit2/libgit2.c | 8 | ||||
-rw-r--r-- | src/libgit2/repository.c | 125 | ||||
-rw-r--r-- | src/libgit2/repository.h | 1 | ||||
-rw-r--r-- | src/util/fs_path.c | 267 | ||||
-rw-r--r-- | src/util/fs_path.h | 36 | ||||
-rw-r--r-- | tests/clar/clar_libgit2.c | 5 | ||||
-rw-r--r-- | tests/clar/clar_libgit2.h | 1 | ||||
-rw-r--r-- | tests/clar/main.c | 1 | ||||
-rw-r--r-- | tests/libgit2/repo/config.c | 1 | ||||
-rw-r--r-- | tests/libgit2/repo/open.c | 147 | ||||
-rw-r--r-- | tests/util/path.c | 25 |
14 files changed, 554 insertions, 92 deletions
diff --git a/include/git2/common.h b/include/git2/common.h index 52000e8eb..c3e3e7b4e 100644 --- a/include/git2/common.h +++ b/include/git2/common.h @@ -225,7 +225,9 @@ typedef enum { GIT_OPT_SET_ODB_PACKED_PRIORITY, GIT_OPT_SET_ODB_LOOSE_PRIORITY, GIT_OPT_GET_EXTENSIONS, - GIT_OPT_SET_EXTENSIONS + GIT_OPT_SET_EXTENSIONS, + GIT_OPT_GET_OWNER_VALIDATION, + GIT_OPT_SET_OWNER_VALIDATION } git_libgit2_opt_t; /** @@ -463,6 +465,14 @@ typedef enum { * > to support repositories with the `noop` extension but does want * > to support repositories with the `newext` extension. * + * opts(GIT_OPT_GET_OWNER_VALIDATION, int *enabled) + * > Gets the owner validation setting for repository + * > directories. + * + * opts(GIT_OPT_SET_OWNER_VALIDATION, int enabled) + * > Set that repository directories should be owned by the current + * > user. The default is to validate ownership. + * * @param option Option key * @param ... value to set the option * @return 0 on success, <0 on failure diff --git a/include/git2/errors.h b/include/git2/errors.h index aba6d75e3..a61964bbb 100644 --- a/include/git2/errors.h +++ b/include/git2/errors.h @@ -57,7 +57,8 @@ typedef enum { GIT_RETRY = -32, /**< Internal only */ GIT_EMISMATCH = -33, /**< Hashsum mismatch in object */ GIT_EINDEXDIRTY = -34, /**< Unsaved changes in the index would be overwritten */ - GIT_EAPPLYFAIL = -35 /**< Patch application failed */ + GIT_EAPPLYFAIL = -35, /**< Patch application failed */ + GIT_EOWNER = -36 /**< The object is not owned by the current user */ } git_error_code; /** diff --git a/src/libgit2/config.c b/src/libgit2/config.c index 88da34c5e..6bd59f2a5 100644 --- a/src/libgit2/config.c +++ b/src/libgit2/config.c @@ -1170,14 +1170,18 @@ int git_config_find_programdata(git_buf *path) int git_config__find_programdata(git_str *path) { - int ret; + bool is_safe; - ret = git_sysdir_find_programdata_file(path, GIT_CONFIG_FILENAME_PROGRAMDATA); + if (git_sysdir_find_programdata_file(path, GIT_CONFIG_FILENAME_PROGRAMDATA) < 0 || + git_fs_path_owner_is_system_or_current_user(&is_safe, path->ptr) < 0) + return -1; - if (ret != GIT_OK) - return ret; + if (!is_safe) { + git_error_set(GIT_ERROR_CONFIG, "programdata path has invalid ownership"); + return -1; + } - return git_fs_path_validate_system_file_ownership(path->ptr); + return 0; } int git_config__global_location(git_str *buf) diff --git a/src/libgit2/libgit2.c b/src/libgit2/libgit2.c index efad3bf6d..2fda0722e 100644 --- a/src/libgit2/libgit2.c +++ b/src/libgit2/libgit2.c @@ -406,6 +406,14 @@ int git_libgit2_opts(int key, ...) } break; + case GIT_OPT_GET_OWNER_VALIDATION: + *(va_arg(ap, int *)) = git_repository__validate_ownership; + break; + + case GIT_OPT_SET_OWNER_VALIDATION: + git_repository__validate_ownership = (va_arg(ap, int) != 0); + break; + default: git_error_set(GIT_ERROR_INVALID, "invalid option key"); error = -1; diff --git a/src/libgit2/repository.c b/src/libgit2/repository.c index f202623d6..48a0b70f5 100644 --- a/src/libgit2/repository.c +++ b/src/libgit2/repository.c @@ -39,6 +39,7 @@ # include "win32/w32_util.h" #endif +bool git_repository__validate_ownership = true; bool git_repository__fsync_gitdir = false; static const struct { @@ -65,6 +66,7 @@ static const struct { static int check_repositoryformatversion(int *version, git_config *config); static int check_extensions(git_config *config, int version); +static int load_global_config(git_config **config); #define GIT_COMMONDIR_FILE "commondir" #define GIT_GITDIR_FILE "gitdir" @@ -483,6 +485,63 @@ static int read_gitfile(git_str *path_out, const char *file_path) return error; } +typedef struct { + const char *repo_path; + git_str tmp; + bool is_safe; +} validate_ownership_data; + +static int validate_ownership_cb(const git_config_entry *entry, void *payload) +{ + validate_ownership_data *data = payload; + + if (strcmp(entry->value, "") == 0) + data->is_safe = false; + + if (git_fs_path_prettify_dir(&data->tmp, entry->value, NULL) == 0 && + strcmp(data->tmp.ptr, data->repo_path) == 0) + data->is_safe = true; + + return 0; +} + +static int validate_ownership(const char *repo_path) +{ + git_config *config = NULL; + validate_ownership_data data = { repo_path, GIT_STR_INIT, false }; + bool is_safe; + int error; + + if ((error = git_fs_path_owner_is_current_user(&is_safe, repo_path)) < 0) { + if (error == GIT_ENOTFOUND) + error = 0; + + goto done; + } + + if (is_safe) { + error = 0; + goto done; + } + + if (load_global_config(&config) == 0) { + error = git_config_get_multivar_foreach(config, "safe.directory", NULL, validate_ownership_cb, &data); + + if (!error && data.is_safe) + goto done; + } + + git_error_set(GIT_ERROR_CONFIG, + "repository path '%s' is not owned by current user", + repo_path); + error = GIT_EOWNER; + +done: + git_config_free(config); + git_str_dispose(&data.tmp); + return error; +} + static int find_repo( git_str *gitdir_path, git_str *workdir_path, @@ -856,6 +915,7 @@ int git_repository_open_ext( gitlink = GIT_STR_INIT, commondir = GIT_STR_INIT; git_repository *repo = NULL; git_config *config = NULL; + const char *validation_path; int version = 0; if (flags & GIT_REPOSITORY_OPEN_FROM_ENV) @@ -904,16 +964,24 @@ int git_repository_open_ext( if ((error = check_extensions(config, version)) < 0) goto cleanup; - if ((flags & GIT_REPOSITORY_OPEN_BARE) != 0) + if ((flags & GIT_REPOSITORY_OPEN_BARE) != 0) { repo->is_bare = 1; - else { - + } else { if (config && ((error = load_config_data(repo, config)) < 0 || (error = load_workdir(repo, config, &workdir)) < 0)) goto cleanup; } + /* + * Ensure that the git directory is owned by the current user. + */ + validation_path = repo->is_bare ? repo->gitdir : repo->workdir; + + if (git_repository__validate_ownership && + (error = validate_ownership(validation_path)) < 0) + goto cleanup; + cleanup: git_str_dispose(&gitdir); git_str_dispose(&workdir); @@ -1607,13 +1675,40 @@ static bool is_filesystem_case_insensitive(const char *gitdir_path) return is_insensitive; } -static bool are_symlinks_supported(const char *wd_path) +/* + * Return a configuration object with only the global and system + * configurations; no repository-level configuration. + */ +static int load_global_config(git_config **config) { - git_config *config = NULL; git_str global_buf = GIT_STR_INIT; git_str xdg_buf = GIT_STR_INIT; git_str system_buf = GIT_STR_INIT; git_str programdata_buf = GIT_STR_INIT; + int error; + + git_config__find_global(&global_buf); + git_config__find_xdg(&xdg_buf); + git_config__find_system(&system_buf); + git_config__find_programdata(&programdata_buf); + + error = load_config(config, NULL, + path_unless_empty(&global_buf), + path_unless_empty(&xdg_buf), + path_unless_empty(&system_buf), + path_unless_empty(&programdata_buf)); + + git_str_dispose(&global_buf); + git_str_dispose(&xdg_buf); + git_str_dispose(&system_buf); + git_str_dispose(&programdata_buf); + + return error; +} + +static bool are_symlinks_supported(const char *wd_path) +{ + git_config *config = NULL; int symlinks = 0; /* @@ -1624,19 +1719,9 @@ static bool are_symlinks_supported(const char *wd_path) * _not_ set, then we do not test or enable symlink support. */ #ifdef GIT_WIN32 - git_config__find_global(&global_buf); - git_config__find_xdg(&xdg_buf); - git_config__find_system(&system_buf); - git_config__find_programdata(&programdata_buf); - - if (load_config(&config, NULL, - path_unless_empty(&global_buf), - path_unless_empty(&xdg_buf), - path_unless_empty(&system_buf), - path_unless_empty(&programdata_buf)) < 0) - goto done; - - if (git_config_get_bool(&symlinks, config, "core.symlinks") < 0 || !symlinks) + if (load_global_config(&config) < 0 || + git_config_get_bool(&symlinks, config, "core.symlinks") < 0 || + !symlinks) goto done; #endif @@ -1644,10 +1729,6 @@ static bool are_symlinks_supported(const char *wd_path) goto done; done: - git_str_dispose(&global_buf); - git_str_dispose(&xdg_buf); - git_str_dispose(&system_buf); - git_str_dispose(&programdata_buf); git_config_free(config); return symlinks != 0; } diff --git a/src/libgit2/repository.h b/src/libgit2/repository.h index 3c3aa1e8e..a488f2bf2 100644 --- a/src/libgit2/repository.h +++ b/src/libgit2/repository.h @@ -34,6 +34,7 @@ #define GIT_DIR_SHORTNAME "GIT~1" extern bool git_repository__fsync_gitdir; +extern bool git_repository__validate_ownership; /** Cvar cache identifiers */ typedef enum { diff --git a/src/util/fs_path.c b/src/util/fs_path.c index 65842426c..9bd773d27 100644 --- a/src/util/fs_path.c +++ b/src/util/fs_path.c @@ -1785,82 +1785,241 @@ done: return supported; } -int git_fs_path_validate_system_file_ownership(const char *path) +static git_fs_path__mock_owner_t mock_owner = GIT_FS_PATH_MOCK_OWNER_NONE; + +void git_fs_path__set_owner(git_fs_path__mock_owner_t owner) +{ + mock_owner = owner; +} + +#ifdef GIT_WIN32 +static PSID *sid_dup(PSID sid) +{ + DWORD len; + PSID dup; + + len = GetLengthSid(sid); + + if ((dup = git__malloc(len)) == NULL) + return NULL; + + if (!CopySid(len, dup, sid)) { + git_error_set(GIT_ERROR_OS, "could not duplicate sid"); + git__free(dup); + return NULL; + } + + return dup; +} + +static int current_user_sid(PSID *out) { -#ifndef GIT_WIN32 - GIT_UNUSED(path); - return GIT_OK; -#else - git_win32_path buf; - PSID owner_sid; - PSECURITY_DESCRIPTOR descriptor = NULL; - HANDLE token; TOKEN_USER *info = NULL; - DWORD err, len; - int ret; + HANDLE token = NULL; + DWORD len = 0; + int error = -1; - if (git_win32_path_from_utf8(buf, path) < 0) - return -1; + if (!OpenProcessToken(GetCurrentProcess(), TOKEN_QUERY, &token)) { + git_error_set(GIT_ERROR_OS, "could not lookup process information"); + goto done; + } + + if (GetTokenInformation(token, TokenUser, NULL, 0, &len) || + GetLastError() != ERROR_INSUFFICIENT_BUFFER) { + git_error_set(GIT_ERROR_OS, "could not lookup token metadata"); + goto done; + } - err = GetNamedSecurityInfoW(buf, SE_FILE_OBJECT, - OWNER_SECURITY_INFORMATION | - DACL_SECURITY_INFORMATION, - &owner_sid, NULL, NULL, NULL, &descriptor); + info = git__malloc(len); + GIT_ERROR_CHECK_ALLOC(info); - if (err == ERROR_FILE_NOT_FOUND || err == ERROR_PATH_NOT_FOUND) { - ret = GIT_ENOTFOUND; - goto cleanup; + if (!GetTokenInformation(token, TokenUser, info, len, &len)) { + git_error_set(GIT_ERROR_OS, "could not lookup current user"); + goto done; } - if (err != ERROR_SUCCESS) { + if ((*out = sid_dup(info->User.Sid))) + error = 0; + +done: + if (token) + CloseHandle(token); + + git__free(info); + return error; +} + +static int file_owner_sid(PSID *out, const char *path) +{ + git_win32_path path_w32; + PSECURITY_DESCRIPTOR descriptor = NULL; + PSID owner_sid; + DWORD ret; + int error = -1; + + if (git_win32_path_from_utf8(path_w32, path) < 0) + return -1; + + ret = GetNamedSecurityInfoW(path_w32, SE_FILE_OBJECT, + OWNER_SECURITY_INFORMATION | DACL_SECURITY_INFORMATION, + &owner_sid, NULL, NULL, NULL, &descriptor); + + if (ret == ERROR_FILE_NOT_FOUND || ret == ERROR_PATH_NOT_FOUND) + error = GIT_ENOTFOUND; + else if (ret != ERROR_SUCCESS) git_error_set(GIT_ERROR_OS, "failed to get security information"); - ret = GIT_ERROR; - goto cleanup; + else if (!IsValidSid(owner_sid)) + git_error_set(GIT_ERROR_OS, "file owner is not valid"); + else if ((*out = sid_dup(owner_sid))) + error = 0; + + if (descriptor) + LocalFree(descriptor); + + return error; +} + +int git_fs_path_owner_is_current_user(bool *out, const char *path) +{ + PSID owner_sid = NULL, user_sid = NULL; + int error = -1; + + if (mock_owner) { + *out = (mock_owner == GIT_FS_PATH_MOCK_OWNER_CURRENT_USER); + return 0; } - if (!IsValidSid(owner_sid)) { - git_error_set(GIT_ERROR_INVALID, "programdata configuration file owner is unknown"); - ret = GIT_ERROR; - goto cleanup; + if ((error = file_owner_sid(&owner_sid, path)) < 0 || + (error = current_user_sid(&user_sid)) < 0) + goto done; + + *out = EqualSid(owner_sid, user_sid); + error = 0; + +done: + git__free(owner_sid); + git__free(user_sid); + return error; +} + +int git_fs_path_owner_is_system(bool *out, const char *path) +{ + PSID owner_sid; + + if (mock_owner) { + *out = (mock_owner == GIT_FS_PATH_MOCK_OWNER_SYSTEM); + return 0; } + if (file_owner_sid(&owner_sid, path) < 0) + return -1; + + *out = IsWellKnownSid(owner_sid, WinBuiltinAdministratorsSid) || + IsWellKnownSid(owner_sid, WinLocalSystemSid); + + git__free(owner_sid); + return 0; +} + +int git_fs_path_owner_is_system_or_current_user(bool *out, const char *path) +{ + PSID owner_sid = NULL, user_sid = NULL; + int error = -1; + + if (mock_owner) { + *out = (mock_owner == GIT_FS_PATH_MOCK_OWNER_SYSTEM || + mock_owner == GIT_FS_PATH_MOCK_OWNER_CURRENT_USER); + return 0; + } + + if (file_owner_sid(&owner_sid, path) < 0) + goto done; + if (IsWellKnownSid(owner_sid, WinBuiltinAdministratorsSid) || IsWellKnownSid(owner_sid, WinLocalSystemSid)) { - ret = GIT_OK; - goto cleanup; - } - - /* Obtain current user's SID */ - if (OpenProcessToken(GetCurrentProcess(), TOKEN_QUERY, &token) && - !GetTokenInformation(token, TokenUser, NULL, 0, &len)) { - info = git__malloc(len); - GIT_ERROR_CHECK_ALLOC(info); - if (!GetTokenInformation(token, TokenUser, info, len, &len)) { - git__free(info); - info = NULL; + *out = 1; + error = 0; + goto done; + } + + if (current_user_sid(&user_sid) < 0) + goto done; + + *out = EqualSid(owner_sid, user_sid); + error = 0; + +done: + git__free(owner_sid); + git__free(user_sid); + return error; +} + +#else + +static int fs_path_owner_is(bool *out, const char *path, uid_t *uids, size_t uids_len) +{ + struct stat st; + size_t i; + + *out = false; + + if (p_lstat(path, &st) != 0) { + if (errno == ENOENT) + return GIT_ENOTFOUND; + + git_error_set(GIT_ERROR_OS, "could not stat '%s'", path); + return -1; + } + + for (i = 0; i < uids_len; i++) { + if (uids[i] == st.st_uid) { + *out = true; + break; } } - /* - * If the file is owned by the same account that is running the current - * process, it's okay to read from that file. - */ - if (info && EqualSid(owner_sid, info->User.Sid)) - ret = GIT_OK; - else { - git_error_set(GIT_ERROR_INVALID, "programdata configuration file owner is not valid"); - ret = GIT_ERROR; + return 0; +} + +int git_fs_path_owner_is_current_user(bool *out, const char *path) +{ + uid_t userid = geteuid(); + + if (mock_owner) { + *out = (mock_owner == GIT_FS_PATH_MOCK_OWNER_CURRENT_USER); + return 0; } - git__free(info); -cleanup: - if (descriptor) - LocalFree(descriptor); + return fs_path_owner_is(out, path, &userid, 1); +} - return ret; -#endif +int git_fs_path_owner_is_system(bool *out, const char *path) +{ + uid_t userid = 0; + + if (mock_owner) { + *out = (mock_owner == GIT_FS_PATH_MOCK_OWNER_SYSTEM); + return 0; + } + + return fs_path_owner_is(out, path, &userid, 1); } +int git_fs_path_owner_is_system_or_current_user(bool *out, const char *path) +{ + uid_t userids[2] = { geteuid(), 0 }; + + if (mock_owner) { + *out = (mock_owner == GIT_FS_PATH_MOCK_OWNER_SYSTEM || + mock_owner == GIT_FS_PATH_MOCK_OWNER_CURRENT_USER); + return 0; + } + + return fs_path_owner_is(out, path, userids, 2); +} + +#endif + int git_fs_path_find_executable(git_str *fullpath, const char *executable) { #ifdef GIT_WIN32 diff --git a/src/util/fs_path.h b/src/util/fs_path.h index bb840c43c..cf4664178 100644 --- a/src/util/fs_path.h +++ b/src/util/fs_path.h @@ -731,17 +731,37 @@ int git_fs_path_normalize_slashes(git_str *out, const char *path); bool git_fs_path_supports_symlinks(const char *dir); +typedef enum { + GIT_FS_PATH_MOCK_OWNER_NONE = 0, /* do filesystem lookups as normal */ + GIT_FS_PATH_MOCK_OWNER_SYSTEM = 1, + GIT_FS_PATH_MOCK_OWNER_CURRENT_USER = 2, + GIT_FS_PATH_MOCK_OWNER_OTHER = 3 +} git_fs_path__mock_owner_t; + +/** + * Sets the mock ownership for files; subsequent calls to + * `git_fs_path_owner_is_*` functions will return this data until cleared + * with `GIT_FS_PATH_MOCK_OWNER_NONE`. + */ +void git_fs_path__set_owner(git_fs_path__mock_owner_t owner); + /** - * Validate a system file's ownership - * * Verify that the file in question is owned by an administrator or system - * account, or at least by the current user. - * - * This function returns 0 if successful. If the file is not owned by any of - * these, or any other if there have been problems determining the file - * ownership, it returns -1. + * account. + */ +int git_fs_path_owner_is_system(bool *out, const char *path); + +/** + * Verify that the file in question is owned by the current user; + */ + +int git_fs_path_owner_is_current_user(bool *out, const char *path); + +/** + * Verify that the file in question is owned by an administrator or system + * account _or_ the current user; */ -int git_fs_path_validate_system_file_ownership(const char *path); +int git_fs_path_owner_is_system_or_current_user(bool *out, const char *path); /** * Search the current PATH for the given executable, returning the full diff --git a/tests/clar/clar_libgit2.c b/tests/clar/clar_libgit2.c index 55a09d111..783b457f9 100644 --- a/tests/clar/clar_libgit2.c +++ b/tests/clar/clar_libgit2.c @@ -599,6 +599,11 @@ void cl_sandbox_set_search_path_defaults(void) git_str_dispose(&path); } +void cl_sandbox_disable_ownership_validation(void) +{ + git_libgit2_opts(GIT_OPT_SET_OWNER_VALIDATION, 0); +} + #ifdef GIT_WIN32 bool cl_sandbox_supports_8dot3(void) { diff --git a/tests/clar/clar_libgit2.h b/tests/clar/clar_libgit2.h index e3b7bd9f8..da3f41524 100644 --- a/tests/clar/clar_libgit2.h +++ b/tests/clar/clar_libgit2.h @@ -222,6 +222,7 @@ void cl_fake_home(void); void cl_fake_home_cleanup(void *); void cl_sandbox_set_search_path_defaults(void); +void cl_sandbox_disable_ownership_validation(void); #ifdef GIT_WIN32 # define cl_msleep(x) Sleep(x) diff --git a/tests/clar/main.c b/tests/clar/main.c index 56751c288..d879073a8 100644 --- a/tests/clar/main.c +++ b/tests/clar/main.c @@ -26,6 +26,7 @@ int main(int argc, char *argv[]) cl_global_trace_register(); cl_sandbox_set_search_path_defaults(); + cl_sandbox_disable_ownership_validation(); /* Run the test suite */ res = clar_test_run(); diff --git a/tests/libgit2/repo/config.c b/tests/libgit2/repo/config.c index ee7e43dff..37f6b521d 100644 --- a/tests/libgit2/repo/config.c +++ b/tests/libgit2/repo/config.c @@ -28,7 +28,6 @@ void test_repo_config__cleanup(void) cl_assert(!git_fs_path_isdir("alternate")); cl_fixture_cleanup("empty_standard_repo"); - } void test_repo_config__can_open_global_when_there_is_no_file(void) diff --git a/tests/libgit2/repo/open.c b/tests/libgit2/repo/open.c index f7ed2c373..4b6609a81 100644 --- a/tests/libgit2/repo/open.c +++ b/tests/libgit2/repo/open.c @@ -3,13 +3,30 @@ #include "sysdir.h" #include <ctype.h> +static int validate_ownership = 0; +static git_buf config_path = GIT_BUF_INIT; + +void test_repo_open__initialize(void) +{ + cl_git_pass(git_libgit2_opts(GIT_OPT_GET_SEARCH_PATH, GIT_CONFIG_LEVEL_GLOBAL, &config_path)); + cl_git_pass(git_libgit2_opts(GIT_OPT_GET_OWNER_VALIDATION, &validate_ownership)); +} void test_repo_open__cleanup(void) { cl_git_sandbox_cleanup(); + cl_fixture_cleanup("empty_standard_repo"); + cl_fixture_cleanup("__global_config"); if (git_fs_path_isdir("alternate")) git_futils_rmdir_r("alternate", NULL, GIT_RMDIR_REMOVE_FILES); + + git_fs_path__set_owner(GIT_FS_PATH_MOCK_OWNER_NONE); + + cl_git_pass(git_libgit2_opts(GIT_OPT_SET_SEARCH_PATH, GIT_CONFIG_LEVEL_GLOBAL, config_path.ptr)); + git_buf_dispose(&config_path); + + cl_git_pass(git_libgit2_opts(GIT_OPT_SET_OWNER_VALIDATION, validate_ownership)); } void test_repo_open__bare_empty_repo(void) @@ -453,3 +470,133 @@ void test_repo_open__force_bare(void) git_repository_free(barerepo); } +void test_repo_open__validates_dir_ownership(void) +{ + git_repository *repo; + + cl_git_pass(git_libgit2_opts(GIT_OPT_SET_OWNER_VALIDATION, 1)); + + cl_fixture_sandbox("empty_standard_repo"); + cl_git_pass(cl_rename("empty_standard_repo/.gitted", "empty_standard_repo/.git")); + + /* When the current user owns the repo config, that's acceptable */ + git_fs_path__set_owner(GIT_FS_PATH_MOCK_OWNER_CURRENT_USER); + cl_git_pass(git_repository_open(&repo, "empty_standard_repo")); + git_repository_free(repo); + + /* When the system user owns the repo config, fail */ + git_fs_path__set_owner(GIT_FS_PATH_MOCK_OWNER_SYSTEM); + cl_git_fail(git_repository_open(&repo, "empty_standard_repo")); + + /* When an unknown user owns the repo config, fail */ + git_fs_path__set_owner(GIT_FS_PATH_MOCK_OWNER_OTHER); + cl_git_fail(git_repository_open(&repo, "empty_standard_repo")); +} + +void test_repo_open__can_allowlist_dirs_with_problematic_ownership(void) +{ + git_repository *repo; + git_str config_path = GIT_STR_INIT, + config_filename = GIT_STR_INIT, + config_data = GIT_STR_INIT; + + cl_git_pass(git_libgit2_opts(GIT_OPT_SET_OWNER_VALIDATION, 1)); + + cl_fixture_sandbox("empty_standard_repo"); + cl_git_pass(cl_rename("empty_standard_repo/.gitted", "empty_standard_repo/.git")); + + git_fs_path__set_owner(GIT_FS_PATH_MOCK_OWNER_OTHER); + cl_git_fail(git_repository_open(&repo, "empty_standard_repo")); + + /* Add safe.directory options to the global configuration */ + git_str_joinpath(&config_path, clar_sandbox_path(), "__global_config"); + cl_must_pass(p_mkdir(config_path.ptr, 0777)); + git_libgit2_opts(GIT_OPT_SET_SEARCH_PATH, GIT_CONFIG_LEVEL_GLOBAL, config_path.ptr); + + git_str_joinpath(&config_filename, config_path.ptr, ".gitconfig"); + + git_str_printf(&config_data, + "[foo]\n" \ + "\tbar = Foobar\n" \ + "\tbaz = Baz!\n" \ + "[safe]\n" \ + "\tdirectory = /non/existent/path\n" \ + "\tdirectory = /\n" \ + "\tdirectory = c:\\\\temp\n" \ + "\tdirectory = %s/%s\n" \ + "\tdirectory = /tmp\n" \ + "[bar]\n" \ + "\tfoo = barfoo\n", + clar_sandbox_path(), "empty_standard_repo"); + cl_git_rewritefile(config_filename.ptr, config_data.ptr); + + cl_git_pass(git_repository_open(&repo, "empty_standard_repo")); + git_repository_free(repo); + + git_str_dispose(&config_path); + git_str_dispose(&config_filename); + git_str_dispose(&config_data); +} + +void test_repo_open__can_reset_safe_directory_list(void) +{ + git_repository *repo; + git_str config_path = GIT_STR_INIT, + config_filename = GIT_STR_INIT, + config_data = GIT_STR_INIT; + + cl_git_pass(git_libgit2_opts(GIT_OPT_SET_OWNER_VALIDATION, 1)); + + cl_fixture_sandbox("empty_standard_repo"); + cl_git_pass(cl_rename("empty_standard_repo/.gitted", "empty_standard_repo/.git")); + + git_fs_path__set_owner(GIT_FS_PATH_MOCK_OWNER_OTHER); + cl_git_fail(git_repository_open(&repo, "empty_standard_repo")); + + /* Add safe.directory options to the global configuration */ + git_str_joinpath(&config_path, clar_sandbox_path(), "__global_config"); + cl_must_pass(p_mkdir(config_path.ptr, 0777)); + git_libgit2_opts(GIT_OPT_SET_SEARCH_PATH, GIT_CONFIG_LEVEL_GLOBAL, config_path.ptr); + + git_str_joinpath(&config_filename, config_path.ptr, ".gitconfig"); + + /* The blank resets our sandbox directory and opening fails */ + + git_str_printf(&config_data, + "[foo]\n" \ + "\tbar = Foobar\n" \ + "\tbaz = Baz!\n" \ + "[safe]\n" \ + "\tdirectory = %s/%s\n" \ + "\tdirectory = \n" \ + "\tdirectory = /tmp\n" \ + "[bar]\n" \ + "\tfoo = barfoo\n", + clar_sandbox_path(), "empty_standard_repo"); + cl_git_rewritefile(config_filename.ptr, config_data.ptr); + + cl_git_fail(git_repository_open(&repo, "empty_standard_repo")); + + /* The blank resets tmp and allows subsequent declarations to succeed */ + + git_str_clear(&config_data); + git_str_printf(&config_data, + "[foo]\n" \ + "\tbar = Foobar\n" \ + "\tbaz = Baz!\n" \ + "[safe]\n" \ + "\tdirectory = /tmp\n" \ + "\tdirectory = \n" \ + "\tdirectory = %s/%s\n" \ + "[bar]\n" \ + "\tfoo = barfoo\n", + clar_sandbox_path(), "empty_standard_repo"); + cl_git_rewritefile(config_filename.ptr, config_data.ptr); + + cl_git_pass(git_repository_open(&repo, "empty_standard_repo")); + git_repository_free(repo); + + git_str_dispose(&config_path); + git_str_dispose(&config_filename); + git_str_dispose(&config_data); +} diff --git a/tests/util/path.c b/tests/util/path.c index 404c17a2b..2c39e0887 100644 --- a/tests/util/path.c +++ b/tests/util/path.c @@ -737,3 +737,28 @@ void test_path__find_exe_in_path(void) git_str_dispose(&sandbox_path); git__free(orig_path); } + +void test_path__validate_current_user_ownership(void) +{ + bool is_cur; + + cl_must_pass(p_mkdir("testdir", 0777)); + cl_git_pass(git_fs_path_owner_is_current_user(&is_cur, "testdir")); + cl_assert_equal_i(is_cur, 1); + + cl_git_rewritefile("testfile", "This is a test file."); + cl_git_pass(git_fs_path_owner_is_current_user(&is_cur, "testfile")); + cl_assert_equal_i(is_cur, 1); + +#ifdef GIT_WIN32 + cl_git_pass(git_fs_path_owner_is_current_user(&is_cur, "C:\\")); + cl_assert_equal_i(is_cur, 0); + + cl_git_fail(git_fs_path_owner_is_current_user(&is_cur, "c:\\path\\does\\not\\exist")); +#else + cl_git_pass(git_fs_path_owner_is_current_user(&is_cur, "/")); + cl_assert_equal_i(is_cur, 0); + + cl_git_fail(git_fs_path_owner_is_current_user(&is_cur, "/path/does/not/exist")); +#endif +} |