Repository navigation
Refuse oversized image uploads at the PUT - #423
Merged
Merged
Conversation
|
Warning Review limit reachedYou've used all free OSS reviews for now. Wait for the free limit to reset to keep reviewing this public repository. Next included review available in 1 minute. View limit details
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. |
The upload URL signed no length, so a script holding one could store any size until the stale sweep. Sign the stated size into the URL: R2 refuses any other length before storing a byte. Every client since May sends the size and a real Content-Length.
The test now checks an independently worked out signature and that the URL is signed for the stated size. R2 refuses a wrong declared length, though not always with a 403, so the docs stop promising one answer.
SunkenInTime
force-pushed
the
t3/upload-size-limit
branch
from
November 1, 2026 14:17
ab6e461 to
05438c7
Compare
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Nothing changes on screen, so there is no picture. This run on real R2 is the before and after. The "before" row used a limit lowered to 1 MB, for the reason under "How far this got".
main)SignatureDoesNotMatchin 1.0 s for 40 MB, public URL 404mainINVALID_PAYLOADmain(mainalready refuses a stated size over the limit)INVALID_PAYLOADmainactive, public URL serves the same bytesmainactive, public URL serves the same bytesci: convex
Why
The 15 MB picture limit only ran at
images:completeUpload, after the bytes had landed. A script holding an upload URL could PUT any size and never call complete. The object then sat at a public URL until the stale sweep removed it about a day later. Reddit traffic is coming and a per-account storage cap is planned, and a cap that counts completed uploads would not see those objects.What this does
images:generateUploadUrlnow requires an exact positive integerbyteSizewithin the limit, andpresignR2PutUrlsigns that length into the URL (content-length;content-type;host). R2 checks the signature when the PUT arrives, so any other length gets a 403 before anything is stored.Cloudflare's docs don't say this works. The presigned URL page never mentions
Content-Length, and POST policies are unsupported, so I tested it. With a URL signed for 1024 bytes:SignatureDoesNotMatchContent-LengthContent-Length: 1024aws-chunkedclaiming a larger decoded sizeContent-Length: 1024with 2048 bytes sentThe check in
completeUploadstays. It still catches a limit lowered after a URL was signed.Old clients
No opt-in is needed. Shipped clients never see the difference, because the response shape is unchanged and
Content-Lengthstays out ofrequiredHeaders. The argument staysv.optionalin the validator and the handler requires it, so the published contract and the generated Dart don't change.byteSizeand a real length._putBytesWithProgressandmedia_bytes_source.darthash the same at all seven tags from 5.0.0 to 5.1.3, the last Deploy Web commit (9c47335b) and HEAD. Thehttplockfile entry, Flutter 3.44.4 and protocol 6 also match.IOClient, which setscontentLength. Web goes throughBrowserClient, which sends aUint8Arraybody, so the browser fills the header in. Edge sentcontent-length: 1954430and the real bucket's CORS rule let it through, sinceContent-Lengthisn't a preflight header.Server order doesn't matter here. An old web tab on the new server takes the same path as the new one.
How far this got
npx tsc --noEmitclean.npm run test:convex447 of 447.desktop-stable-v5.1.3+110tag. The only edit is a scratch patch that lets them reachhttp://127.0.0.1. Desktop ran throughtool/drivewith a real sign-in. Web ran in Edge on port 8765, through the Upload image dialog.The dev deployment's
R2_BUCKETisicarus-media, the bucket production uses, and its key can't reach any other bucket. So I kept over-limit writes out of it. The R2 probes used bodies of a few KB, themainrepro used 2 MB against a limit lowered to 1 MB, and the 40 MB PUTs only ran after the small probes showed R2 refuses without storing. Every object this work wrote there has been deleted by exact key.Run on dev
After rebasing onto main, I deployed this head to the dev deployment and ran release-configuration builds against it.
ICARUS_CLOUD_ENVIRONMENT=developmentresolves to dev, so neither build needed a URL patch.tool/drive/build.ps1, nothing editedactive, public URL serves the same bytesactive, public URL serves the same bytes5.0.0 has no driver, so it got a scratch harness: a call after
runApp, its sign-in in its own file, andicarus:registration skipped. Its upload queue,media_bytes_source.dartand repository are byte-identical to the tag. The picture went on the canvas through the app's ownaddImagein an open editor, and the upload was the unchanged queue. Theicarus:handler and the shared session file were the same before and after both runs.With a real test-account token on the same server:
SignatureDoesNotMatch, nothing storedINVALID_PAYLOAD, no URLThe 40 MB PUTs only ran after the 2 KB probe left nothing behind. Every test strategy is deleted, and all 7 exact object keys now answer 404 at the public URL.
What isn't covered


Confidence Score: 5/5
The PR appears safe to merge; no blocking issue was found.Summary
The PR requires an exact positive upload size within the configured limit and includes that size in the signed PUT URL.
Reviews (3) · Last reviewed commit: "Merge remote-tracking branch 'origin/mai..." · Reviewed by Greptile