From 930bd3d0a545dc98be6ac83c59c080eef60d97ae Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E3=81=8A=E3=81=95=E3=82=80=E3=81=AE=E3=81=B2=E3=81=A8?= <46447427+samunohito@users.noreply.github.com> Date: Sun, 27 Apr 2025 14:53:41 +0900 Subject: [PATCH] =?UTF-8?q?=E3=83=AD=E3=83=BC=E3=83=AB=E3=83=9D=E3=83=AA?= =?UTF-8?q?=E3=82=B7=E3=83=BC=E3=81=AE=E5=80=A4=E3=82=82=E5=8F=82=E7=85=A7?= =?UTF-8?q?=E3=81=99=E3=82=8B=E3=82=88=E3=81=86=E3=81=AB?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../backend/src/server/api/ApiCallService.ts | 75 ++++++++++--------- 1 file changed, 40 insertions(+), 35 deletions(-) diff --git a/packages/backend/src/server/api/ApiCallService.ts b/packages/backend/src/server/api/ApiCallService.ts index 6c48a8bb59..b16cc236cc 100644 --- a/packages/backend/src/server/api/ApiCallService.ts +++ b/packages/backend/src/server/api/ApiCallService.ts @@ -19,7 +19,7 @@ import type Logger from '@/logger.js'; import type { MiMeta, UserIpsRepository } from '@/models/_.js'; import { createTemp } from '@/misc/create-temp.js'; import { bindThis } from '@/decorators.js'; -import { RoleService } from '@/core/RoleService.js'; +import { type RolePolicies, RoleService } from '@/core/RoleService.js'; import type { Config } from '@/config.js'; import { ApiError } from './error.js'; import { RateLimiterService } from './RateLimiterService.js'; @@ -356,6 +356,37 @@ export class ApiCallService implements OnApplicationShutdown { } } + // Cast non JSON input + if ((ep.meta.requireFile || request.method === 'GET') && ep.params.properties) { + for (const k of Object.keys(ep.params.properties)) { + const param = ep.params.properties![k]; + if (['boolean', 'number', 'integer'].includes(param.type ?? '') && typeof data[k] === 'string') { + try { + data[k] = JSON.parse(data[k]); + } catch (e) { + throw new ApiError({ + message: 'Invalid param.', + code: 'INVALID_PARAM', + id: '0b5f1631-7c1a-41a6-b399-cce335f34d85', + }, { + param: k, + reason: `cannot cast to ${param.type}`, + }); + } + } + } + } + + if (token && ((ep.meta.kind && !token.permission.some(p => p === ep.meta.kind)) + || (!ep.meta.kind && (ep.meta.requireCredential || ep.meta.requireModerator || ep.meta.requireAdmin)))) { + throw new ApiError({ + message: 'Your app does not have the necessary permissions to use this endpoint.', + code: 'PERMISSION_DENIED', + kind: 'permission', + id: '1370e5b7-d4eb-4566-bb1d-7748ee6a1838', + }); + } + if ((ep.meta.requireModerator || ep.meta.requireAdmin) && (this.meta.rootUserId !== user!.id)) { const myRoles = await this.roleService.getUserRoles(user!.id); if (ep.meta.requireModerator && !myRoles.some(r => r.isModerator || r.isAdministrator)) { @@ -389,41 +420,15 @@ export class ApiCallService implements OnApplicationShutdown { } } - if (token && ((ep.meta.kind && !token.permission.some(p => p === ep.meta.kind)) - || (!ep.meta.kind && (ep.meta.requireCredential || ep.meta.requireModerator || ep.meta.requireAdmin)))) { - throw new ApiError({ - message: 'Your app does not have the necessary permissions to use this endpoint.', - code: 'PERMISSION_DENIED', - kind: 'permission', - id: '1370e5b7-d4eb-4566-bb1d-7748ee6a1838', - }); - } - - // Cast non JSON input - if ((ep.meta.requireFile || request.method === 'GET') && ep.params.properties) { - for (const k of Object.keys(ep.params.properties)) { - const param = ep.params.properties![k]; - if (['boolean', 'number', 'integer'].includes(param.type ?? '') && typeof data[k] === 'string') { - try { - data[k] = JSON.parse(data[k]); - } catch (e) { - throw new ApiError({ - message: 'Invalid param.', - code: 'INVALID_PARAM', - id: '0b5f1631-7c1a-41a6-b399-cce335f34d85', - }, { - param: k, - reason: `cannot cast to ${param.type}`, - }); - } - } - } - } - let attachmentFile: AttachmentFile | null = null; let cleanup = () => {}; if (ep.meta.requireFile && request.method === 'POST' && multipartFile) { - const result = await this.handleAttachmentFile(request, multipartFile); + const policies = await this.roleService.getUserPolicies(user!.id); + const result = await this.handleAttachmentFile( + Math.min((policies.maxFileSizeMb * 1024 * 1024), this.config.maxFileSize), + request, + multipartFile, + ); attachmentFile = result.attachmentFile; cleanup = result.cleanup; } @@ -446,6 +451,7 @@ export class ApiCallService implements OnApplicationShutdown { @bindThis private async handleAttachmentFile( + fileSizeLimit: number, request: FastifyRequest<{ Body: Record | undefined, Querystring: Record }>, multipartFile: MultipartFile, ) { @@ -463,7 +469,7 @@ export class ApiCallService implements OnApplicationShutdown { let total = 0; return new Transform({ - transform(chunk, encoding, callback) { + transform(chunk, _, callback) { total += chunk.length; if (total > limit) { callback(createTooLongError()); @@ -474,7 +480,6 @@ export class ApiCallService implements OnApplicationShutdown { }); } - const fileSizeLimit = this.config.maxFileSize; if (request.headers['content-length'] && Number(request.headers['content-length']) > fileSizeLimit) { throw createTooLongError(); }