Security Notes
CI/CD & Supply Chain

Git Security: How Git Works and Where It Leaks

14 min read 8 sections 6 model answers verified 2026-10 version 1 · updated 2026-10-15

Last verified2026-10 Also see: GitHub & CI/CD security (forks, pull requests, Actions) · OWASP CI/CD risks · Secure coding


Start here: the basics in plain words

Git is the tool almost every team uses to track changes to code. Think of it as a time machine for a folder: every time you commit, Git saves a snapshot of all the files, with a note saying who made it, when, and why. You can go back to any snapshot, compare two of them, or work on a separate line of changes (a branch) and merge it back later.

The security story of Git comes from three facts:

History is permanent and copied everywhere.

Every clone holds the entire history. Delete a password file in today's commit, and it is still sitting in last week's commit on every laptop, CI runner and fork.

Names on commits are just text.

Git records whatever name and email the committer typed into their config. Nothing checks it unless commits are signed.

A repository is code you will run.

Build scripts, hooks managers and submodules mean that cloning and building an untrusted repo can run someone else's code.

Repository (repo)

A folder plus its whole history, stored in the hidden .git directory.

Commit

One saved snapshot, with author, committer, timestamp, message and a pointer to the previous commit.

Branch

A movable name pointing at one commit, e.g. main. Creating a branch costs nothing; it's just a pointer.

Remote

Another copy of the repo you sync with, usually on GitHub or GitLab, named origin by default.

Clone / fetch / pull / push

Copy a whole repo / download new commits / download and merge / upload your commits.

Hash (commit ID)

The fingerprint that names every commit, e.g. 9f3c1e7…. Change anything in the commit and the hash changes.

Memory hook

"Git never forgets, and Git believes whatever you tell it your name is." Secrets leak because of the first half; impersonation works because of the second.


How Git Stores Data

What is this?Inside .git, everything is stored as objects, each named by the hash of its own content. A commit points to a tree (a folder listing), trees point to blobs (file contents) and other trees, and each commit points to its parent. That chain is why history is tamper-evident: changing an old commit changes its hash, which changes every commit after it.

Why it mattersIt explains the interview favourites: why you can't "just delete" a secret, why rewriting history forces everyone to re-clone, why a commit hash identifies code exactly, and why SHA-1 collisions were a real concern for Git.

How it works

refs/heads/main ──▶ commit 9f3c1e7                    ← a branch is just a file holding a commit hash
                      tree   4b82…                     ← the root folder at this moment
                      parent 21aa0d3                   ← previous commit (the chain = history)
                      author Alice <alice@acme.com>    ← free text: nothing verifies this
                      "Add login rate limiting"
                         │
                         ▼
                    tree 4b82…
                      blob a1f0…  README.md            ← file contents, stored once, named by hash
                      tree 77c9…  src/
                                    blob 3e9d…  auth.py
Blob

The contents of one file, without its name. Identical files in different places are stored once.

Tree

A folder listing: names, permissions, and the hashes of the blobs and sub-trees inside it.

Commit

A tree hash, parent commit hash(es), author, committer, timestamps and the message.

Tag

A named pointer to a commit; an "annotated" tag can be signed, which is how releases are often signed.

HEAD

The commit you currently have checked out, usually via a branch name.

Reflog

Your local diary of where HEAD and branches pointed recently. The safety net for "I lost my commits" and a useful forensic trail.

In practiceReading the raw objects yourself:

text
$ git cat-file -p HEAD
tree 4b825dc642cb6eb9a060e54bf8d69288fbee4904
parent 21aa0d3c5e…
author Alice <alice@acme.com> 1760510400 +0200        ← self-asserted name, email and time
committer Alice <alice@acme.com> 1760510400 +0200

Add login rate limiting

$ git count-objects -vH
size-pack: 182.4 MiB                                   ← every version of every file ever committed lives here

Hash algorithmGit names objects with SHA-1. After researchers produced the first SHA-1 collision in 2017 ("SHAttered"), Git switched to a hardened SHA-1 implementation that detects collision attempts (Git 2.13, 2017). Repositories using SHA-256 object names have been supported since Git 2.29 (2020) but are still rare because hosting services and tools lag behind.


Who Wrote This Commit? Identity and Signing

What is this?Every commit has an author (who wrote the change) and a committer (who applied it). Both are just a name and email taken from the local Git config. Anyone can set them to anything. Commit signing adds a cryptographic signature that proves which key made the commit.

Why it mattersWithout signing, a reviewer, an auditor or an incident responder can't trust the name on a commit. Attackers who get push access, or who open pull requests, can make malicious commits appear to come from a trusted maintainer.

How it works

  1. Unsigned commitattackanyone can claim any name
    • git config user.name "Linus Torvalds" and every commit says so
    • GitHub even shows that person's avatar if the email matches their account
  2. Signed commitdefendthe signature proves the key
    • GPG keys (classic), SSH keys (Git 2.34, 2021: reuse the key you already push with), or Sigstore gitsign (short-lived certificates tied to your login)
    • The hosting service checks the signature against keys registered to the account and shows "Verified"
  3. Enforced signingdefendpolicy, not habit
    • Branch protection "Require signed commits" rejects unsigned pushes to protected branches
    • GitHub "vigilant mode" marks unsigned commits claiming to be you as "Unverified"

In practiceWhat a verified commit looks like locally:

text
$ git log --show-signature -1
commit 9f3c1e7…
Good "git" signature for alice@acme.com with ED25519 key SHA256:Q2xv…   ← key matches allowed_signers
Author: Alice <alice@acme.com>

