{"slug": "source-code-in-a-public-bucket-when-git-survives-deployment", "title": "Source Code in a Public Bucket: When .git/ Survives Deployment", "summary": "A security analysis of HackerOne report 2383486 shows how a public S3 static-site bucket leaked Mozilla's full Git repository because the CI pipeline ran `git clone` into the build directory and then `aws s3 sync`'d it, including the `.git/` folder. Three commands — fetching `.git/HEAD`, mirroring `.git/`, and running `git log --all --full-history` — expose every branch, commit, and secret ever committed, even ones later removed. The fix is to strip `.git/` before syncing, use `git archive`, or sync only a separate `dist/` directory, since the bucket policy itself is correctly public.", "body_md": "✓ Human-authored analysis; AI used for formatting and proofreading.\n\nA static-site S3 bucket is, by design, public. Marketing copy, infographics, downloadable assets, anyone on the internet can read the contents. The bucket policy admits `Principal: \"*\"` for `s3:GetObject`, the CDN serves the URLs, the team treats this as a feature.\n\nThe bug Mozilla shipped to production in [HackerOne 2383486](https://hackerone.com/reports/2383486) was that the `.git/` directory shipped with the marketing content.\n\n``` bash\n$ curl -s https://acme-marketing-site.example/.git/HEAD\nref: refs/heads/main\n\n$ curl -s https://acme-marketing-site.example/.git/config\n[core]\n    repositoryformatversion = 0\n    filemode = true\n[remote \"origin\"]\n    url = git@github.com:acme/marketing-site.git\n[user]\n    email = alice@acme.example\n```\n\n`.git/HEAD` is two lines. `.git/config` is more interesting. It leaks the git remote, the developer's email, and the path of the credential store that was committed once and `git rm`'d later. From there:\n\n``` bash\n$ wget --mirror https://acme-marketing-site.example/.git/\n$ cd acme-marketing-site.example/.git/..\n$ git checkout HEAD -- .\n# Full repository history, every branch, every commit.\n$ git log --all --full-history -- '.env' 'credentials*' 'config/*.yml'\n# Every secret ever committed, even the ones that were\n# \"removed\" in a later commit.\n```\n\nThree commands turn a public bucket into a source-code disclosure. Plus the credential set the team thought they had rotated when they `git rm`'d the original commit.\n\nThe deployment pipeline looks like:\n\n```\n# In the build server / CI runner\ngit clone https://github.com/acme/marketing-site.git build/\n# Build steps run inside build/...\naws s3 sync build/ s3://acme-marketing-site/ --delete\n```\n\n`build/` is a clone, not an export. It contains `.git/`. `aws s3 sync` faithfully uploads every file under `build/`, including the `.git/` subdirectory. The team's mental model is \"we sync the build output to S3\"; reality is \"we sync the build directory plus whatever non-build files are in it.\"\n\nThe fix is one line. The bug ships because nobody's testing \"is `.git/` in the published artefacts?\" before the `aws s3 sync` runs.\n\nThis pattern is broader than a single Mozilla report. Searching public S3 endpoints for `.git/HEAD` returns hits across every industry; the count tracks how many teams use `git clone` as their deployment-source step. Most of those teams' S3 buckets are public for the same reason as Mozilla's. The site is intentionally public and the same mistake exposes the same artefacts.\n\nThe same shape applies to:\n\n`.svn/` — older, but recurring on legacy infrastructure.`.env` — environment files committed by a developer for local convenience, never rewritten out of history.`node_modules/.cache/`, `__pycache__/` — sometimes leak module names not intended for public consumption.`.aws/credentials`, `.ssh/id_rsa` — the developer-machine artefacts that occasionally end up in a build directory through a misconfigured Dockerfile.\nThe `.git/` is the most catastrophic and the source of the failure is the most generic.\n\nArticle on (`s3-public-read-policy`) was about buckets that should not be public. This bucket is *supposed to be public*. The bucket policy is correct; making the bucket private would break the marketing site.\n\nThe fix is at the **deployment pipeline** layer:\n\n```\n # In the build server / CI runner\n git clone https://github.com/acme/marketing-site.git build/\n # Build steps run inside build/...\n+rm -rf build/.git\n aws s3 sync build/ s3://acme-marketing-site/ --delete\n```\n\n…or use `git archive` (which has no `.git/`):\n\n```\ngit archive --format=tar HEAD | tar -xC build/\n```\n\n…or have the build pipeline write to a separate `dist/` directory and sync only that.\n\nThe bucket policy is the right layer for \"is this bucket public?\" The deployment pipeline is the right layer for \"are sensitive paths in the published artefact set?\" Conflating them produced the bug.\n\n**A public bucket must not contain version-control\nartefacts (`.git/`, `.svn/`) or developer-machine\nartefacts (`.env`, `.aws/credentials`).**\n\nIn Stave's observation schema, the bucket's content is modelled with two fields under `storage.content`:\n\n```\n{\n  \"storage\": {\n    \"access\": {\n      \"public_read\": true\n    },\n    \"content\": {\n      \"exposed_repo_artifacts\": true,\n      \"exposed_paths\": [\n        \".git/HEAD\",\n        \".git/config\",\n        \".git/objects/pack/pack-abc123.pack\",\n        \".env\"\n      ]\n    }\n  }\n}\n```\n\n`exposed_repo_artifacts` is the engine's verdict (a collector inspects the bucket's object listing for the known unsafe path patterns). `exposed_paths` is the evidence which is the key set the collector found.\n\n```\nid: CTL.S3.REPO.ARTIFACT.001\nname: Public Buckets Must Not Expose VCS Artifacts\nseverity: medium\nunsafe_predicate:\n  all:\n    - any:\n        - field: properties.storage.access.public_read\n          op: eq\n          value: true\n        - field: properties.storage.access.public_list\n          op: eq\n          value: true\n    - field: properties.storage.content.exposed_repo_artifacts\n      op: eq\n      value: true\n```\n\nTwo clauses, both required. Either alone is a non-finding:\n\n`.git/` in it is The combination of public *and* repo artefacts present is the one that fires. Severity is `medium` because the *intended* content of the public bucket is by definition public. The escalation comes from artefact paths revealing content the team didn't intend to publish.\n\nSame answer as bucket name dangling: this is a **presence check** at the collector layer, not a reachability question. The collector observes the bucket's object inventory, applies a known-unsafe-paths filter, and emits a boolean. CEL evaluates two booleans.\n\nA reachability question would look like \"given this set of admitted requests, is there one that produces a different intended output?\". Here the unsafe state is: \"the set of objects in the public bucket contains a key matching a known unsafe pattern.\" That's a deterministic look-up, not a search.\n\nThe article's value is in the *layer-boundary* framing (the bucket policy is the wrong layer to fix this), not in solver mechanics. Some bugs are small predicates with big stories.\n\nThe example is at `stave/examples/s3-dotgit-readable/`:\n\n```\ngo run ./examples/s3-dotgit-readable before\n```\n\nCaptured stdout (`expected/before-output.txt`):\n\n```\n=== before (.git/ exposed) ===\n  status: NON_COMPLIANT   total_assets=1   violations=1\n  CTL.S3.REPO.ARTIFACT.001 fired on 1 asset(s):\n    - arn:aws:s3:::acme-marketing-site   severity=medium   exposure_score=51.10\n  assertion: fires=true (expected) ✓\n```\n\nAfter the deployment pipeline is fixed:\n\n```\n=== after  (artefacts removed) ===\n  status: COMPLIANT   total_assets=1   violations=0\n  CTL.S3.REPO.ARTIFACT.001: no findings\n  assertion: fires=false (expected) ✓\n```\n\n`public_read` is still `true` in the after fixture. The bucket is still a public marketing site. The remediation is at the deployment-pipeline layer.\n\nThree options, escalating in robustness:\n\n**A. `rm -rf .git` before `s3 sync`.** Smallest change.\n\nWorks as long as nobody changes the deployment script:\n\n```\ngit clone \"$REPO\" build/\n# build steps...\nrm -rf build/.git\naws s3 sync build/ s3://acme-marketing-site/ --delete\n```\n\n**B. Use `git archive` instead of `git clone`.** Stronger — the source has no `.git/` directory at all:\n\n```\nmkdir build/\ngit archive --format=tar --remote=\"$REPO\" HEAD | tar -xC build/\n# build steps...\naws s3 sync build/ s3://acme-marketing-site/ --delete\n```\n\n**C. Two-directory build pipeline.** Strongest — the build output goes to `dist/`, the bucket only sees `dist/`:\n\n```\ngit clone \"$REPO\" src/\ncd src && build && cd ..\n# build/ is the source-of-truth checkout\n# dist/ is the artefact-only output\naws s3 sync dist/ s3://acme-marketing-site/ --delete\n```\n\nOption C is the right pattern for any non-trivial pipeline because it forces the team to be explicit about what is and isn't deployable. The artefact set is a separate output, not a filtered view of the source tree.\n\nThree layers prevent recurrence:\n\n**Pre-deploy artefact scan.** Before `aws s3 sync` runs, the deployment script scans the build directory for known-unsafe paths and aborts if any are present. A small helper, ~20 lines:\n\n```\nunsafe_paths=$(find dist/ -type d -name '.git' -o -name '.svn' \\\n  -o -name '.env' -o -name '.aws')\nif [ -n \"$unsafe_paths\" ]; then\n  echo \"ERROR: unsafe artefacts in dist/:\" >&2\n  echo \"$unsafe_paths\" >&2\n  exit 1\nfi\naws s3 sync dist/ s3://...\n```\n\nThe check runs in the same script that does the deploy, so the gate is in the right place.\n\n**Post-deploy invariant check.** `stave apply` runs against the post-deploy observation snapshot. The observation collector includes a step that probes the bucket for known-unsafe path patterns and emits `exposed_repo_artifacts: true/false`. The example shipped with this article is the template with the same predicate, same exit code 3 for any regression.\n\n**`.gitignore` + secret scanning.** Upstream of all of this: a `.gitignore` that includes `.env`, `*.key`, `credentials*`, and a pre-commit secret-scanner that prevents the secrets from ever reaching `.git/` in the first place. This is the layer that handles the \"secrets in old commits even after `git rm`\" failure mode. If the secret was never committed, the `.git/` disclosure has nothing of value to reveal.\n\n`git archive` or a separate `dist/` directory; `git clone` output never reaches `aws s3 sync`\n`.aws/`, `.ssh/` and aborts on any hit`stave apply` runs in CI against post-deploy observations; PRs that introduce a public bucket with `exposed_repo_artifacts: true` fail the gate\nThe bucket was always going to be public. The marketing site is *for the public*. The bug was that public got applied to one more directory than the team meant.\n\n*The example at [`stave/examples/s3-dotgit-readable/`](https://github.com/sufield/stave/tree/main/examples/s3-dotgit-readable) is a self-contained Go program that loads two fixture snapshots, runs `pkg/stave.Apply`, asserts that `CTL.S3.REPO.ARTIFACT.001` fires on the .git-exposed fixture and is silent on the cleaned one, and exits zero when both assertions hold. The remediation in the after fixture leaves `public_read: true` on purpose where the bucket is intentionally public; only the artefact paths needed removal. [Stave](https://github.com/sufield/stave) detects this pattern and 31 other H1-grounded scenarios from local AWS configuration snapshots, with no cloud credentials.*", "url": "https://wpnews.pro/news/source-code-in-a-public-bucket-when-git-survives-deployment", "canonical_source": "https://dev.to/bala_paranj_059d338e44e7e/source-code-in-a-public-bucket-when-git-survives-deployment-4nnp", "published_at": "2026-09-28 12:19:09+00:00", "updated_at": "2026-09-28 12:20:37.421582+00:00", "lang": "en", "topics": ["ai-crawlers", "developer-tools"], "entities": ["Mozilla", "HackerOne", "Amazon S3", "GitHub", "Git"], "also_reported_by": [], "alternates": {"html": "https://wpnews.pro/news/source-code-in-a-public-bucket-when-git-survives-deployment", "markdown": "https://wpnews.pro/news/source-code-in-a-public-bucket-when-git-survives-deployment.md", "text": "https://wpnews.pro/news/source-code-in-a-public-bucket-when-git-survives-deployment.txt", "jsonld": "https://wpnews.pro/news/source-code-in-a-public-bucket-when-git-survives-deployment.jsonld"}}