Skip to content

Commit d1b3230

Browse files
committed
fix(uploads): enforce attachment size limits on resolved bytes
Size limits were checked against userFile.size (source metadata) before resolution, but a generated doc resolves to a larger compiled binary — so a small-source doc could pass the pre-check yet exceed the service limit. Add a post-resolution check on the actual resolved bytes (mirroring docusign/vanta) across gmail send/draft/edit-draft, smtp, outlook send/draft, telegram, sftp, and teams; the cheap source pre-check stays as an early reject.
1 parent 813c8dc commit d1b3230

9 files changed

Lines changed: 126 additions & 0 deletions

File tree

apps/sim/app/api/tools/gmail/draft/route.ts

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -118,6 +118,21 @@ export const POST = withRouteHandler(async (request: NextRequest) => {
118118
)
119119
}
120120

121+
// Re-check size against the RESOLVED bytes: a generated doc stores small
122+
// source metadata but resolves to a larger compiled binary, so the source
123+
// pre-check above can pass a payload that exceeds the limit.
124+
const resolvedTotal = resolved.reduce((sum, r) => sum + r.buffer.length, 0)
125+
if (resolvedTotal > maxSize) {
126+
const sizeMB = (resolvedTotal / (1024 * 1024)).toFixed(2)
127+
return NextResponse.json(
128+
{
129+
success: false,
130+
error: `Total attachment size (${sizeMB}MB) exceeds Gmail's limit of 25MB`,
131+
},
132+
{ status: 400 }
133+
)
134+
}
135+
121136
const attachmentBuffers = attachments.map((file, i) => ({
122137
filename: file.name,
123138
mimeType: resolved[i].contentType || file.type || 'application/octet-stream',

apps/sim/app/api/tools/gmail/edit-draft/route.ts

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -114,6 +114,21 @@ export const POST = withRouteHandler(async (request: NextRequest) => {
114114
)
115115
}
116116

117+
// Re-check size against the RESOLVED bytes: a generated doc stores small
118+
// source metadata but resolves to a larger compiled binary, so the source
119+
// pre-check above can pass a payload that exceeds the limit.
120+
const resolvedTotal = resolved.reduce((sum, r) => sum + r.buffer.length, 0)
121+
if (resolvedTotal > maxSize) {
122+
const sizeMB = (resolvedTotal / (1024 * 1024)).toFixed(2)
123+
return NextResponse.json(
124+
{
125+
success: false,
126+
error: `Total attachment size (${sizeMB}MB) exceeds Gmail's limit of 25MB`,
127+
},
128+
{ status: 400 }
129+
)
130+
}
131+
117132
const attachmentBuffers = attachments.map((file, i) => ({
118133
filename: file.name,
119134
mimeType: resolved[i].contentType || file.type || 'application/octet-stream',

apps/sim/app/api/tools/gmail/send/route.ts

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -118,6 +118,21 @@ export const POST = withRouteHandler(async (request: NextRequest) => {
118118
)
119119
}
120120

121+
// Re-check size against the RESOLVED bytes: a generated doc stores small
122+
// source metadata but resolves to a larger compiled binary, so the source
123+
// pre-check above can pass a payload that exceeds Gmail's limit.
124+
const resolvedTotal = resolved.reduce((sum, r) => sum + r.buffer.length, 0)
125+
if (resolvedTotal > maxSize) {
126+
const sizeMB = (resolvedTotal / (1024 * 1024)).toFixed(2)
127+
return NextResponse.json(
128+
{
129+
success: false,
130+
error: `Total attachment size (${sizeMB}MB) exceeds Gmail's limit of 25MB`,
131+
},
132+
{ status: 400 }
133+
)
134+
}
135+
121136
const attachmentBuffers = attachments.map((file, i) => ({
122137
filename: file.name,
123138
mimeType: resolved[i].contentType || file.type || 'application/octet-stream',

apps/sim/app/api/tools/outlook/draft/route.ts

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -131,6 +131,21 @@ export const POST = withRouteHandler(async (request: NextRequest) => {
131131
)
132132
}
133133

134+
// Re-check size against the RESOLVED bytes: a generated doc stores small
135+
// source metadata but resolves to a larger compiled binary, so the source
136+
// pre-check above can pass a payload that exceeds the limit.
137+
const resolvedTotal = resolved.reduce((sum, r) => sum + r.buffer.length, 0)
138+
if (resolvedTotal > maxSize) {
139+
const sizeMB = (resolvedTotal / (1024 * 1024)).toFixed(2)
140+
return NextResponse.json(
141+
{
142+
success: false,
143+
error: `Total attachment size (${sizeMB}MB) exceeds Outlook's limit of 4MB per request`,
144+
},
145+
{ status: 400 }
146+
)
147+
}
148+
134149
const attachmentObjects = attachments.map((file, i) => ({
135150
'@odata.type': '#microsoft.graph.fileAttachment',
136151
name: file.name,

apps/sim/app/api/tools/outlook/send/route.ts

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -131,6 +131,21 @@ export const POST = withRouteHandler(async (request: NextRequest) => {
131131
)
132132
}
133133

134+
// Re-check size against the RESOLVED bytes: a generated doc stores small
135+
// source metadata but resolves to a larger compiled binary, so the source
136+
// pre-check above can pass a payload that exceeds the limit.
137+
const resolvedTotal = resolved.reduce((sum, r) => sum + r.buffer.length, 0)
138+
if (resolvedTotal > maxSize) {
139+
const sizeMB = (resolvedTotal / (1024 * 1024)).toFixed(2)
140+
return NextResponse.json(
141+
{
142+
success: false,
143+
error: `Total attachment size (${sizeMB}MB) exceeds Microsoft Graph API limit of 3MB per request`,
144+
},
145+
{ status: 400 }
146+
)
147+
}
148+
134149
const attachmentObjects = attachments.map((file, i) => ({
135150
'@odata.type': '#microsoft.graph.fileAttachment',
136151
name: file.name,

apps/sim/app/api/tools/sftp/upload/route.ts

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -110,6 +110,17 @@ export const POST = withRouteHandler(async (request: NextRequest) => {
110110
)
111111
const { buffer } = await downloadServableFileFromStorage(file, requestId, logger)
112112

113+
// Re-check size against the RESOLVED bytes: a generated doc stores small
114+
// source metadata but resolves to a larger compiled binary, so the source
115+
// pre-check above can pass a payload that exceeds the limit.
116+
if (buffer.length > maxSize) {
117+
const sizeMB = (buffer.length / (1024 * 1024)).toFixed(2)
118+
return NextResponse.json(
119+
{ success: false, error: `Total file size (${sizeMB}MB) exceeds limit of 100MB` },
120+
{ status: 400 }
121+
)
122+
}
123+
113124
const safeFileName = sanitizeFileName(file.name)
114125
const fullRemotePath = remotePath.endsWith('/')
115126
? `${remotePath}${safeFileName}`

apps/sim/app/api/tools/smtp/send/route.ts

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -151,6 +151,21 @@ export const POST = withRouteHandler(async (request: NextRequest) => {
151151
)
152152
}
153153

154+
// Re-check size against the RESOLVED bytes: a generated doc stores small
155+
// source metadata but resolves to a larger compiled binary, so the source
156+
// pre-check above can pass a payload that exceeds the limit.
157+
const resolvedTotal = resolved.reduce((sum, r) => sum + r.buffer.length, 0)
158+
if (resolvedTotal > maxSize) {
159+
const sizeMB = (resolvedTotal / (1024 * 1024)).toFixed(2)
160+
return NextResponse.json(
161+
{
162+
success: false,
163+
error: `Total attachment size (${sizeMB}MB) exceeds SMTP limit of 25MB`,
164+
},
165+
{ status: 400 }
166+
)
167+
}
168+
154169
const attachmentBuffers = attachments.map((file, i) => ({
155170
filename: file.name,
156171
content: resolved[i].buffer,

apps/sim/app/api/tools/telegram/send-document/route.ts

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -113,6 +113,20 @@ export const POST = withRouteHandler(async (request: NextRequest) => {
113113
)
114114
}
115115

116+
// Re-check size against the RESOLVED bytes: a generated doc stores small
117+
// source metadata but resolves to a larger compiled binary, so the source
118+
// pre-check above can pass a payload that exceeds the limit.
119+
if (buffer.length > maxSize) {
120+
const sizeMB = (buffer.length / (1024 * 1024)).toFixed(2)
121+
return NextResponse.json(
122+
{
123+
success: false,
124+
error: `The following files exceed Telegram's 50MB limit: ${userFile.name} (${sizeMB}MB)`,
125+
},
126+
{ status: 400 }
127+
)
128+
}
129+
116130
const resolvedMimeType = contentType || userFile.type || 'application/octet-stream'
117131
const filesOutput = [
118132
{

apps/sim/tools/microsoft_teams/server-utils.ts

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -81,6 +81,17 @@ export async function uploadFilesForTeamsMessage(params: {
8181

8282
// Download file from storage
8383
const { buffer, contentType } = await downloadServableFileFromStorage(file, requestId, log)
84+
85+
// Re-check size against the RESOLVED bytes: a generated doc stores small
86+
// source metadata but resolves to a larger compiled binary, so the source
87+
// pre-check above can pass a payload that exceeds the limit.
88+
if (buffer.length > MAX_TEAMS_FILE_SIZE) {
89+
const sizeMB = (buffer.length / (1024 * 1024)).toFixed(2)
90+
throw new Error(
91+
`File "${file.name}" (${sizeMB}MB) exceeds the 4MB limit for Teams attachments. Use smaller files or upload to SharePoint/OneDrive first.`
92+
)
93+
}
94+
8495
const resolvedMimeType = contentType || file.type || 'application/octet-stream'
8596
filesOutput.push({
8697
name: file.name,

0 commit comments

Comments
 (0)