Skip to content

Instantly share code, notes, and snippets.

@miallo
Last active March 14, 2025 21:12
git only in trusted paths
#!/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
}
#!/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
}
#!/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