fix(web): cap maximum upload size#1930
Conversation
|
Paragon Review Skipped Hi @richiemcilroy! Your Polarity credit balance is insufficient to complete this review. Please visit https://app.paragon.run to finish your review. |
|
@greptileai please review the PR |
|
@greptileai please review the PR |
|
@greptileai please review the PR |
|
Superagent didn't find any vulnerabilities or security issues in this PR. |
|
@greptileai please review the PR |
| const callerMin = | ||
| callerRange && | ||
| typeof callerRange[1] === "number" && | ||
| Number.isFinite(callerRange[1]) | ||
| ? Math.max(0, callerRange[1]) | ||
| : 0; | ||
| const callerMax = | ||
| callerRange && | ||
| typeof callerRange[2] === "number" && | ||
| Number.isFinite(callerRange[2]) | ||
| ? Math.max(0, callerRange[2]) | ||
| : MAX_UPLOAD_BYTES; | ||
| const otherConditions = callerConditions.filter( | ||
| (condition) => !isLengthRange(condition), | ||
| ); | ||
| return createPresignedPost(client, { | ||
| ...args, | ||
| Conditions: [ | ||
| [ | ||
| "content-length-range", | ||
| callerMin, | ||
| Math.min(callerMax, MAX_UPLOAD_BYTES), | ||
| ], | ||
| ...otherConditions, | ||
| ], |
There was a problem hiding this comment.
One edge case: if the caller supplies a content-length-range where min exceeds MAX_UPLOAD_BYTES (or min > max), we’ll generate an invalid policy (min > max). Might be worth clamping + normalizing so we always emit a valid range.
| const callerMin = | |
| callerRange && | |
| typeof callerRange[1] === "number" && | |
| Number.isFinite(callerRange[1]) | |
| ? Math.max(0, callerRange[1]) | |
| : 0; | |
| const callerMax = | |
| callerRange && | |
| typeof callerRange[2] === "number" && | |
| Number.isFinite(callerRange[2]) | |
| ? Math.max(0, callerRange[2]) | |
| : MAX_UPLOAD_BYTES; | |
| const otherConditions = callerConditions.filter( | |
| (condition) => !isLengthRange(condition), | |
| ); | |
| return createPresignedPost(client, { | |
| ...args, | |
| Conditions: [ | |
| [ | |
| "content-length-range", | |
| callerMin, | |
| Math.min(callerMax, MAX_UPLOAD_BYTES), | |
| ], | |
| ...otherConditions, | |
| ], | |
| const callerMin = | |
| callerRange && | |
| typeof callerRange[1] === "number" && | |
| Number.isFinite(callerRange[1]) | |
| ? Math.max(0, callerRange[1]) | |
| : 0; | |
| const callerMax = | |
| callerRange && | |
| typeof callerRange[2] === "number" && | |
| Number.isFinite(callerRange[2]) | |
| ? Math.max(0, callerRange[2]) | |
| : MAX_UPLOAD_BYTES; | |
| const minBytes = Math.min(callerMin, MAX_UPLOAD_BYTES); | |
| const maxBytes = Math.min(Math.max(callerMax, minBytes), MAX_UPLOAD_BYTES); | |
| const otherConditions = callerConditions.filter( | |
| (condition) => !isLengthRange(condition), | |
| ); | |
| return createPresignedPost(client, { | |
| ...args, | |
| Conditions: [ | |
| ["content-length-range", minBytes, maxBytes], | |
| ...otherConditions, | |
| ], | |
| Bucket: provider.bucket, | |
| Key: key, | |
| }); |
| // Server-side backstop for the maximum upload size. Presigned POST URLs | ||
| // enforce a content-length-range policy, but presigned PUT part URLs | ||
| // cannot enforce a total size, so reject an oversized assembled upload | ||
| // here before persisting (and before paying to assemble it). Part sizes |
There was a problem hiding this comment.
Worth calling out: this guard trusts part.size from the request body. A malicious client can under-report sizes and still complete an oversized MPU (S3 only cares about PartNumber/ETag). If the intent is a hard storage cap, you probably need to sum authoritative sizes from S3 (ListParts) or enforce Content-Length on the presigned part PUTs.
Caps the maximum upload size on presigned POST uploads and at multipart completion to prevent unbounded storage usage.
Greptile Summary
This PR caps upload sizes at 100 GiB by enforcing a
content-length-rangepolicy on all presigned POST URLs and adding a server-side total-size gate at multipart completion. It also tightens the part-size Zod schema toz.number().int().nonnegative()and importsMAX_UPLOAD_BYTESfrom a single exported source of truth.S3BucketAccess.ts: Injects acontent-length-rangecondition into every presigned POST, capping the effective max atMAX_UPLOAD_BYTESeven when a caller supplies a looser range, and exports the constant.multipart.ts: Sums client-reported part sizes againstMAX_UPLOAD_BYTESbefore callingCompleteMultipartUpload; on excess it aborts the incomplete MPU, deletes thevideoUploadsrow (each step independently guarded), and returns 413. Partsizeis now validated as a non-negative integer, closing both the negative-value and fractional-value bypass paths.index.ts: Re-exportsMAX_UPLOAD_BYTESso it can be shared without duplication.Confidence Score: 5/5
This PR is safe to merge; the upload cap and schema hardening are self-contained and do not alter any existing data path.
The changes are additive guards — a new schema constraint, a new presigned-POST policy injection, and a new total-size check with cleanup. All previously flagged issues (negative-size bypass, MAX_UPLOAD_BYTES duplication, fractional-size bypass) have been addressed. The cleanup logic for the 413 path correctly handles abort and delete independently so a failure in one step does not suppress the other.
No files require special attention.
Important Files Changed
Comments Outside Diff (1)
apps/web/app/api/upload/[...route]/multipart.ts, line 293-299 (link)z.number()accepts any real number, including negatives. A client that submitssize: -Nfor each part makestotalUploadSizenegative, sototalUploadSize > MAX_UPLOAD_BYTESis alwaysfalseand the guard never fires. Adding.nonnegative()(i.e.,z.number().nonnegative()) to thesizefield closes this bypass at the schema layer before the cap logic is ever reached.Prompt To Fix With AI
Reviews (5): Last reviewed commit: "fix(web): require integer part sizes and..." | Re-trigger Greptile