-
Notifications
You must be signed in to change notification settings - Fork 1.8k
fix(web): cap maximum upload size #1930
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from 1 commit
24a0832
4c0d4e7
6637e5c
a663a44
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -34,6 +34,10 @@ const MEDIA_SERVER_PRESIGNED_PUT_EXPIRES_SECONDS = 3 * 60 * 60; | |
| // Clients stop at the cap and then finalize, so reported durations can land | ||
| // slightly past the limit for honest recordings. | ||
| const FREE_PLAN_DURATION_GRACE_SECONDS = 30; | ||
| // Upper bound on a completed multipart upload to prevent unbounded storage | ||
| // abuse. Generous on purpose so legitimate long/high-bitrate recordings are | ||
| // never blocked; kept in sync with MAX_UPLOAD_BYTES in S3BucketAccess.ts. | ||
| const MAX_UPLOAD_BYTES = 100 * 1024 * 1024 * 1024; // 100 GiB | ||
|
richiemcilroy marked this conversation as resolved.
Outdated
|
||
|
|
||
| const runPromiseAnyEnv = runPromise as <A, E>( | ||
| effect: Effect.Effect<A, E, unknown>, | ||
|
|
@@ -399,6 +403,40 @@ app.post( | |
| } | ||
| } | ||
|
|
||
| // 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. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Worth calling out: this guard trusts |
||
| // are client-reported, so this raises the bar rather than enforcing | ||
| // authoritatively. | ||
| const totalUploadSize = parts.reduce((acc, part) => acc + part.size, 0); | ||
|
richiemcilroy marked this conversation as resolved.
Outdated
|
||
| if (totalUploadSize > MAX_UPLOAD_BYTES) { | ||
| // Avoid leaving the parts as incomplete-MPU storage and a stale | ||
| // videoUploads row, mirroring the free-plan rejection cleanup. The | ||
| // 413 stands regardless of cleanup success. | ||
| yield* Effect.gen(function* () { | ||
|
richiemcilroy marked this conversation as resolved.
|
||
| const [bucket] = yield* Storage.getAccessForVideo(video); | ||
| yield* bucket.multipart.abort(fileKey, uploadId); | ||
| yield* db.use((db) => | ||
| db | ||
| .delete(Db.videoUploads) | ||
| .where(eq(Db.videoUploads.videoId, videoId)), | ||
| ); | ||
| }).pipe( | ||
| Effect.catchAll((error) => | ||
| Effect.logError( | ||
| "Failed to clean up rejected oversized multipart upload", | ||
| error, | ||
| ), | ||
| ), | ||
| ); | ||
|
|
||
| c.status(413); | ||
| return c.text( | ||
| "Upload exceeds the maximum allowed size and cannot be completed.", | ||
| ); | ||
| } | ||
|
|
||
| return yield* Effect.gen(function* () { | ||
| const [bucket] = yield* Storage.getAccessForVideo(video); | ||
|
|
||
|
|
||
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
|
@@ -14,6 +14,11 @@ import { S3BucketClientProvider } from "./S3BucketClientProvider.ts"; | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const DEFAULT_PRESIGNED_GET_EXPIRES_SECONDS = 3600; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const DEFAULT_PRESIGNED_PUT_EXPIRES_SECONDS = 3600; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| // Upper bound on a single upload to prevent unbounded storage abuse. Generous | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| // on purpose so legitimate long/high-bitrate recordings are never blocked; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| // tune here if the product ever needs a larger ceiling. | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| export const MAX_UPLOAD_BYTES = 100 * 1024 * 1024 * 1024; // 100 GiB | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| type NodeReadableWebStream = Parameters<typeof Readable.fromWeb>[0]; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const wrapS3Promise = <T>( | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
@@ -269,6 +274,14 @@ export const createS3BucketAccess = Effect.gen(function* () { | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| Effect.map((client) => | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| createPresignedPost(client, { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| ...args, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| // Enforce an upper bound on the uploaded object size. The POST | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| // policy rejects the upload at S3 if the body exceeds this, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| // closing the unbounded-storage hole for presigned POSTs. | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| // Any caller-supplied conditions are preserved. | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| Conditions: [ | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
richiemcilroy marked this conversation as resolved.
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| ["content-length-range", 0, MAX_UPLOAD_BYTES], | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| ...(args.Conditions ?? []), | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| ], | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
Comment on lines
+288
to
+312
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. One edge case: if the caller supplies a
Suggested change
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| Bucket: provider.bucket, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| Key: key, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| }), | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
Uh oh!
There was an error while loading. Please reload this page.