The static site that published its own .git history
Metadata
- Title: The static site that published its own .git history
- Source of the case: HackerOne report #2383486 (Mozilla)
- AWS service(s): S3
- Risk archetype: sensitive prefix exposed inside an intentionally public bucket
- One-line hook: Can you prove a public static-site bucket isn't also serving its own repository?
0. The challenge (what the reader does first)
Scenario given to the reader:
A public S3 bucket serves a community static site. The bucket policy grants anonymous
s3:GetObject on every object — that's how the site is served. But a .git
directory was synced alongside the site files, so .git/HEAD, .git/config, and
.git/refs/ are now public objects too. There is no Public Access Block.
Evidence they're handed (and nothing else):
{
"bucket": "mozilla-community-site",
"bucket_policy": {"Statement": [{"Effect": "Allow", "Principal": "*", "Action": "s3:GetObject", "Resource": "arn:aws:s3:::mozilla-community-site/*"}]},
"public_access_block": null,
"notable_prefixes": [".git/HEAD", ".git/config", ".git/refs/"]
}
- No AWS credentials. No live account. No scripts.
The questions they must answer from the evidence alone:
- Public read on a static site is intended — so which specific objects make this unsafe right now?
- Why is an exposed
.git/prefix a latent risk far larger than the files themselves — what could it reconstruct? - Through path A — normal static-site assets under the public
s3:GetObjectgrant — what is exposed, and is that acceptable? - Through path B — the
.git/prefix under the same grant — what becomes reconstructable? - What single rule would have kept the public grant serving the site while refusing to serve sensitive prefixes like
.git/?
1. The manual problem
The grant is uniform: s3:GetObject on arn:aws:s3:::mozilla-community-site/*. That
wildcard makes every object public, and a manual reviewer reading the policy sees
exactly what they expect for a static site — anonymous read of the content. The
policy gives no hint that some of those objects are a Git repository.
To catch this by hand you'd have to step outside the policy entirely and inventory
the object keys, then recognize .git/HEAD and .git/config as repository internals
rather than site assets. The policy and the prefix listing live in different places,
and "the site is public on purpose" actively discourages anyone from auditing what
else the wildcard is serving. With no Public Access Block and a single broad grant,
nothing draws a line between "site files" and "everything that happened to get synced
into the same bucket."
2. The reasoning wall (capture, don't invent)
| What they hit | What they said / would say |
|---|---|
| One wildcard grant covers everything | "The policy just says /*. It can't tell me a .git folder is in there." |
| Public-on-purpose hid the sensitive prefix | "It's a public site, so a public-read finding looked completely normal." |
.git is worse than loose files | "That's not a few leaked files — that's the whole repo history, rebuildable." |
The insight the reader should reach on their own:
A blanket public grant exposes whatever ended up in the bucket, not only what you meant to publish.
3. Why scanners miss or flatten it
A per-setting scanner evaluates the policy and reports "bucket allows public read."
On a static-site bucket that's both true and expected, so it's dismissed. The scanner
operates on the grant, not on the object keys the grant covers, so it has no way to
notice that .git/HEAD, .git/config, and .git/refs/ are sitting under the same
wildcard. The specific thing it cannot see is the sensitivity of the prefix: that
within an intentionally public bucket, particular object paths constitute a source
repository whose history (commit messages, author emails, every version of every
file, and any secret ever committed) can be reconstructed with tools like
git-dumper. The scanner flattens "public site" and "public repository" into the
same green check.
Pivot point. Everything above is the gap. Everything below is Stave filling it. The reader has now done the work and hit the wall. Only now does the tool appear.
4. The evidence Stave consumes
- The same static snapshot the reader had: the public-read bucket policy, the missing Public Access Block, and the notable object prefixes present in the bucket.
{
"bucket": "mozilla-community-site",
"bucket_policy": {"Statement": [{"Effect": "Allow", "Principal": "*", "Action": "s3:GetObject", "Resource": "arn:aws:s3:::mozilla-community-site/*"}]},
"public_access_block": null,
"notable_prefixes": [".git/HEAD", ".git/config", ".git/refs/"]
}
- Object prefixes are evaluated against the public grant, so a uniform
/*policy is checked for sensitive paths it inadvertently exposes.
5. The reasoning Stave performs
- Control / invariant:
CTL.S3.PUBLIC.PREFIX.001— a protected prefix (e.g..git/) must not be publicly readable. - What it evaluates: The bucket has anonymous
s3:GetObjecton/*(path A — acceptable for the site). The control then asks whether that grant covers a protected prefix like.git/(path B). The intended public read does not excuse the sensitive prefix; the presence of.git/under the wildcard triggers the finding. - Verdict produced:
NON_COMPLIANT— a protected prefix is publicly readable.
control: CTL.S3.PUBLIC.PREFIX.001
asset: s3://mozilla-community-site/.git/
evidence: protected prefix ".git/" (HEAD, config, refs/) is covered by public s3:GetObject on /*
verdict: NON_COMPLIANT
6. The prevention artifact Stave produces
- Artifact: A bucket-policy Deny that refuses anonymous reads under the
.git/prefix while the broad public-read grant continues serving the site. - What it forecloses: The latent state from question 2 — a sensitive prefix being reconstructable into the full repository via the same public grant that serves the site. The site stays public;
.git/stops being served.
{
"Effect": "Deny",
"Principal": "*",
"Action": "s3:GetObject",
"Resource": "arn:aws:s3:::mozilla-community-site/.git/*"
}
7. What the team no longer does manually
| Before | After Stave |
|---|---|
Read a uniform /* public grant and assume it serves only site files | The grant is evaluated against object prefixes, surfacing sensitive paths it covers |
Manually inventory keys to spot a synced .git directory | One control verdict flags protected prefixes publicly readable |
| Discover a source-code leak only after someone runs git-dumper | A committed Deny on .git/* forecloses the leak on every future deploy |
Positioning line for this case
Stave proves that this public static-site bucket also exposes its
.git/history through the same wildcard grant, and emits the Deny that keeps the site public while refusing to serve the repository.
Reuse checklist
- A reader could attempt section 0 with zero Stave knowledge
- Stave is not named or shown before the pivot point
- Section 2 quotes are real (or honestly plausible), not slogans
- Section 3 names the specific thing per-setting tools can't see
- Section 6 closes the exact latent state raised in section 0, question 2
- The title names the failure, not the product