Skip to content

Cap the bundle layer read in ociremote.Bundle - #5052

Open
arpitjain099 wants to merge 1 commit into
sigstore:mainfrom
arpitjain099:bundle-size-cap
Open

Cap the bundle layer read in ociremote.Bundle#5052
arpitjain099 wants to merge 1 commit into
sigstore:mainfrom
arpitjain099:bundle-size-cap

Conversation

@arpitjain099

Copy link
Copy Markdown
Contributor

Summary

Adds a size cap to the bundle layer read in ociremote.Bundle(). Follow-up to the private report on GHSA-cq2j-v5qh-cjqh, which @Hayden-IO closed with the note that DoS vectors do not need an advisory and that a patch adding a sensible limit would be welcome. This is that patch.

Bundle() decompressed the new-bundle-format attestation layer and read all of it into memory with no check, while the three sibling read sinks in pkg/oci all call payloadsize.CheckSize first: pkg/oci/internal/signature/layer.go, pkg/oci/static/file.go and pkg/oci/remote/remote.go.

Why the declared size alone is not enough

The three siblings read through Compressed(), so the size the registry declares genuinely bounds what they read. Bundle() is the one that goes through Uncompressed(), so the declared size does not bound the result and a small compressed blob can inflate past any limit.

So this does both:

  • checks the declared size first, so an oversized layer is rejected before anything is read, which keeps it consistent with the siblings
  • bounds the decompressed stream by the same limit, which is the part the declared size cannot cover

This matters more than a single read suggests. GetBundles in pkg/cosign/verify.go calls Bundle() for every manifest in a referrers index and swallows the error with continue, because non-Sigstore referrers are expected there. Without the second check, each oversized layer in an index was read in full before its error was reached.

Changes

internal/pkg/cosign/payload/size: exports MaxSize(), and CheckSize now calls it. A caller reading a decompressed stream needs the limit itself rather than a yes or no answer about a size it already knows. Behaviour is unchanged, including the fallback to the default when COSIGN_MAX_ATTACHMENT_SIZE fails to parse.

pkg/oci/remote/signatures.go: the declared-size check, plus a readCapped helper that reads one byte past the maximum so that hitting the limit is distinguishable from a payload that is exactly the maximum size.

Tests

pkg/oci/remote/signatures_capped_test.go, three cases: a payload at exactly the limit is accepted, one byte over is rejected, and a gzip stream inflating from roughly 64KiB to 64MiB is rejected against a 1MiB cap. That third one is the case the declared-size check cannot catch, and it asserts the compressed size is under the cap so the test cannot pass for the wrong reason.

With the cap removed, the two rejection tests fail:

--- FAIL: TestReadCappedRejectsOversizedPayload (0.00s)
--- FAIL: TestReadCappedRejectsDecompressionBomb (0.21s)

With it in place, ./pkg/oci/... and ./internal/pkg/cosign/payload/size/... are green, and go build ./... and go vet are clean.

A note on the original report

I verified the code paths by reading them at v3.1.3 and I said so in the report: I did not stand up a crafted registry, so I had no measured RSS figure. The decompression behaviour is now covered by the test above rather than by reasoning.

Bundle() decompressed the new-bundle-format attestation layer and read all of
it into memory with no size check, while every other layer read in pkg/oci
calls payloadsize.CheckSize first. A registry the user does not control could
make cosign verify-attestation --new-bundle-format allocate without bound from
a small compressed layer.

The size a registry declares is the compressed size, and this is the only one
of these reads that goes through Uncompressed(), so checking that size is
necessary but not sufficient. This does both: the declared size is checked
first so an oversized layer is rejected before anything is read, and the
decompressed stream is then bounded by the same limit.

GetBundles calls Bundle() for every manifest in a referrers index and swallows
the error with continue, since non-Sigstore referrers are expected there, so
without the second check each oversized layer in an index was read in full
before its error was reached.

MaxSize is exported from the payload size package because a caller reading a
decompressed stream needs the limit itself rather than a yes or no answer
about a size it already knows.

Signed-off-by: Arpit Jain <arpitjain099@gmail.com>
@arpitjain099
arpitjain099 requested a review from a team as a code owner August 10, 2026 22:24
@codecov

codecov Bot commented Aug 10, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 61.90476% with 8 lines in your changes missing coverage. Please review.
✅ Project coverage is 39.49%. Comparing base (2ef6022) to head (f06de04).
⚠️ Report is 821 commits behind head on main.

Files with missing lines Patch % Lines
pkg/oci/remote/signatures.go 42.85% 7 Missing and 1 partial ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##             main    #5052      +/-   ##
==========================================
- Coverage   40.10%   39.49%   -0.61%     
==========================================
  Files         155      207      +52     
  Lines       10044    13047    +3003     
==========================================
+ Hits         4028     5153    +1125     
- Misses       5530     7162    +1632     
- Partials      486      732     +246     

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant