Sitelet https://github.com/systemslibrarian/crypto-lab-ghost-commit
Skip to content

About

Browser-based git secret-leak lab — commit, overwrite, and git rm of an API key side by side, with real SHA-1 and SHA-256 object ids matched against the git binary, a HEAD scan that reports clean while a history walk finds the key, and a rotate button that changes nothing in git and everything that matters

Topics

Resources

Stars

0 stars

Watchers

0 watching

Forks

Repository files navigation

Ghost Commit

Commit an API key, delete it two commits later, and watch it sit in git history forever — then watch an entropy scanner find it in seconds.

▶ Live demo


What It Is

A browser demo of two things that are usually described and rarely shown: how git's object model makes a committed secret permanent, and how a secret scanner actually decides that a string looks like a credential.

The primitives. Git names every object by the hash of <type> <byte-length>\0<content> — SHA-1 in the default object format, SHA-256 in the newer one. This demo builds real blob, tree, and commit objects from those exact byte layouts and hashes them with WebCrypto. Every object id shown on the page is byte-identical to what the git binary produces for the same input, and the test suite proves it against ids generated by the real binary. On top of that sits a hand-rolled Shannon-entropy scanner (H = −Σ p·log₂p), charset classification, and a small set of pattern rules modelled on published gitleaks and Semgrep detections.

The problem. Git does not edit files; it adds objects. Removing a secret from a file writes a new blob and a new tree. The old blob keeps its name, keeps its contents, and stays reachable from the commit that introduced it. Deleting the file entirely does the same thing. Both operations feel like removal and are additions.

The security model. There is none to break here — nothing on this page is a confidentiality mechanism. The claim being demonstrated is a durability property of a content-addressed store, and the operational consequence: once a credential is pushed, the only action that changes an attacker's position is revoking it at the system that issued it.

Not production crypto — a teaching demo. No backend, no network, no persistence. The repository lives in memory for the length of the page. The leaked value uses the prefix acmepay_live_, which belongs to no real service — AcmePay is invented, so the key cannot authenticate anything. Use gitleaks, TruffleHog, or your platform's own secret scanning for real work.

A note on the fictional vendor. The first version of this demo used a real sk_live_ prefix, and GitHub's push protection refused the push. That is the control working exactly as designed: a scanner cannot tell a teaching sample from a live credential, which is this demo's own thesis. Rather than allowlist the block or split the literal to slip past it, the sample moved to an invented vendor. The genuine Stripe, AWS, and GitHub patterns remain in src/scan/rules.ts as the real-world originals the demo rule is modelled on.

Exhibits

  1. The repository. A six-commit Python billing service you advance one commit at a time. Each step shows the real commit id, tree id, and parent, computed live. A selector switches the whole repository between git's SHA-1 and SHA-256 object formats — the bytes being hashed are identical, only the digest changes.
  2. The leak, and the two failed fixes. You commit the key, ship an unrelated feature past it, replace the literal with os.environ, and finally git rm the file. The object count goes up at every step, including the deletions.
  3. HEAD scan versus history walk. The same scanner run twice, side by side, differing only in which commits it is handed. HEAD reports clean; the parent-chain walk surfaces the credential with the real blob id and the commits that still reach it.
  4. The object store, laid bare. Every object ever written, with the blobs holding the key marked — including the ones HEAD can no longer reach.
  5. Entropy as arithmetic. Pick a string, or type your own, and the per-character sum is computed in front of you: count, probability, and each character's −p·log₂p contribution, adding to H. Nothing is asserted; the table is the formula.
  6. Where the threshold actually sits. Labelled reference strings — the leaked key, a base64 blob, a JWT header, an English sentence, a git object id, a UUID, a repeated character — each scored against its own charset threshold, marked per row because hex and base64 do not share a ceiling.
  7. The format rules. The gitleaks- and Semgrep-shaped patterns that run alongside entropy, with what each one keys on and why vendors now prefix their credentials deliberately.
  8. Delete versus rotate. A rotate action that leaves the scan output byte-for-byte identical and changes only what the value can open, next to a table of five responses and what each actually does.

When to Use It

Use this to teach: why "I removed the key in the next commit" is not a remediation; why content-addressed storage makes history durable; how entropy scanning really works and where it fails; why incident response for a leaked credential starts with rotation rather than with git.

Use the underlying idea in production by: running a secret scanner in CI over full history (not just the diff), adding a pre-commit hook so the push never happens, and treating any credential that reached a remote as compromised from that moment.

Do NOT use this as a scanner. The rule set here is five patterns chosen for readability; gitleaks ships hundreds. It walks one branch's parent chain and would miss a secret on another branch, in a stash, in a dangling object, or in a submodule. A clean result from this page means nothing about a real repository.

Do NOT treat entropy as detection. It measures distribution, not secrecy. Use it as a cheap prior behind pattern rules, never as the decision.

Live Demo

https://systemslibrarian.github.io/crypto-lab-ghost-commit/

You can: advance the commit graph and watch real object ids appear; switch the whole repository between SHA-1 and SHA-256; scan HEAD and then walk the history and compare the two reports; inspect every object in the store; score any string you type against the real entropy function and the real rules; and rotate the credential to see exactly which parts of the picture change and which do not.

What Can Go Wrong

  • Mistaking a clean HEAD for a clean repository. This is the demo's whole subject. The scanner in panel 2 reports honestly on the commits it was given, and no further.
  • Force-pushing and believing it worked. Existing clones, forks, CI caches, and platform-side dangling objects all keep the commit. On GitHub an unreferenced commit stays fetchable by its SHA, and a fork makes it permanent.
  • Rewriting history as the remedy. git filter-repo genuinely rewrites objects — and changes every commit id from that point forward, breaking every clone and every reference to those SHAs. It is hygiene after rotation, never a substitute for it.
  • Trusting entropy. A git object id and a UUID both score at the top of the hex range and are harmless. An English sentence scores about 4.3 bits/char, within half a bit of the leaked API key. Prose escapes flagging only because the tokenizer never hands it to the entropy function — the threshold gets more credit than it deserves.
  • Tuning the threshold down. Every point of extra recall arrives as UUID alerts, and a scanner developers have learned to ignore is worse than none.
  • Committing the key with a helpful variable name. ACMEPAY_API_KEY = "..." matches the generic assignment rule on the name alone. That is the rule working as designed.

Real-World Usage

Git's object formats are specified in the git source tree (Documentation/gitformat-*); the SHA-256 object format landed in git 2.29 and remains opt-in per repository, since the two formats cannot be mixed. The entropy heuristic here is the one TruffleHog made standard: score maximal runs over the base64 and hex alphabets, flag past 4.5 and 3.0 bits/char respectively. The pattern rules mirror published detections — gitleaks' stripe-access-token, aws-access-token, and private-key, and the generic hardcoded-secret-assignment shape used by Semgrep. Vendor prefixes such as sk_live_, AKIA, and ghp_ exist precisely so that scanners can match on them, which is why prefix rules now outperform entropy on the credentials that matter most.

How to Run Locally

npm install
npm run dev          # http://localhost:5173
npm test             # 63 unit tests, including the git known-answer tests
npm run build        # typecheck + production build
npm run test:a11y    # WCAG 2.1 AA gate against the production build, both themes

Regenerating the known-answer fixture needs a local git ≥ 2.29 (for --object-format=sha256):

npm run kat:regen    # drives the real git binary; rewrites src/git/git-objects.kat.json

Related Demos

Build & Verify

63 unit tests (Vitest), all passing, colocated in src/:

File Tests Covers
src/git/objects.test.ts 21 Object hashing, tree ordering, hex helpers, the full scenario in both formats, the persistence invariant
src/scan/entropy.test.ts 20 Hand-checkable entropy values, charset classification, scoring, the prose-versus-key measurement
src/scan/scanner.test.ts 22 Tokenization, pattern rules, HEAD-versus-history behaviour

67 known-answer values, generated by the real git binary. src/git/git-objects.kat.json is produced by tools/gen-git-kat.ts, which builds the demo's repository twice with actual git (SHA-1 and SHA-256 object formats) using the same file bytes, author, and fixed timestamps the page uses, then records what git computed: 12 commit ids, 12 tree ids, 38 blob-id assertions, and 5 canonical values (git's empty blob, empty tree, and the hello world blob). The browser code must reproduce all of them or the suite fails. The fixture records the git version that generated it.

Accessibility is gated, not aspirational. npm run test:a11y runs @axe-core/playwright against the production build in both themes and asserts zero WCAG 2.1 A/AA violations. The spec drives the demo to its end state first — every commit made, both scans run, the finding cards and reveal rendered, the key rotated, every reference string loaded, every disclosure opened — because an unscanned state is an ungated state. The GitHub Pages deploy runs the unit tests, the typechecked build, and this gate before it will publish.

Every color token was contrast-checked against the surface it sits on before it was written: body text ≥ 4.5:1, UI and graphical marks ≥ 3:1, in both themes. State is never carried by color alone — every status is an icon plus text plus color. Charts are real HTML tables with proportional bars, so the structure a screen reader reads and the picture a sighted reader sees come from the same markup.

Performance

Everything runs synchronously in the browser. Building the six-commit repository is roughly thirty WebCrypto digests and completes in a few milliseconds; a full history scan reads about forty lines across eleven blobs. Switching the object format rebuilds the entire graph from scratch, which is still imperceptible. Nothing is cached — the ids are recomputed on every render, so a stale value cannot be displayed.


Part of the Crypto Lab suite.

"So whether you eat or drink or whatever you do, do it all for the glory of God." — 1 Corinthians 10:31

About

Browser-based git secret-leak lab — commit, overwrite, and git rm of an API key side by side, with real SHA-1 and SHA-256 object ids matched against the git binary, a HEAD scan that reports clean while a history walk finds the key, and a rotate button that changes nothing in git and everything that matters

Topics

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages