Last active
March 14, 2025 21:12
git only in trusted paths
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
#!/usr/bin/env bash | |
# path based trust wrapper for git | |
GIT_ALLOW_DIR="$HOME/.local/share/git/allow" | |
GIT_DENY_DIR="$HOME/.local/share/git/deny" | |
git() { | |
[ -n "$GIT_DEBUG" ] && printf "\e[32mgit %s\e[0m\n" "$*" | |
# a few small helper functions in the beginning | |
warn() { | |
echo "$@" >&2 | |
} | |
path_hash() { | |
if [ -z "$1" ]; then | |
/usr/bin/env git rev-parse --show-toplevel 2>/dev/null | shasum - | cut -d " " -f 1 | |
else | |
realpath "$1" | shasum - | cut -d " " -f 1 | |
fi | |
} | |
allow() { | |
rm -rf "$GIT_DENY_DIR/$(path_hash "$1")" | |
mkdir -p "$GIT_ALLOW_DIR/$(path_hash "$1")" | |
} | |
deny() { | |
rm -rf "$GIT_ALLOW_DIR/$(path_hash "$1")" | |
mkdir -p "$GIT_DENY_DIR/$(path_hash "$1")" | |
} | |
# implement new explicit subcommands for "allow" and "deny" | |
if [ "$1" = allow ]; then | |
allow "$2" # allows for optional `git allow <path>` | |
return 0 | |
fi | |
if [ "$1" = deny ]; then | |
deny "$2" # allows for optional `git deny <path>` | |
return 0 | |
fi | |
# allow skipping the tests by variable or argument | |
skip_allow_check="$GIT_ALLOW" | |
if [ "$1" = --allow ]; then # TODO: not nice to do this in the first arg only, but easy to implement for now... | |
skip_allow_check=true | |
shift | |
fi | |
# FIXME: do not rely on this being safe - right now it might be, but there are thinkable ways of this being vulnerable | |
# allow execution outside of repository, since then it should be safe | |
is_inside_work_tree="$(/usr/bin/env git rev-parse --is-inside-work-tree 2>/dev/null || :)" | |
if [ "$is_inside_work_tree" != true ]; then | |
skip_allow_check=true | |
fi | |
if [ "$skip_allow_check" != true ] ; then | |
# silently exit if this repository is explicitly denied | |
if [ -e "$GIT_DENY_DIR/$(path_hash)" ]; then | |
return 1 | |
fi | |
# show warning and exit if repo is not allowed | |
if ! [ -e "$GIT_ALLOW_DIR/$(path_hash)" ]; then | |
warn 'This repo is not allowed. Run `git allow` and try again or to supress this warning in the future `git deny`' | |
return 1 | |
fi | |
fi | |
# deny paths that will be removed from worktree | |
if [ "$1" = worktree ]; then | |
partial_path_removal_warning='Warning: only removal of git permissions with full paths (relative or absolute) are supported right now. Partial, but unique ones will not get disallowed automatically.' | |
if [ "$2" = remove ]; then | |
warn "$partial_path_removal_warning" # TODO | |
deny "${*: -1}" | |
fi | |
if [ "$2" = move ]; then | |
warn "$partial_path_removal_warning" # TODO | |
deny "${*:(-2):1}" | |
fi | |
fi | |
/usr/bin/env git "$@" | |
# Allow git if repos were created in a safe manner | |
if [ "$1" = init ] || [ "$1" = clone ]; then | |
if [ "$1" = init ] && [ $# -lt 2 ] && ! [[ "${*: -1}" =~ -.* ]]; then | |
# called init without a path, it is the current directory | |
allow . | |
elif [ "$1" = clone ] && { { [ $# -ge 3 ] && [[ "${*:(-2):1}" =~ -.* ]]; } || [[ "${*: -1}" =~ .*/.* ]]; }; then | |
allow "$(basename -s .git "${*: -1}")" # extract the path from the URL while removing the extension ".git" | |
else | |
allow "${*: -1}" # The path is the last argument | |
fi | |
fi | |
# add worktree permissions | |
if [ "$1" = worktree ] && { [ "$2" = move ] || [ "$2" = add ]; }; then | |
# check if the second last argument is the path (also make sure that the third last arg doesn't have take the second last) | |
if [ "$2" = add ] && [ $# -ge 4 ] && ! { [[ "${*:(-2):1}" =~ -.* ]] || [[ "${*:(-3):1}" =~ -{b,B,-reason} ]]; }; then | |
allow "${*:(-2):1}" | |
else | |
allow "${*: -1}" | |
fi | |
fi | |
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
#!/usr/bin/env bash | |
# secret based trust wrapper for git | |
GIT_SECRET_PATH="$HOME/.local/share/git/secret" | |
# equivalent of /usr/bin/env git rev-parse --show-toplevel but without possible code execution | |
git_safe_show_toplevel() { | |
DIR="$(pwd)" | |
while [ "$DIR" != / ]; do | |
if [ -f "$DIR/.git" ]; then # is worktree | |
grep -q "^gitdir: " .git && echo "$DIR" | |
[ -n "$GIT_SHOW_TOPLEVEL_DEBUG" ] && printf "\e[32mgit worktree toplevel %s\e[0m\n" "$DIR" >&2 | |
return | |
fi | |
# TODO: make sure this is alligned with how git decides what a repo is. | |
# The more specific we are here, the more we are vulnerable for a command to parse a config / execute a hook since it thinks it is in a git repo, but this test will not find this | |
# if [ -d "$DIR/.git" ] && [ -f "$DIR/.git/HEAD" ] && [ -d "$DIR/.git/objects" ] && [ -d "$DIR/.git/refs/heads" ]; then | |
if [ -d "$DIR/.git" ]; then | |
#found a repo | |
echo "$DIR" | |
[ -n "$GIT_SHOW_TOPLEVEL_DEBUG" ] && printf "\e[32mgit toplevel %s\e[0m\n" "$DIR" >&2 | |
return | |
fi | |
DIR="$(dirname "$DIR")" | |
done | |
# arrived at "/" without finding a worktree or repo | |
[ -n "$GIT_SHOW_TOPLEVEL_DEBUG" ] && printf "\e[32mno git repo\e[0m\n" >&2 | |
return 1 | |
} | |
git() { | |
[ -n "$GIT_DEBUG" ] && printf "\e[32mgit secret %s\e[0m\n" "$*" | |
# a few small helper functions in the beginning | |
warn() { | |
echo "$@" >&2 | |
} | |
allow() { | |
if [ -z "$1" ]; then | |
cp "$GIT_SECRET_PATH" "$secret_dir" | |
else | |
cp "$GIT_SECRET_PATH" "$1/.git/secret" | |
fi | |
} | |
deny() { | |
: > "$secret_dir" | |
} | |
# create global secret if not exists | |
if [ ! -e "$GIT_SECRET_PATH" ]; then | |
mkdir -p "$(basename "$GIT_SECRET_PATH")" | |
# TODO: think about a sane default size - maybe even configurable? | |
# FIXME: maybe not the right way to generate the random number... | |
head -c 1024 </dev/urandom > "$GIT_SECRET_PATH" | |
fi | |
toplevel_dir="$(git_safe_show_toplevel || :)" | |
if [ -d "$toplevel_dir/.git" ]; then | |
secret_dir="$toplevel_dir/.git/secret" | |
elif [ -z "$toplevel_dir" ]; then | |
# not a git repo | |
secret_dir= | |
else | |
# worktree | |
secret_dir="$(dirname "$(dirname "$(grep '^gitdir:' "$toplevel_dir/.git" | cut -f2 -d " ")")")/secret" | |
fi | |
[ -n "$GIT_DEBUG" ] && printf "\e[36msecret_dir %s\e[0m\n" "$secret_dir" | |
# implement new explicit subcommands for "allow" and "deny" | |
if [ "$1" = allow ]; then | |
allow | |
return 0 | |
fi | |
if [ "$1" = deny ]; then | |
deny | |
return 0 | |
fi | |
# allow skipping the tests by variable or argument | |
skip_allow_check="$GIT_ALLOW" | |
if [ "$1" = --allow ]; then # TODO: not nice to do this in the first arg only, but easy to implement for now... | |
skip_allow_check=true | |
shift | |
fi | |
# allow outside of repository, since then it is safe | |
if ! [ -e "$toplevel_dir" ]; then | |
skip_allow_check=true | |
fi | |
if [ "$skip_allow_check" != true ] ; then | |
# silently exit if this repository is explicitly denied, aka an empty file | |
if [ -f "$secret_dir" ] && [ ! -s "$secret_dir" ]; then | |
return 1 | |
fi | |
# show warning and exit if repo is not allowed (aka. the files differ in content) | |
if ! cmp --silent "$GIT_SECRET_PATH" "$secret_dir"; then | |
# if ! diff -q "$GIT_SECRET_PATH" "$secret_dir"; then | |
warn 'This repo is not allowed. Run `git allow` and try again or to supress this warning in the future `git deny`' | |
return 1 | |
fi | |
fi | |
/usr/bin/env git "$@" | |
# Allow git if repos were created in a safe manner | |
if [ "$1" = init ] || [ "$1" = clone ]; then | |
if [ "$1" = init ] && [ $# -lt 2 ] && ! [[ "${*: -1}" =~ -.* ]]; then | |
# called init without a path, it is the current directory | |
allow . | |
elif [ "$1" = clone ] && { { [ $# -ge 3 ] && [[ "${*:(-2):1}" =~ -.* ]]; } || [[ "${*: -1}" =~ .*/.* ]]; }; then | |
allow "$(basename -s .git "${*: -1}")" # extract the path from the URL while removing the extension ".git" | |
else | |
allow "${*: -1}" # The path is the last argument | |
fi | |
fi | |
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
#!/usr/bin/env bash | |
set -e | |
# Set to non-empty for debug logs | |
GIT_DEBUG=1 | |
GIT_SHOW_TOPLEVEL_DEBUG= | |
# source whatever file you want to test | |
if [ "$1" = path ]; then | |
source ./git-pwd-hash.sh | |
else | |
source ./git-secret.sh | |
is_secret_based=1 | |
fi | |
TMP_DIR="$(mktemp -d)" | |
trap cleanup 1 2 3 6 | |
cleanup() { | |
exitst=$? | |
echo "Removing temporary files: $TMP_DIR" | |
rm -rf "$TMP_DIR" | |
exit $exitst | |
} | |
pushd "$TMP_DIR" | |
git status 2>&1 | grep -q "not a git repo" | |
/usr/bin/env git init -q untrusted | |
pushd untrusted | |
touch README | |
/usr/bin/env git add README | |
/usr/bin/env git commit -m "Initial Commit" | |
git status && exit 1 | |
/usr/bin/env git worktree add ../untrusted_worktree | |
pushd ../untrusted_worktree | |
git status && exit 1 | |
popd | |
git allow | |
git status | |
git deny | |
git --allow status | |
GIT_ALLOW=true git status | |
git status && exit 1 | |
popd | |
echo init | |
git init -q trusted | |
pushd trusted | |
git status | |
if [ "$is_secret_based" = 1 ]; then | |
echo hi > .git/secret | |
git status && exit 1 | |
fi | |
popd | |
git clone -q trusted trusted2 | |
pushd trusted2 | |
git status | |
popd | |
git clone -q http://github.com/miallo/nuggit | |
pushd nuggit | |
git status | |
git worktree add ../git-documentation | |
pushd ../git-documentation | |
git status | |
popd | |
git worktree add -b my-worktree-branch ../worktree | |
pushd ../worktree | |
git status | |
popd | |
git worktree add -b test ../worktree3 ba6711355c5e514fe9267107a6d13b270fa26c1a | |
pushd ../worktree3 | |
git status | |
popd | |
git worktree add ../worktree2 ba6711355c5e514fe9267107a6d13b270fa26c1a | |
pushd ../worktree2 | |
git status | |
popd | |
git worktree remove ../git-documentation | |
/usr/bin/env git init ../git-documentation | |
pushd ../git-documentation | |
if git status; then | |
echo should not succeed for deleted worktree >&2 | |
exit 1 | |
fi | |
popd | |
popd | |
git clone -q http://github.com/miallo/nuggit.git nuggit2 | |
pushd nuggit2 | |
git status | |
popd | |
/usr/bin/env git init -q malicious | |
pushd malicious | |
echo "README" > README | |
/usr/bin/env git add README | |
{ | |
echo '#!/bin/sh' | |
echo 'touch xxx' | |
} > .git/hooks/post-index-change | |
chmod +x .git/hooks/post-index-change | |
# Make this folder available as a download and wait for the $PS1 of a developer to run a simple | |
git status && exit 1 | |
git help && exit 1 | |
git version && exit 1 | |
git config --list && exit 1 | |
if [ -e xxx ]; then | |
echo "ERROR! Did execute hook" >&2 | |
exit 1 | |
fi | |
popd | |
popd | |
printf "\e[32;1msuccess!\e[0m\n" | |
cleanup |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment