Skip to content

feat(upload): Skip decompression - #6358

Draft
jjbayer wants to merge 2 commits into
masterfrom
feat/upload-no-decompress
Draft

feat(upload): Skip decompression#6358
jjbayer wants to merge 2 commits into
masterfrom
feat/upload-no-decompress

Conversation

@jjbayer

@jjbayer jjbayer commented Sep 8, 2026

Copy link
Copy Markdown
Member

jjbayer and others added 2 commits September 8, 2026 13:48
The `/upload` PATCH endpoint no longer decompresses request bodies. A
`zstd` body is forwarded or stored verbatim and the algorithm is recorded
on the object via objectstore's `precompressed` API. Any other content
encoding is rejected with 415.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@linear-code

linear-code Bot commented Sep 8, 2026

Copy link
Copy Markdown

INGEST-1162

Comment on lines +161 to +164
/// The compression the stream is already encoded with, if any.
///
/// The stream is uploaded verbatim and the algorithm is recorded on the object.
pub compression: Option<Compression>,

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Precompressed uploads bypass decompressed size limits (zip bomb)

Client-controlled Content-Encoding is stored via precompressed metadata while only the compressed wire size is limited, so a small zstd body can expand far past max_upload_size when objectstore or downloads auto-decompress.

Evidence
  • The upload PATCH route is merged after RequestDecompressionLayer, so bodies are no longer decompressed at the edge (endpoints/mod.rs raw_routes).
  • body_compression() accepts client Content-Encoding (e.g. zstd) and passes it through upload::Stream.compression into this objectstore::Stream.compression field.
  • attempt_upload calls request.precompressed(compression) and stores the body verbatim; RequestBodyLimitLayer / BoundedStream only cap compressed bytes.
  • Integration coverage (test_objectstore_precompressed) asserts downloads auto-decompress the annotated object, so a high-ratio compressed payload expands on read without an ingest-time decompressed size check.
Also found at 1 additional location
  • relay-server/src/services/upload.rs:14-20

Identified by Warden · security-review · 7J5-ST5

Comment on lines 835 to +845
@@ -837,6 +842,7 @@ impl ObjectstoreServiceInner {
body: TakeOnce::new(stream),
upload_ref,
retention,
compression,

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Precompressed upload path bounds only wire size, not decompressed size

Client Content-Encoding is plumbed into objectstore precompressed() while RequestBodyLimitLayer/BoundedStream only cap compressed bytes, so a small zstd body (e.g. with Upload-Defer-Length) can expand far past max_upload_size on download; enforce a decompressed/logical size cap before accepting precompressed uploads.

Evidence
  • handle_stream now forwards attacker-controlled compression into Upload::Stream (this hunk); attempt_upload calls request.precompressed(compression) and stores the body verbatim.
  • Upload PATCH is merged outside RequestDecompressionLayer (endpoints/mod.rs raw_routes), and body_compression() accepts client Content-Encoding (e.g. zstd) without inflating or measuring output size.
  • RequestBodyLimitLayer / BoundedStream only cap wire bytes (max_upload_size); with Upload-Defer-Length the bound is (1, max_upload_size) on compressed bytes only.
  • test_objectstore_precompressed asserts session.get(key) auto-decompresses the annotated object, so a high-ratio payload expands on read with no ingest-time decompressed-size check.
Also found at 1 additional location
  • relay-server/src/services/objectstore.rs:974-979

Identified by Warden · wrdn-dos-review · XX6-2T3

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