Security
A git command line has more ways in than it looks. A branch named --upload-pack=touch /tmp/x is an option. A URL ext::sh -c ... runs a command. GIT_DIR in the environment, as a hook sets it, makes -C point somewhere else. A repository's hooks and core.fsmonitor are programs. The deck closes each of these, so names, revisions, paths and URLs can come from users.
What the deck does
- No shell. Every command is an argument list handed to the program.
- Names are checked as git checks them. Branch, tag and remote names follow
git check-ref-format: no spaces,..,~ ^ : ? * [ \,@plus an opening brace, leading or trailing/or.,.lockendings, and no leading-. - Revisions and URLs may not start with
-, and revisions are passed after--end-of-optionswhere git supports it. Control characters are refused in both. - Paths come after
--and are literal (--literal-pathspecs):-rfis a file name and*.txtis not a glob, nor is:(top)magic. - Remotes are allow-listed.
protocol.allow=never, then onlyConfig.protocols(defaulthttpsandssh). Local paths andfile://needfilein the list: with it, a URL from a user can clone any repository the process can read.ext::,fd::and every othertransport::addressform is refused before git sees it, and ascheme://URL cannot reach agit-remote-<scheme>helper on thePATH.withProtocolschanges the list;httpandgitare unencrypted. - The environment cannot redirect a command.
GIT_DIR,GIT_WORK_TREE,GIT_INDEX_FILE,GIT_OBJECT_DIRECTORYand the other variables that beat-Care removed withenv -uon every command. - git does not look above the repository.
opentakes the top level;GIT_CEILING_DIRECTORIESstops a subdirectory of some other repository from being taken for it. - Programs from the repository do not run. Hooks are off (
core.hooksPath=/dev/null) unlesswithHooksturns them on, the fsmonitor is off, and diffs and blame run no external diff or text conversion, which a.gitattributesin the repository could select. openchecks the top level. git does not look above the directory, andopencompares the top level git reports with the one asked for.- Credentials stay out of error messages.
https://user:token@...is writtenhttps://***@...in the deck's messages; git strips it from its own. - Nothing waits for a person.
GIT_TERMINAL_PROMPT=0: a remote that wants a password fails instead of hanging. The locale isC, so messages are English and stable. - Every command has a timeout (
withTimeout, two minutes by default), and replaced objects (refs/replace/) are ignored, so an id names what it says.
What it does not do
- A repository's own configuration is still git's.
.git/configcan setcore.sshCommand, filters and aliases that run programs. git refuses repositories owned by another user (safe.directory); do not run the deck on a.gitdirectory that came from an upload or an archive. - The timeout kills git, not its children. An
sshorgit-remote-httpsit started can outlive it until its connection drops. A command killed while writing can leave.git/index.lockbehind; the next command that needs the index then fails until it is removed. - A URL can reach any host the machine can. Cloning a user's
https://orssh://URL connects to internal hosts as readily as to GitHub; filter hosts before callingcloneorlsRemoteif that matters. runis unchecked. It passes your arguments as they are, after the hardened prefix. Never build them from user input.- Credentials are git's. The deck never sees them: use an ssh agent, deploy keys, or a credential helper.