Skip to content

Commit c0dc91e

Browse files
committed
fix(share): close two holes in the public content route
Both from Cursor, both real, both in the byte-range work rather than the view migration. **The per-share ceiling was bypassable with any non-start Range.** The limit was gated on `isReadStart(range)` so a player scrubbing a video would not 429 every other visitor to the link. But that predicate was read before knowing whether the file was media, and only media honours `Range` — every other response ignores the header and returns the whole body. So `Range: bytes=1-` on a shared PDF took the full download and never touched the share budget. The exemption now applies only where the response actually is byte-served. Media still gets its free seeks; everything else is charged unconditionally. **Shares classified media differently than the workspace does.** This route keyed off the stored `file.contentType`; `serve/[...path]` keys off `getContentType(displayName)`. Uploads routinely land as `application/octet-stream`, so a shared `.mp4` fell out of the byte-range branch, lost `Accept-Ranges`, and could not be scrubbed — while the identical file stayed seekable inside the workspace. Now classified by filename as well as stored type, which is a superset of what the workspace path accepts, so nothing that is seekable there is unseekable here. `rangeHeader` is read once at the top now instead of twice, since both the limit decision and the audit row need it. Suite: 20951 passed. files API: 267 passed.
1 parent ce234f4 commit c0dc91e

1 file changed

Lines changed: 23 additions & 8 deletions

File tree

  • apps/sim/app/api/files/public/[token]/content

apps/sim/app/api/files/public/[token]/content/route.ts

Lines changed: 23 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -68,6 +68,21 @@ export const GET = withRouteHandler(
6868
return NextResponse.json({ error: auth.error ?? 'auth_required_password' }, { status: 401 })
6969
}
7070

71+
const { file } = resolved
72+
const rangeHeader = request.headers.get('range')
73+
74+
/**
75+
* Whether this file is byte-served. Classified from the filename as well
76+
* as the stored type, matching how the workspace serve path decides
77+
* (`getContentType(displayName)`): uploads frequently land as
78+
* `application/octet-stream`, and keying only off `file.contentType` meant
79+
* a shared `.mp4` silently lost `Accept-Ranges` while the very same file
80+
* stayed seekable inside the workspace.
81+
*/
82+
const isMedia =
83+
isMediaContentType(file.contentType) ||
84+
isMediaContentType(getContentType(file.originalName))
85+
7186
/**
7287
* The share is only known after the token resolves, so the aggregate
7388
* per-share ceiling is enforced here rather than alongside the per-IP
@@ -77,28 +92,28 @@ export const GET = withRouteHandler(
7792
*
7893
* A seek within an in-progress playback is charged nothing: the ceiling is
7994
* shared by every visitor to the link, so counting each of a player's range
80-
* requests would let one person scrubbing a video 429 everyone else. Same
81-
* rule as the audit row below, from the same predicate.
95+
* requests would let one person scrubbing a video 429 everyone else.
96+
*
97+
* That exemption is scoped to responses that actually honour `Range`.
98+
* Every other path ignores the header and returns the whole body, so
99+
* reading the predicate unconditionally let `Range: bytes=1-` on any
100+
* non-media file collect the full download for free.
82101
*/
83-
if (isReadStart(request.headers.get('range'))) {
102+
if (!isMedia || isReadStart(rangeHeader)) {
84103
const shareLimited = await enforcePerShareRateLimit('content', resolved.share.id)
85104
if (shareLimited) return shareLimited
86105
}
87106

88-
const { file } = resolved
89-
90107
/**
91108
* Media is byte-served so a shared video or track is seekable and never
92109
* resident in memory on either side. Taken before the download because
93110
* the buffered path below exists for the generated-document swap, which
94111
* no media file can be subject to.
95112
*/
96-
if (isMediaContentType(file.contentType)) {
113+
if (isMedia) {
97114
const head = await headObject(file.key, 'workspace')
98115
if (!head) throw new FileNotFoundError('Not found')
99116

100-
const rangeHeader = request.headers.get('range')
101-
102117
logger.info('Public shared media served', {
103118
shareId: resolved.share.id,
104119
key: file.key,

0 commit comments

Comments
 (0)