security(upload): cap upload size to prevent memody/disk consumption

add OXICLOUD_CHUNK_MAX_BYTES which correspond to the max upload chunk allowed
    (differs from OXICLOUD_MAX_UPLOAD_SIZE which is the max total size of a file)
    hurl test validate the change

    Streams the request body straight to the chunk file with peak heap of
    ~one HTTP frame, regardless of chunk size or the configured cap. The
    `storage.chunk_max_bytes` config (env `OXICLOUD_CHUNK_MAX_BYTES`,
    default 100 MB) bounds a single PUT — separate from `max_upload_size`
    which governs whole-file uploads. Without this separation, a client
    could submit a chunk up to the whole-file cap (10 GB default) and
    monopolise server memory.
This commit is contained in:
Edouard Vanbelle
2026-06-08 21:14:04 +02:00
parent 233531bce5
commit 5e638691ad
9 changed files with 345 additions and 20 deletions
@@ -52,7 +52,30 @@ impl NextcloudChunkedUploadService {
Ok(())
}
/// Store a chunk in the session directory.
/// Resolve and validate the filesystem path for a chunk file.
///
/// Public so the interface layer can stream an HTTP body straight into
/// the chunk file without copying through the service. The service
/// retains responsibility for path-component validation; the caller
/// owns the I/O (open, write, fsync, size enforcement, cleanup on
/// failure). All three `validate_path_component` calls run before the
/// path is constructed, so a returned `PathBuf` is always inside
/// `base_dir/{user}/{upload_id}`.
pub fn safe_chunk_path(
&self,
user: &str,
upload_id: &str,
chunk_name: &str,
) -> Result<PathBuf> {
Self::validate_path_component(chunk_name, "chunk_name")?;
Ok(self.safe_session_dir(user, upload_id)?.join(chunk_name))
}
/// Store a chunk in the session directory. Buffers `data` in memory —
/// use [`safe_chunk_path`](Self::safe_chunk_path) + the
/// `interfaces/upload_spool::stream_body_to_path` helper to stream the
/// HTTP body directly to disk and avoid materialising the whole chunk
/// in RAM.
pub async fn store_chunk(
&self,
user: &str,
@@ -60,8 +83,7 @@ impl NextcloudChunkedUploadService {
chunk_name: &str,
data: &[u8],
) -> Result<()> {
Self::validate_path_component(chunk_name, "chunk_name")?;
let chunk_path = self.safe_session_dir(user, upload_id)?.join(chunk_name);
let chunk_path = self.safe_chunk_path(user, upload_id, chunk_name)?;
let mut file = fs::File::create(&chunk_path)
.await
.map_err(|e| DomainError::internal_error("ChunkedUpload", e.to_string()))?;