mirror of
https://github.com/fluxerapp/fluxer
synced 2026-10-17 21:15:32 +09:00
fix(api): harden ratelimit, SSRF, uploads and DM guards (#2074)
This commit is contained in:
1 parent
bbd93df239
commit
f2ea10f951
9 files changed
+121
-41
No files matched your search
@@ -233,11 +233,14 @@ export class AttachmentUploadService {
|
||||
const parts = await Promise.all(
|
||||
Array.from({length: partCount}, async (_, index) => {
|
||||
const partNumber = index + 1;
|
||||
const partContentLength =
|
||||
partNumber < partCount ? partSize : attachment.file_size - partSize * (partCount - 1);
|
||||
const presigned_upload_url = await this.storageService.getPresignedUploadPartURL({
|
||||
bucket,
|
||||
key: uploadKey,
|
||||
uploadId,
|
||||
partNumber,
|
||||
contentLength: partContentLength,
|
||||
});
|
||||
const upload_url = applyUploadRelayDecision({
|
||||
presignedUrl: presigned_upload_url,
|
||||
@@ -246,7 +249,7 @@ export class AttachmentUploadService {
|
||||
relayDecision: uploadRelayDecision,
|
||||
uploadId,
|
||||
partNumber,
|
||||
maxBytes: partSize,
|
||||
maxBytes: partContentLength,
|
||||
});
|
||||
return {part_number: partNumber, upload_url};
|
||||
}),
|
||||
@@ -275,7 +278,7 @@ export class AttachmentUploadService {
|
||||
if (!Config.presignedAttachmentUploadsEnabled) {
|
||||
throw new FeatureTemporarilyDisabledError();
|
||||
}
|
||||
await this.getUploadPermissionAndLimit({userId, channelId});
|
||||
const {maxFileSize} = await this.getUploadPermissionAndLimit({userId, channelId});
|
||||
const bucket = Config.s3.buckets.uploads;
|
||||
return Promise.all(
|
||||
uploads.map(async ({upload_filename, upload_id}, index) => {
|
||||
@@ -305,6 +308,13 @@ export class AttachmentUploadService {
|
||||
.catch(() => undefined);
|
||||
throw InputValidationError.fromCode('parts', ValidationErrorCodes.NO_UPLOADED_PARTS_TO_FINALIZE);
|
||||
}
|
||||
const totalUploadedBytes = parts.reduce((sum, part) => sum + (part.size ?? 0), 0);
|
||||
if (totalUploadedBytes > maxFileSize) {
|
||||
await this.storageService
|
||||
.abortMultipartUpload({bucket, key: upload_filename, uploadId: upload_id})
|
||||
.catch(() => undefined);
|
||||
throw new FileSizeTooLargeError(maxFileSize);
|
||||
}
|
||||
try {
|
||||
await runAttachmentStorageOperation(() =>
|
||||
this.storageService.completeMultipartUpload({
|
||||
|
||||
@@ -40,9 +40,8 @@ export class DMPermissionValidator {
|
||||
if (isBugHunterBotUser(senderUser)) {
|
||||
return;
|
||||
}
|
||||
if (!senderUser.isBot && (senderUser.flags & UserFlags.SPAMMER) === UserFlags.SPAMMER) {
|
||||
return;
|
||||
}
|
||||
const isShadowbannedSpammer =
|
||||
!senderUser.isBot && (senderUser.flags & UserFlags.SPAMMER) === UserFlags.SPAMMER;
|
||||
const [senderBlockedTarget, targetBlockedSender, areFriends, targetSettings, senderSettings] = await Promise.all([
|
||||
this.deps.userRepository.getRelationship(senderId, recipientId, RelationshipTypes.BLOCKED),
|
||||
this.deps.userRepository.getRelationship(recipientId, senderId, RelationshipTypes.BLOCKED),
|
||||
@@ -53,6 +52,9 @@ export class DMPermissionValidator {
|
||||
if (senderBlockedTarget || targetBlockedSender) {
|
||||
throw new CannotSendMessagesToUserError();
|
||||
}
|
||||
if (isShadowbannedSpammer) {
|
||||
return;
|
||||
}
|
||||
if (areFriends) {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -51,6 +51,7 @@ function isUnsatisfiableRangeError(error: unknown): boolean {
|
||||
}
|
||||
const DESKTOP_BUCKET_PREFIX = 'desktop';
|
||||
const DESKTOP_TEST_BUCKET_PREFIX = 'desktop-test';
|
||||
const DOWNLOAD_KEY_ALLOWED_PREFIXES = [`${DESKTOP_BUCKET_PREFIX}/`, `${DESKTOP_TEST_BUCKET_PREFIX}/`];
|
||||
const DEFAULT_API_CLIENT_BASE_URL = 'https://api.fluxer.app';
|
||||
const GITHUB_RELEASE_DOWNLOAD_BASE_URL = 'https://github.com/fluxerapp/fluxer/releases/download';
|
||||
const GITHUB_RELEASE_MARKER_DIRECTORY = 'github-releases';
|
||||
@@ -987,7 +988,13 @@ export class DownloadService {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
return normalized.length > 0 ? normalized : null;
|
||||
if (normalized.length === 0) {
|
||||
return null;
|
||||
}
|
||||
if (!DOWNLOAD_KEY_ALLOWED_PREFIXES.some((prefix) => normalized.startsWith(prefix))) {
|
||||
return null;
|
||||
}
|
||||
return normalized;
|
||||
}
|
||||
|
||||
private normalizePlatformArchKey(key: string): string | null {
|
||||
|
||||
@@ -91,6 +91,7 @@ export interface IStorageService {
|
||||
key: string;
|
||||
uploadId: string;
|
||||
partNumber: number;
|
||||
contentLength?: number;
|
||||
expiresIn?: number;
|
||||
}): Promise<string>;
|
||||
purgeBucket(bucket: string): Promise<void>;
|
||||
|
||||
@@ -296,12 +296,14 @@ export class StorageService implements IStorageService {
|
||||
key,
|
||||
uploadId,
|
||||
partNumber,
|
||||
contentLength,
|
||||
expiresIn = seconds('1 hour'),
|
||||
}: {
|
||||
bucket: string;
|
||||
key: string;
|
||||
uploadId: string;
|
||||
partNumber: number;
|
||||
contentLength?: number;
|
||||
expiresIn?: number;
|
||||
}): Promise<string> {
|
||||
const command = new UploadPartCommand({
|
||||
@@ -309,6 +311,7 @@ export class StorageService implements IStorageService {
|
||||
Key: key,
|
||||
UploadId: uploadId,
|
||||
PartNumber: partNumber,
|
||||
ContentLength: contentLength,
|
||||
});
|
||||
return getSignedUrl(this.presignClient, command, {expiresIn});
|
||||
}
|
||||
|
||||
@@ -52,7 +52,11 @@ function shouldShowHeadersOnSuccess(accountType: AccountType): boolean {
|
||||
function getClientIdentifier(ctx: Context<HonoEnv>): string {
|
||||
const user = ctx.get('user');
|
||||
if (user?.id) {
|
||||
return `user:${user.id}`;
|
||||
const tokenType = ctx.get('authTokenType') ?? 'session';
|
||||
if (tokenType === 'bearer') {
|
||||
return `user:${user.id}:bearer:${ctx.get('oauthBearerApplicationId') ?? 'unknown'}`;
|
||||
}
|
||||
return `user:${user.id}:${tokenType}`;
|
||||
}
|
||||
const ip = extractClientIp(ctx.req.raw, {
|
||||
trustClientIpHeader: Config.proxy.trust_client_ip_header,
|
||||
|
||||
@@ -89,14 +89,17 @@ export class UserAccountLookupService {
|
||||
let guildMember: GuildMemberResponse | null = null;
|
||||
let guildMemberDomain: GuildMember | null = null;
|
||||
if (guildId != null) {
|
||||
guildMemberDomain = await this.deps.guildRepository.getMember(guildId, targetId);
|
||||
if (guildMemberDomain) {
|
||||
guildMember = await this.deps.guildService.members.getMember({
|
||||
userId,
|
||||
targetId,
|
||||
guildId,
|
||||
requestCache,
|
||||
});
|
||||
const viewerMember = await this.deps.guildRepository.getMember(guildId, userId);
|
||||
if (viewerMember) {
|
||||
guildMemberDomain = await this.deps.guildRepository.getMember(guildId, targetId);
|
||||
if (guildMemberDomain) {
|
||||
guildMember = await this.deps.guildService.members.getMember({
|
||||
userId,
|
||||
targetId,
|
||||
guildId,
|
||||
requestCache,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
let premiumType = user.premiumType ?? undefined;
|
||||
|
||||
@@ -118,15 +118,17 @@ export class UserRelationshipService {
|
||||
userCacheService: UserCacheService;
|
||||
requestCache: RequestCache;
|
||||
}): Promise<Relationship> {
|
||||
if (!staffForceAccept && (await getInstanceConfigRepository().getInstancePolicyConfig()).direct_messages_disabled) {
|
||||
const requesterUser = await this.userRepository.findUnique(userId);
|
||||
const requesterIsStaff =
|
||||
requesterUser != null && (requesterUser.flags & UserFlags.STAFF) === UserFlags.STAFF;
|
||||
if (
|
||||
!requesterIsStaff &&
|
||||
(await getInstanceConfigRepository().getInstancePolicyConfig()).direct_messages_disabled
|
||||
) {
|
||||
throw new DirectMessagesDisabledError();
|
||||
}
|
||||
const requesterUser = await this.userRepository.findUnique(userId);
|
||||
if (staffForceAccept) {
|
||||
const requesterIsStaff = requesterUser != null && (requesterUser.flags & UserFlags.STAFF) === UserFlags.STAFF;
|
||||
if (requesterIsStaff) {
|
||||
return await this.forceCreateFriendship({userId, targetId, userCacheService, requestCache});
|
||||
}
|
||||
if (staffForceAccept && requesterIsStaff) {
|
||||
return await this.forceCreateFriendship({userId, targetId, userCacheService, requestCache});
|
||||
}
|
||||
if (!requesterUser) {
|
||||
throw new UnknownUserError();
|
||||
|
||||
Reference in new issue
Block a user