From 693aec2b4d5b4dad8cdb1562b4386c8c138d82e6 Mon Sep 17 00:00:00 2001 From: Hampus Date: Wed, 2 Sep 2026 17:23:23 +0200 Subject: [PATCH] fix(api): trust recorded upload types and bound edit sizes (#2345) --- .../message/AttachmentProcessingService.ts | 2 +- .../message/MessagePersistenceService.ts | 36 +++++++++++++++++-- 2 files changed, 34 insertions(+), 4 deletions(-) diff --git a/fluxer_api/src/api/channel/services/message/AttachmentProcessingService.ts b/fluxer_api/src/api/channel/services/message/AttachmentProcessingService.ts index 20eab45c7..8ebf460b6 100644 --- a/fluxer_api/src/api/channel/services/message/AttachmentProcessingService.ts +++ b/fluxer_api/src/api/channel/services/message/AttachmentProcessingService.ts @@ -209,7 +209,7 @@ export class AttachmentProcessingService { } const attachmentId = createAttachmentID(await this.snowflakeService.generate()); const cdnKey = makeAttachmentCdnKey(message.channelId, attachmentId, attachment.filename); - let contentType = attachment.content_type ?? getContentType(attachment.filename); + let contentType = pendingUpload.content_type ?? getContentType(attachment.filename); let size = BigInt(uploadedFile.contentLength); const clientFlags = (attachment.flags ?? 0) & (MessageAttachmentFlags.IS_SPOILER | MessageAttachmentFlags.CONTAINS_EXPLICIT_MEDIA); diff --git a/fluxer_api/src/api/channel/services/message/MessagePersistenceService.ts b/fluxer_api/src/api/channel/services/message/MessagePersistenceService.ts index c9adc5b22..28d96e5fd 100644 --- a/fluxer_api/src/api/channel/services/message/MessagePersistenceService.ts +++ b/fluxer_api/src/api/channel/services/message/MessagePersistenceService.ts @@ -2,6 +2,7 @@ import assert from 'node:assert/strict'; import {MessageFlags, Permissions, SENDABLE_MESSAGE_FLAGS} from '@fluxer/constants/src/ChannelConstants'; +import {ATTACHMENT_MAX_SIZE_NON_PREMIUM} from '@fluxer/constants/src/LimitConstants'; import {UserFlags} from '@fluxer/constants/src/UserConstants'; import {ValidationErrorCodes} from '@fluxer/constants/src/ValidationErrorCodes'; import {InputValidationError} from '@fluxer/errors/src/domains/core/InputValidationError'; @@ -16,6 +17,7 @@ import type {IVirusScanService} from '@pkgs/virus_scan/src/IVirusScanService'; import {AttachmentDecayService} from '../../../attachment/AttachmentDecayService'; import type {ChannelID, EmojiID, GuildID, MessageID, RoleID, StickerID, UserID, WebhookID} from '../../../BrandedTypes'; import {createAttachmentID, createEmojiID, createGuildID} from '../../../BrandedTypes'; +import {Config} from '../../../Config'; import {getContentMessage} from '../../../content_i18n/ContentI18n'; import type { MessageAttachment, @@ -29,6 +31,8 @@ import type {IMediaService, MediaProxyNsfwMode} from '../../../infrastructure/IM import type {ISnowflakeService} from '../../../infrastructure/ISnowflakeService'; import type {IStorageService} from '../../../infrastructure/IStorageService'; import type {LimitConfigService} from '../../../limits/LimitConfigService'; +import {resolveLimitSafe} from '../../../limits/LimitConfigUtils'; +import {createLimitMatchContext} from '../../../limits/LimitMatchContextBuilder'; import type {Channel} from '../../../models/Channel'; import type {Message} from '../../../models/Message'; import type {MessageSnapshot} from '../../../models/MessageSnapshot'; @@ -43,7 +47,7 @@ import type {AttachmentUploadTraceRepository} from '../../repositories/message/A import {AttachmentProcessingService} from './AttachmentProcessingService'; import {type DmNsfwContext, MessageContentService} from './MessageContentService'; import {MessageEmbedAttachmentResolver} from './MessageEmbedAttachmentResolver'; -import {collectMessageAttachments} from './MessageHelpers'; +import {assertAttachmentFileSizesWithinLimit, collectMessageAttachments} from './MessageHelpers'; import {MessageStickerService} from './MessageStickerService'; function mapAttachmentForEmbedResolution(att: MessageAttachment) { @@ -119,13 +123,13 @@ export class MessagePersistenceService { private userRepository: IUserRepository, private guildRepository: IGuildRepositoryAggregate, private embedService: EmbedService, - storageService: IStorageService, + private readonly storageService: IStorageService, attachmentUploadTraceRepository: AttachmentUploadTraceRepository, mediaService: IMediaService, virusScanService: IVirusScanService, snowflakeService: ISnowflakeService, private readStateService: ReadStateService, - limitConfigService: LimitConfigService, + private readonly limitConfigService: LimitConfigService, ) { this.attachmentService = new AttachmentProcessingService( storageService, @@ -492,6 +496,32 @@ export class MessagePersistenceService { uploadUserId !== undefined, 'Attachment upload actor must be resolved before processing new attachments', ); + const uploader = await this.userRepository.findUnique(uploadUserId); + const guildFeatures = guild?.features ?? null; + const maxFileSize = Math.floor( + resolveLimitSafe( + this.limitConfigService.getConfigSnapshot(), + createLimitMatchContext({user: uploader, guildFeatures}), + 'max_attachment_file_size', + ATTACHMENT_MAX_SIZE_NON_PREMIUM, + guildFeatures ? 'guild' : 'user', + ), + ); + const uploadedSizes: Array = []; + for (const [index, attachment] of newAttachments.entries()) { + const uploadedFile = await this.storageService.getObjectMetadata( + Config.s3.buckets.uploads, + attachment.upload_filename, + ); + if (!uploadedFile) { + throw InputValidationError.fromCode( + `attachments.${index}.upload_filename`, + ValidationErrorCodes.FILE_NOT_FOUND, + ); + } + uploadedSizes.push(uploadedFile.contentLength); + } + assertAttachmentFileSizesWithinLimit(uploadedSizes, maxFileSize); const attachmentResult = await this.attachmentService.computeAttachments({ message, attachments: newAttachments,