Skip to content

Commit e5d3049

Browse files
authored
fix(slack): resolve file metadata via files.info when event payload is partial (#3176)
1 parent b3dbb44 commit e5d3049

File tree

1 file changed

+66
-21
lines changed

1 file changed

+66
-21
lines changed

apps/sim/lib/webhooks/utils.server.ts

Lines changed: 66 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -527,17 +527,61 @@ export async function validateTwilioSignature(
527527
}
528528
}
529529

530-
const SLACK_FILE_HOSTS = new Set(['files.slack.com', 'files-pri.slack.com'])
531530
const SLACK_MAX_FILE_SIZE = 50 * 1024 * 1024 // 50 MB
532-
const SLACK_MAX_FILES = 10
531+
const SLACK_MAX_FILES = 15
532+
533+
/**
534+
* Resolves the full file object from the Slack API when the event payload
535+
* only contains a partial file (e.g. missing url_private due to file_access restrictions).
536+
* @see https://docs.slack.dev/reference/methods/files.info
537+
*/
538+
async function resolveSlackFileInfo(
539+
fileId: string,
540+
botToken: string
541+
): Promise<{ url_private?: string; name?: string; mimetype?: string; size?: number } | null> {
542+
try {
543+
const response = await fetch(
544+
`https://slack.com/api/files.info?file=${encodeURIComponent(fileId)}`,
545+
{
546+
headers: { Authorization: `Bearer ${botToken}` },
547+
}
548+
)
549+
550+
const data = (await response.json()) as {
551+
ok: boolean
552+
error?: string
553+
file?: Record<string, any>
554+
}
555+
556+
if (!data.ok || !data.file) {
557+
logger.warn('Slack files.info failed', { fileId, error: data.error })
558+
return null
559+
}
560+
561+
return {
562+
url_private: data.file.url_private,
563+
name: data.file.name,
564+
mimetype: data.file.mimetype,
565+
size: data.file.size,
566+
}
567+
} catch (error) {
568+
logger.error('Error calling Slack files.info', {
569+
fileId,
570+
error: error instanceof Error ? error.message : String(error),
571+
})
572+
return null
573+
}
574+
}
533575

534576
/**
535577
* Downloads file attachments from Slack using the bot token.
536578
* Returns files in the format expected by WebhookAttachmentProcessor:
537579
* { name, data (base64 string), mimeType, size }
538580
*
581+
* When the event payload contains partial file objects (missing url_private),
582+
* falls back to the Slack files.info API to resolve the full file metadata.
583+
*
539584
* Security:
540-
* - Validates each url_private against allowlisted Slack file hosts
541585
* - Uses validateUrlWithDNS + secureFetchWithPinnedIP to prevent SSRF
542586
* - Enforces per-file size limit and max file count
543587
*/
@@ -549,30 +593,31 @@ async function downloadSlackFiles(
549593
const downloaded: Array<{ name: string; data: string; mimeType: string; size: number }> = []
550594

551595
for (const file of filesToProcess) {
552-
const urlPrivate = file.url_private as string | undefined
553-
if (!urlPrivate) {
554-
continue
555-
}
556-
557-
// Validate the URL points to a known Slack file host
558-
let parsedUrl: URL
559-
try {
560-
parsedUrl = new URL(urlPrivate)
561-
} catch {
562-
logger.warn('Slack file has invalid url_private, skipping', { fileId: file.id })
563-
continue
596+
let urlPrivate = file.url_private as string | undefined
597+
let fileName = file.name as string | undefined
598+
let fileMimeType = file.mimetype as string | undefined
599+
let fileSize = file.size as number | undefined
600+
601+
// If url_private is missing, resolve via files.info API
602+
if (!urlPrivate && file.id) {
603+
const resolved = await resolveSlackFileInfo(file.id, botToken)
604+
if (resolved?.url_private) {
605+
urlPrivate = resolved.url_private
606+
fileName = fileName || resolved.name
607+
fileMimeType = fileMimeType || resolved.mimetype
608+
fileSize = fileSize ?? resolved.size
609+
}
564610
}
565611

566-
if (!SLACK_FILE_HOSTS.has(parsedUrl.hostname)) {
567-
logger.warn('Slack file url_private points to unexpected host, skipping', {
612+
if (!urlPrivate) {
613+
logger.warn('Slack file has no url_private and could not be resolved, skipping', {
568614
fileId: file.id,
569-
hostname: sanitizeUrlForLog(urlPrivate),
570615
})
571616
continue
572617
}
573618

574619
// Skip files that exceed the size limit
575-
const reportedSize = Number(file.size) || 0
620+
const reportedSize = Number(fileSize) || 0
576621
if (reportedSize > SLACK_MAX_FILE_SIZE) {
577622
logger.warn('Slack file exceeds size limit, skipping', {
578623
fileId: file.id,
@@ -618,9 +663,9 @@ async function downloadSlackFiles(
618663
}
619664

620665
downloaded.push({
621-
name: file.name || 'download',
666+
name: fileName || 'download',
622667
data: buffer.toString('base64'),
623-
mimeType: file.mimetype || 'application/octet-stream',
668+
mimeType: fileMimeType || 'application/octet-stream',
624669
size: buffer.length,
625670
})
626671
} catch (error) {

0 commit comments

Comments
 (0)