Skip to content

multiple unbounded io.ReadAll reads allow registry-controlled memory exhaustion (dos) #2204

@1seal

Description

@1seal

summary

go-containerregistry reads multiple untrusted http response bodies using io.ReadAll without an explicit size limit. a malicious (or compromised/misconfigured) registry endpoint, or a registry-controlled auth/token endpoint, can return an arbitrarily large response body and force unbounded memory allocation in the client process, leading to memory exhaustion and potential oom termination.

this is the same invariant violation across multiple callsites, so fixing only one path may leave others reachable.

note: this is being filed as a public issue after google bug hunters marked the report "won't fix (infeasible)" as a security escalation on 2026-01-30, and explicitly suggested public disclosure on github.

impact

  • availability: memory exhaustion in the client process (potential oom kill depending on limits)
  • attacker requirements: attacker controls the registry endpoint the client is configured to talk to, or an auth/token endpoint used during registry access
  • scope: client-side dos (no confidentiality/integrity impact expected)

affected versions

confirmed at:

  • v0.20.3 (commit c4dd792fa06c1f8b780ad90c8ab4f38b4eac05bd)

also confirmed present in:

  • v0.20.7 (same io.ReadAll callsites)

callsites (unbounded reads)

  • pkg/v1/remote/referrers.go:70 (Referrers / fetchReferrers) — io.ReadAll(resp.Body)
  • pkg/v1/remote/transport/error.go:164 (CheckError) — io.ReadAll(resp.Body)
  • pkg/v1/remote/transport/error.go:188 (retryError) — io.ReadAll(resp.Body)
  • pkg/v1/remote/transport/bearer.go:362 (refreshOauth) — io.ReadAll(resp.Body)
  • pkg/v1/remote/transport/bearer.go:406 (refreshBasic) — io.ReadAll(resp.Body)
  • pkg/v1/remote/image.go:129 (RawConfigFile) — io.ReadAll(body)

suggested remediation

replace io.ReadAll on untrusted http bodies with a bounded read that:

  • enforces a per-callsite maximum (e.g. a few mib for errors/token/referrers; higher but still bounded for config blobs)
  • detects truncation (read limit+1) and returns a clear “response body too large” error

example helper:

func readAllLimit(r io.Reader, max int64) ([]byte, error) {
    lr := io.LimitReader(r, max+1)
    b, err := io.ReadAll(lr)
    if err != nil {
        return nil, err
    }
    if int64(len(b)) > max {
        return nil, fmt.Errorf("response body exceeds %d bytes", max)
    }
    return b, nil
}

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions