From ee8dccea2ffb151636e520f71b7dfe2b91e06c71 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E3=81=8B=E3=81=A3=E3=81=93=E3=81=8B=E3=82=8A?= <67428053+kakkokari-gtyih@users.noreply.github.com> Date: Sat, 20 Dec 2025 19:07:05 +0900 Subject: [PATCH] fix(backend): fix #16994 by approach 6 (#17005) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * fix(backend): narrow down trustproxy default value and enhance documentation on how to configure it * Update Changelog * indent [ci skip] * Update CHANGELOG.md [ci skip] * add cloudflare specific example * Update .config/example.yml Co-authored-by: anatawa12 * fix: productionでIPレートリミットされる際にlocalhostからリクエストが来たらログを残すように * fix: wrong condition * fix: use own logger for signin api * flip configuration * fix * fix [ci skip] * fix: wrong message [ci skip] * fix: どこがおかしいか明記 [ci skip] --------- Co-authored-by: anatawa12 --- .config/example.yml | 47 ++++++++++++++++--- CHANGELOG.md | 12 ++++- packages/backend/src/config.ts | 14 +++++- packages/backend/src/server/ServerService.ts | 2 +- .../backend/src/server/api/ApiCallService.ts | 13 +++-- .../src/server/api/SigninApiService.ts | 13 +++-- .../server/api/SigninWithPasskeyApiService.ts | 8 ++-- 7 files changed, 84 insertions(+), 25 deletions(-) diff --git a/.config/example.yml b/.config/example.yml index 1c07c4bc16..c7884a3687 100644 --- a/.config/example.yml +++ b/.config/example.yml @@ -107,14 +107,39 @@ port: 3000 # Proxy trust settings # -# Changes how the server interpret the origin IP of the request. +# Specifies the IP addresses that Misskey will use as trusted +# reverse proxies (e.g., nginx, Cloudflare). This affects how +# Misskey determines the source IP for each request and is used +# for important rate limiting and security features. If the value +# is not set correctly, Misskey may use the IP address of the +# reverse proxy instead of the actual source IP, which may lead to +# unintended rate limiting or security vulnerabilities. +# By default, the loopback network and private network address +# ranges shown below are trusted. +# If you are using a single reverse proxy and it is on the same +# machine or the same private network as Misskey, it is unlikely you +# need to change this setting, and the default setting is fine. +# Also, if you are using multiple reverse proxy servers and they are +# all on the same private network as Misskey, the default setting +# is fine. +# However, if you are using a reverse proxy server that accesses +# Misskey web servers and streaming servers via public IP addresses +# (for example, Cloudflare), you must set this variable. +# When changing this setting, you can use one of the following values: # -# Any format supported by Fastify is accepted. -# Default: trust all proxies (i.e. trustProxy: true) -# See: https://fastify.dev/docs/latest/reference/server/#trustproxy -# To improve security, we recommend that you configure your settings appropriately. -# Incorrect configuration can cause issues such as difficulty signing in, -# so please configure your settings carefully. +# - true: Trust all proxies +# - false: Do not trust any proxies +# - IP address, IP address range, or array of them: Trust hops that +# match the specified criteria. +# - Integer: Trust the nth hop from the front-facing proxy server as +# the client. +# For more information on how to configure this setting, please refer +# to the Fastify documentation: +# https://fastify.dev/docs/latest/Reference/Server/#trustproxy +# +# Note that if this variable is set, it overrides the default range, +# so if you have both an external reverse proxy and a proxy on the +# local host, you must include both IPs (or IP ranges). # #trustProxy: # - '10.0.0.0/8' @@ -123,6 +148,10 @@ port: 3000 # - '127.0.0.1/32' # - '::1/128' # - 'fc00::/7' +# # Example: If you are using some external reverse proxies like CDNs, +# # you may need to add the CDN IP ranges here. +# # If you're using Cloudflare, you can find IP Ranges at: +# # https://www.cloudflare.com/ips/ # ┌──────────────────────────┐ #───┘ PostgreSQL configuration └──────────────────────────────── @@ -292,6 +321,10 @@ id: 'aidx' # Whether disable HSTS #disableHsts: true +# Enable internal IP-based rate limiting (default: true) +# To configure them in reverse proxy instead, set this to false. +#enableIpRateLimit: true + # Number of worker processes #clusterLimit: 1 diff --git a/CHANGELOG.md b/CHANGELOG.md index 3c2a0bb8a2..e25096addc 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,9 +1,13 @@ ## 2025.12.2 ### Note -v2025.12.0で行われた「configの`trustProxy`のデフォルト値を`false`に変更」について、正しく環境に応じた設定を行わないとサインインが困難になるといった状態を緩和するために、以前のデフォルト値に戻す暫定対応を行いました。 +v2025.12.0で行われた「configの`trustProxy`のデフォルト値を`false`に変更」について、正しく環境に応じた設定を行わないとサインインが困難になるといった状態を緩和するために、以下の対応を行いました。 -**セキュリティを向上させるためには適切な設定を行うことを推奨しますが、間違った設定値を入れると上述のような不具合の原因となりますので、慎重に行ってください。** +**正しく設定しないと、上記のような不具合の原因となったり、セキュリティリスクが高まったりする可能性があります。必ず現在のconfigをご確認の上、必要に応じて値を変更してください。** + +- `trustProxy`について、デフォルト(configに値が設定されていない状態)ではループバックアドレスとローカルIPアドレス空間を信頼するようにしました。 +- `trustProxy`の設定方法について、より詳細に記述しました。 +- リバースプロキシやCDNなどのより上流のレイヤでレートリミットを設定したい場合や、緊急時の一時的な緩和策として、Misskey内部でのIPアドレスペースでのレートリミットを無効化できるようにしました。 ### General - 依存関係の更新 @@ -14,6 +18,10 @@ v2025.12.0で行われた「configの`trustProxy`のデフォルト値を`false` バージョン表記のないものは v0.x 系として実行されます。v1.x 系で動作させたい場合は必ずバージョン表記を含めてください。 - Fix: デッキUIでメニュー位置を下にしているとプロファイル削除ボタンが表示されないのを修正 +### Server +- Enhance: Misskey内部でのIPアドレスペースでのレートリミットを無効化できるように + - リバースプロキシやCDNなど別のレイヤで別途レートリミットを設定する場合や、ローカルでのテスト用途等として利用することを想定しています。 + - デフォルトは `enableIpRateLimit: true`(Misskey内部でのIPアドレスペースでのレートリミットは有効)です。 ## 2025.12.1 diff --git a/packages/backend/src/config.ts b/packages/backend/src/config.ts index f9852d3578..657d7869fa 100644 --- a/packages/backend/src/config.ts +++ b/packages/backend/src/config.ts @@ -30,6 +30,7 @@ type Source = { socket?: string; trustProxy?: FastifyServerOptions['trustProxy']; chmodSocket?: string; + enableIpRateLimit?: boolean; disableHsts?: boolean; db: { host: string; @@ -120,8 +121,9 @@ export type Config = { url: string; port: number; socket: string | undefined; - trustProxy: FastifyServerOptions['trustProxy']; + trustProxy: NonNullable; chmodSocket: string | undefined; + enableIpRateLimit: boolean; disableHsts: boolean | undefined; db: { host: string; @@ -263,9 +265,17 @@ export function loadConfig(): Config { url: url.origin, port: config.port ?? parseInt(process.env.PORT ?? '', 10), socket: config.socket, - trustProxy: config.trustProxy, + trustProxy: config.trustProxy ?? [ + '10.0.0.0/8', + '172.16.0.0/12', + '192.168.0.0/16', + '127.0.0.1/32', + '::1/128', + 'fc00::/7', + ], chmodSocket: config.chmodSocket, disableHsts: config.disableHsts, + enableIpRateLimit: config.enableIpRateLimit ?? true, host, hostname, scheme, diff --git a/packages/backend/src/server/ServerService.ts b/packages/backend/src/server/ServerService.ts index 1286b4dad6..ef9ac81f95 100644 --- a/packages/backend/src/server/ServerService.ts +++ b/packages/backend/src/server/ServerService.ts @@ -75,7 +75,7 @@ export class ServerService implements OnApplicationShutdown { @bindThis public async launch(): Promise { const fastify = Fastify({ - trustProxy: this.config.trustProxy ?? true, + trustProxy: this.config.trustProxy, logger: false, }); this.#fastify = fastify; diff --git a/packages/backend/src/server/api/ApiCallService.ts b/packages/backend/src/server/api/ApiCallService.ts index 261e147040..8bae46d9fb 100644 --- a/packages/backend/src/server/api/ApiCallService.ts +++ b/packages/backend/src/server/api/ApiCallService.ts @@ -313,16 +313,15 @@ export class ApiCallService implements OnApplicationShutdown { } if (ep.meta.limit) { - let limitActor: string | null; + let limitActor: string | null = null; if (user) { limitActor = user.id; - } else { - if (request.ip === '::1' || request.ip === '127.0.0.1') { - console.warn('request ip is localhost, maybe caused by misconfiguration of trustProxy or reverse proxy'); - limitActor = null; - } else { - limitActor = getIpHash(request.ip); + } else if (this.config.enableIpRateLimit) { + if (process.env.NODE_ENV === 'production' && (request.ip === '::1' || request.ip === '127.0.0.1')) { + this.logger.warn('Recieved API request from localhost IP address for rate limiting in production environment. This is likely due to an improper trustProxy setting in the config file.'); } + + limitActor = getIpHash(request.ip); } const limit = Object.assign({}, ep.meta.limit); diff --git a/packages/backend/src/server/api/SigninApiService.ts b/packages/backend/src/server/api/SigninApiService.ts index 14726f8411..00e8828242 100644 --- a/packages/backend/src/server/api/SigninApiService.ts +++ b/packages/backend/src/server/api/SigninApiService.ts @@ -15,6 +15,7 @@ import type { UserSecurityKeysRepository, UsersRepository, } from '@/models/_.js'; +import type Logger from '@/logger.js'; import type { Config } from '@/config.js'; import { getIpHash } from '@/misc/get-ip-hash.js'; import type { MiLocalUser } from '@/models/User.js'; @@ -23,6 +24,7 @@ import { bindThis } from '@/decorators.js'; import { WebAuthnService } from '@/core/WebAuthnService.js'; import { UserAuthService } from '@/core/UserAuthService.js'; import { CaptchaService } from '@/core/CaptchaService.js'; +import { LoggerService } from '@/core/LoggerService.js'; import { FastifyReplyError } from '@/misc/fastify-reply-error.js'; import { RateLimiterService } from './RateLimiterService.js'; import { SigninService } from './SigninService.js'; @@ -31,6 +33,8 @@ import type { FastifyReply, FastifyRequest } from 'fastify'; @Injectable() export class SigninApiService { + private logger: Logger; + constructor( @Inject(DI.config) private config: Config, @@ -50,6 +54,7 @@ export class SigninApiService { @Inject(DI.signinsRepository) private signinsRepository: SigninsRepository, + private loggerService: LoggerService, private idService: IdService, private rateLimiterService: RateLimiterService, private signinService: SigninService, @@ -57,6 +62,7 @@ export class SigninApiService { private webAuthnService: WebAuthnService, private captchaService: CaptchaService, ) { + this.logger = this.loggerService.getLogger('Signin'); } @bindThis @@ -89,10 +95,11 @@ export class SigninApiService { return { error }; } - if (request.ip === '::1' || request.ip === '127.0.0.1') { - console.warn('request ip is localhost, maybe caused by misconfiguration of trustProxy or reverse proxy'); - } else { // not more than 1 attempt per second and not more than 10 attempts per hour + if (this.config.enableIpRateLimit) { + if (process.env.NODE_ENV === 'production' && (request.ip === '::1' || request.ip === '127.0.0.1')) { + this.logger.warn('Recieved signin request from localhost IP address for rate limiting in production environment. This is likely due to an improper trustProxy setting in the config file.'); + } const rateLimit = await this.rateLimiterService.limit({ key: 'signin', duration: 60 * 60 * 1000, max: 10, minInterval: 1000 }, getIpHash(request.ip)); if (rateLimit != null) { reply.code(429); diff --git a/packages/backend/src/server/api/SigninWithPasskeyApiService.ts b/packages/backend/src/server/api/SigninWithPasskeyApiService.ts index 1b89752340..920f9d0b3a 100644 --- a/packages/backend/src/server/api/SigninWithPasskeyApiService.ts +++ b/packages/backend/src/server/api/SigninWithPasskeyApiService.ts @@ -84,9 +84,11 @@ export class SigninWithPasskeyApiService { return error(status ?? 500, failure ?? { id: '4e30e80c-e338-45a0-8c8f-44455efa3b76' }); }; - if (request.ip === '::1' || request.ip === '127.0.0.1') { - console.warn('request ip is localhost, maybe caused by misconfiguration of trustProxy or reverse proxy'); - } else { + if (this.config.enableIpRateLimit) { + if (process.env.NODE_ENV === 'production' && (request.ip === '::1' || request.ip === '127.0.0.1')) { + this.logger.warn('Recieved signin with passkey request from localhost IP address for rate limiting in production environment. This is likely due to an improper trustProxy setting in the config file.'); + } + try { // Not more than 1 API call per 250ms and not more than 100 attempts per 30min // NOTE: 1 Sign-in require 2 API calls