Security angleSigning proves which key made a commit, not that the code is good. A stolen laptop with an unprotected signing key still produces "Verified" commits, so protect keys (hardware keys, passphrases) and remember that the pusher (recorded by the hosting service's audit log) can differ from the commit's author and committer. In an investigation, trust the server-side push log over the names inside commits.

🎯

On the job. For repositories that build releases or deploy to production: require signed commits or at least signed tags on release branches, keep an allowed_signers list or registered keys, and when investigating a suspicious commit, pull the push event from the GitHub/GitLab audit log to see which account actually uploaded it and from which IP.


Secrets in Git History

What is this?A password, API key or private key committed to a repository, even for a few minutes, even in a branch that was later deleted.

Why it mattersIt is one of the most common causes of cloud breaches. The commit is copied to every clone, fork, CI cache and backup the moment it is pushed, and bots scan public GitHub for keys within minutes of a push. "I deleted the file in the next commit" protects nothing: the secret is still in the earlier commit.

How it worksWhat actually happens, and the right response order:

  1. Secret committed and pushedattackminute 0
    • Copied to the remote, then to every clone, fork and CI runner that fetches
  2. Foundattackminutes to months
    • Public repo: automated scanners, often within minutes. Private repo: anyone with read access, or an attacker who later gets it
  3. 1. Revoke and rotate the secretdefendfirst, always
    • Rewriting history doesn't un-leak it; only rotation makes the old value useless
  4. 2. Check whether it was useddefendlogs
    • Cloud audit logs (CloudTrail etc.) for the key's activity since the commit date
  5. 3. Clean the historydefendoptional hygiene
    • git filter-repo (or BFG) to remove it, force-push, ask collaborators to re-clone; ask the host to purge cached views
  6. 4. Prevent the next onedefendguardrails
    • Pre-commit secret scanning, server-side push protection, secrets loaded from a secret manager at runtime

In practiceFinding when a secret entered the history (the "pickaxe" search looks for commits that added or removed a string):

text
$ git log --all -S 'AKIA' --oneline --date=short --format='%h %ad %an %s'
8c21f04 2025-03-14 bob   Add deploy script            ← the commit that introduced the AWS key
e1d7a9b 2025-03-14 bob   Remove keys from deploy      ← "removed" — but 8c21f04 still contains it
text
$ gitleaks detect --source . --log-opts="--all"
Finding:  aws_secret_access_key = "wJalr…"
Commit:   8c21f04                                         ← scan ALL history, not just the current files
📰

Real incident — Uber (2016). Attackers got into a private GitHub repository used by Uber engineers and found an AWS access key in it. With that key they downloaded data on 57 million riders and drivers from an S3 bucket. Uber then paid the attackers to stay quiet, which later led to an FTC settlement and a criminal conviction of its former chief security officer. Lesson: private repositories are not a safe place for secrets; one compromised account exposes everything in the history.

📰

Real incident — Cross Fork Object Reference (2024). Truffle Security showed that on GitHub, commits pushed to a fork can still be fetched through the original repository's network if you know (or can guess a short prefix of) the commit hash, even after the fork was deleted or the repository was made public from a private one. GitHub said this is how its fork networks work by design. Lesson: on a hosted service, treat anything ever pushed as permanently exposed, and rotate.

🎯

On the job. Turn on push protection (GitHub secret scanning, GitLab secret push protection) so secrets are blocked before they land, add a pre-commit hook such as gitleaks protect --staged, and when a leak is reported, start the clock from the commit date, not the report date.


Exposed .git Directories

What is this?A website deployed by running git clone (or git pull) straight into the web server's document root. If the server doesn't block it, anyone can download https://example.com/.git/ and rebuild the whole repository: all the source code and its entire history.

Why it mattersThe source code reveals hidden endpoints and logic flaws, and the history often contains old configuration files with database passwords or API keys. It's a common, easy finding in external attack-surface scans and bug bounty reports.

How it worksThe .git folder has a predictable layout (HEAD, config, index, objects/…), so tools can walk it file by file and reassemble the repository, even when directory listing is turned off.

In practiceWhat it looks like in a web server's access log when someone checks for it:

text
203.0.113.77 - - [15/Oct/2026:03:12:01] "GET /.git/HEAD HTTP/1.1" 200 23        ← 200 = exposed
203.0.113.77 - - [15/Oct/2026:03:12:02] "GET /.git/config HTTP/1.1" 200 312
203.0.113.77 - - [15/Oct/2026:03:12:02] "GET /.git/index HTTP/1.1" 200 48211

Security angleThe fix is to deploy build artifacts, not working copies of the repository, and to deny /.git (and other dot-files) at the web server or CDN. If a .git directory was exposed, treat every secret in its history as leaked and rotate it.

🎯

On the job. Add /.git/HEAD and /.git/config to your external scanning checks, alert on 200 responses for those paths in access logs, and look for the same mistake with .env, .svn/ and .DS_Store files.


Untrusted Repositories: When Cloning Runs Code

What is this?Git itself is careful: hooks in .git/hooks are never copied by clone, and repository-local config isn't either. But the things people do right after cloning (installing a hooks manager, running npm install, make, opening the folder in an IDE that runs tasks, initialising submodules) can execute code that the repository's author controls. And Git itself has had vulnerabilities in how it handles crafted repositories.

Why it mattersDevelopers, security researchers and CI systems clone untrusted repositories all the time: reviewing a pull request, investigating malware samples, evaluating a library. Fake "proof of concept" repositories for new CVEs, aimed at researchers, are a recurring trick.

Hooks managers

Tools like husky or pre-commit install hooks from files inside the repo. Once installed, every commit runs the repo author's scripts.

Build and install scripts

npm install, pip install -e ., make: running them on an untrusted repo means running its code.

IDE workspace trust

Editors can run tasks and extensions from workspace settings. VS Code's "Restricted Mode" exists for exactly this.

Submodules

A pointer to a specific commit of another repository. --recurse-submodules pulls in code from somewhere else, chosen by the repo author.

Git vulnerabilities

Bugs in Git's handling of crafted repos have allowed code execution on clone, e.g. CVE-2024-32002 (submodules and symlinks on case-insensitive filesystems). Keep Git updated.

safe.directory

Since 2022 (CVE-2022-24765), Git refuses to use a repository owned by another user unless you mark it safe, so a shared machine's attacker can't plant config that runs commands.

🎯

On the job. Open untrusted repositories in a disposable VM or container without credentials, don't run their install or build steps on your workstation, keep IDE workspace trust on, and keep Git patched on developer machines and CI runners. In code review, treat changes to .husky/, .pre-commit-config.yaml, .vscode/tasks.json, .gitmodules and build scripts as code that will run on reviewers' and CI machines.


Rewriting History and Force Pushes

What is this?Git lets you rewrite commits (amend, rebase, filter) and then force-push, replacing what a branch points to on the server. It's normal on personal branches. On shared branches it can erase work or hide malicious changes.

Why it mattersAn attacker with push access can slip a change into history and force-push so the malicious commit looks old or carries someone else's name. Equally, a force-push can destroy the evidence an investigator needs.

How it worksCommits that a branch no longer points to aren't deleted straight away: they stay in the local reflog and as unreachable objects until garbage collection, and hosting services record push and force-push events.

In practiceRecovering and investigating:

text
$ git reflog -5
9f3c1e7 HEAD@{0}: pull --force: forced-update        ← the branch moved in a non-fast-forward way
21aa0d3 HEAD@{1}: commit: Fix totals rounding        ← what main pointed at before; still recoverable
$ git fsck --lost-found                              ← lists unreachable commits and blobs

Security angleProtect shared branches: disallow force-pushes and deletions on main and release branches, require pull requests with review, and keep the hosting service's audit log (it records who force-pushed, when and from where). Commit timestamps, like names, are self-asserted, so a commit "from 2023" may have been pushed yesterday.

Memory hook

"inside the commit is a claim; the server log is the evidence". Author, committer and dates are what the pusher wrote. Who pushed, when and from where is what the hosting service saw.


Interview Questions

Q
A developer committed an AWS key and deleted it in the next commit. Are we safe?
Model answer

No. Git stores every snapshot, so the key is still in the earlier commit on the server and in every clone, fork and CI cache that fetched it, and on a public repo automated scanners often find keys within minutes. The first step is to revoke and rotate the key, then check CloudTrail for any use of it since the commit time. Rewriting history with git filter-repo is optional hygiene afterwards, not the fix; Uber's 2016 breach started with a key found in a private repository.

Q
How do you know who really wrote a commit?
Model answer

You can't from the commit alone, because the author and committer fields are whatever name and email were in the local Git config, so anyone can commit as anyone. Signed commits prove which key made the commit, with GPG, SSH keys since Git 2.34, or Sigstore's gitsign, and the hosting service marks them Verified against registered keys. For an investigation, I'd also pull the server's audit log, because the account that pushed the commit, and from which IP, is recorded there regardless of what the commit claims.

Q
Why is Git history tamper-evident?
Model answer

Every object is named by the hash of its content, and each commit includes its parent's hash, so the commit ID depends on the entire history behind it. Change one byte in an old file and that blob's hash changes, which changes the tree, the commit and every descendant commit's hash. That's why a release can be pinned by commit ID, and why rewriting history forces everyone to re-clone. The weak point was SHA-1, so Git uses a collision-detecting SHA-1 since 2017 and supports SHA-256 repositories.

Q
What's the risk of an exposed .git directory on a web server?
Model answer

If a site was deployed by cloning the repo into the web root and the server doesn't block /.git, anyone can download the objects and rebuild the full repository, which gives them the source code and its entire history, often including old config files with credentials. It's found by simply requesting /.git/HEAD. The fix is deploying build artifacts instead of working copies, denying dot-files at the web server or CDN, and rotating any secret that was ever in that history.

Q
Is it safe to clone and build a repository you don't trust?
Model answer

Cloning is mostly safe because Git doesn't copy hooks or local config, but building isn't: npm install, make, hooks managers, IDE tasks and submodules all run code the repo author chose. Git itself has also had clone-time code-execution bugs, like CVE-2024-32002, so it needs to be kept patched. I'd open untrusted repos in a throwaway VM or container without credentials, which matters especially for security researchers, since fake proof-of-concept repos for fresh CVEs are a known lure.

Q
How do branch protections and signing fit together?
Model answer

Branch protection decides how code reaches important branches: pull requests with required reviews and status checks, no force-pushes, no deletions, so one compromised account can't quietly rewrite main. Signing decides who the commits provably came from. Together they mean a change on main went through review and was made by a known key, and the hosting service's audit log ties every push to an account. None of them judge whether the code is safe, which is still the reviewer's and the CI checks' job.