fix(backend): fix #16994 by approach 6 (#17005)

* 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 <anatawa12@icloud.com>

* 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 <anatawa12@icloud.com>
This commit is contained in:
かっこかり 2025-12-20 19:07:05 +09:00 committed by GitHub
parent 6d00645bc7
commit ee8dccea2f
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
7 changed files with 84 additions and 25 deletions

View File

@ -107,14 +107,39 @@ port: 3000
# Proxy trust settings # 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. # - true: Trust all proxies
# Default: trust all proxies (i.e. trustProxy: true) # - false: Do not trust any proxies
# See: https://fastify.dev/docs/latest/reference/server/#trustproxy # - IP address, IP address range, or array of them: Trust hops that
# To improve security, we recommend that you configure your settings appropriately. # match the specified criteria.
# Incorrect configuration can cause issues such as difficulty signing in, # - Integer: Trust the nth hop from the front-facing proxy server as
# so please configure your settings carefully. # 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: #trustProxy:
# - '10.0.0.0/8' # - '10.0.0.0/8'
@ -123,6 +148,10 @@ port: 3000
# - '127.0.0.1/32' # - '127.0.0.1/32'
# - '::1/128' # - '::1/128'
# - 'fc00::/7' # - '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 └──────────────────────────────── #───┘ PostgreSQL configuration └────────────────────────────────
@ -292,6 +321,10 @@ id: 'aidx'
# Whether disable HSTS # Whether disable HSTS
#disableHsts: true #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 # Number of worker processes
#clusterLimit: 1 #clusterLimit: 1

View File

@ -1,9 +1,13 @@
## 2025.12.2 ## 2025.12.2
### Note ### Note
v2025.12.0で行われた「configの`trustProxy`のデフォルト値を`false`に変更」について、正しく環境に応じた設定を行わないとサインインが困難になるといった状態を緩和するために、以前のデフォルト値に戻す暫定対応を行いました。 v2025.12.0で行われた「configの`trustProxy`のデフォルト値を`false`に変更」について、正しく環境に応じた設定を行わないとサインインが困難になるといった状態を緩和するために、以下の対応を行いました。
**セキュリティを向上させるためには適切な設定を行うことを推奨しますが、間違った設定値を入れると上述のような不具合の原因となりますので、慎重に行ってください。** **正しく設定しないと、上記のような不具合の原因となったり、セキュリティリスクが高まったりする可能性があります。必ず現在のconfigをご確認の上、必要に応じて値を変更してください。**
- `trustProxy`について、デフォルトconfigに値が設定されていない状態ではループバックアドレスとローカルIPアドレス空間を信頼するようにしました。
- `trustProxy`の設定方法について、より詳細に記述しました。
- リバースプロキシやCDNなどのより上流のレイヤでレートリミットを設定したい場合や、緊急時の一時的な緩和策として、Misskey内部でのIPアドレスペースでのレートリミットを無効化できるようにしました。
### General ### General
- 依存関係の更新 - 依存関係の更新
@ -14,6 +18,10 @@ v2025.12.0で行われた「configの`trustProxy`のデフォルト値を`false`
バージョン表記のないものは v0.x 系として実行されます。v1.x 系で動作させたい場合は必ずバージョン表記を含めてください。 バージョン表記のないものは v0.x 系として実行されます。v1.x 系で動作させたい場合は必ずバージョン表記を含めてください。
- Fix: デッキUIでメニュー位置を下にしているとプロファイル削除ボタンが表示されないのを修正 - Fix: デッキUIでメニュー位置を下にしているとプロファイル削除ボタンが表示されないのを修正
### Server
- Enhance: Misskey内部でのIPアドレスペースでのレートリミットを無効化できるように
- リバースプロキシやCDNなど別のレイヤで別途レートリミットを設定する場合や、ローカルでのテスト用途等として利用することを想定しています。
- デフォルトは `enableIpRateLimit: true`Misskey内部でのIPアドレスペースでのレートリミットは有効です。
## 2025.12.1 ## 2025.12.1

View File

@ -30,6 +30,7 @@ type Source = {
socket?: string; socket?: string;
trustProxy?: FastifyServerOptions['trustProxy']; trustProxy?: FastifyServerOptions['trustProxy'];
chmodSocket?: string; chmodSocket?: string;
enableIpRateLimit?: boolean;
disableHsts?: boolean; disableHsts?: boolean;
db: { db: {
host: string; host: string;
@ -120,8 +121,9 @@ export type Config = {
url: string; url: string;
port: number; port: number;
socket: string | undefined; socket: string | undefined;
trustProxy: FastifyServerOptions['trustProxy']; trustProxy: NonNullable<FastifyServerOptions['trustProxy']>;
chmodSocket: string | undefined; chmodSocket: string | undefined;
enableIpRateLimit: boolean;
disableHsts: boolean | undefined; disableHsts: boolean | undefined;
db: { db: {
host: string; host: string;
@ -263,9 +265,17 @@ export function loadConfig(): Config {
url: url.origin, url: url.origin,
port: config.port ?? parseInt(process.env.PORT ?? '', 10), port: config.port ?? parseInt(process.env.PORT ?? '', 10),
socket: config.socket, 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, chmodSocket: config.chmodSocket,
disableHsts: config.disableHsts, disableHsts: config.disableHsts,
enableIpRateLimit: config.enableIpRateLimit ?? true,
host, host,
hostname, hostname,
scheme, scheme,

View File

@ -75,7 +75,7 @@ export class ServerService implements OnApplicationShutdown {
@bindThis @bindThis
public async launch(): Promise<void> { public async launch(): Promise<void> {
const fastify = Fastify({ const fastify = Fastify({
trustProxy: this.config.trustProxy ?? true, trustProxy: this.config.trustProxy,
logger: false, logger: false,
}); });
this.#fastify = fastify; this.#fastify = fastify;

View File

@ -313,16 +313,15 @@ export class ApiCallService implements OnApplicationShutdown {
} }
if (ep.meta.limit) { if (ep.meta.limit) {
let limitActor: string | null; let limitActor: string | null = null;
if (user) { if (user) {
limitActor = user.id; limitActor = user.id;
} else { } else if (this.config.enableIpRateLimit) {
if (request.ip === '::1' || request.ip === '127.0.0.1') { if (process.env.NODE_ENV === 'production' && (request.ip === '::1' || request.ip === '127.0.0.1')) {
console.warn('request ip is localhost, maybe caused by misconfiguration of trustProxy or reverse proxy'); 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 = null;
} else {
limitActor = getIpHash(request.ip);
} }
limitActor = getIpHash(request.ip);
} }
const limit = Object.assign({}, ep.meta.limit); const limit = Object.assign({}, ep.meta.limit);

View File

@ -15,6 +15,7 @@ import type {
UserSecurityKeysRepository, UserSecurityKeysRepository,
UsersRepository, UsersRepository,
} from '@/models/_.js'; } from '@/models/_.js';
import type Logger from '@/logger.js';
import type { Config } from '@/config.js'; import type { Config } from '@/config.js';
import { getIpHash } from '@/misc/get-ip-hash.js'; import { getIpHash } from '@/misc/get-ip-hash.js';
import type { MiLocalUser } from '@/models/User.js'; import type { MiLocalUser } from '@/models/User.js';
@ -23,6 +24,7 @@ import { bindThis } from '@/decorators.js';
import { WebAuthnService } from '@/core/WebAuthnService.js'; import { WebAuthnService } from '@/core/WebAuthnService.js';
import { UserAuthService } from '@/core/UserAuthService.js'; import { UserAuthService } from '@/core/UserAuthService.js';
import { CaptchaService } from '@/core/CaptchaService.js'; import { CaptchaService } from '@/core/CaptchaService.js';
import { LoggerService } from '@/core/LoggerService.js';
import { FastifyReplyError } from '@/misc/fastify-reply-error.js'; import { FastifyReplyError } from '@/misc/fastify-reply-error.js';
import { RateLimiterService } from './RateLimiterService.js'; import { RateLimiterService } from './RateLimiterService.js';
import { SigninService } from './SigninService.js'; import { SigninService } from './SigninService.js';
@ -31,6 +33,8 @@ import type { FastifyReply, FastifyRequest } from 'fastify';
@Injectable() @Injectable()
export class SigninApiService { export class SigninApiService {
private logger: Logger;
constructor( constructor(
@Inject(DI.config) @Inject(DI.config)
private config: Config, private config: Config,
@ -50,6 +54,7 @@ export class SigninApiService {
@Inject(DI.signinsRepository) @Inject(DI.signinsRepository)
private signinsRepository: SigninsRepository, private signinsRepository: SigninsRepository,
private loggerService: LoggerService,
private idService: IdService, private idService: IdService,
private rateLimiterService: RateLimiterService, private rateLimiterService: RateLimiterService,
private signinService: SigninService, private signinService: SigninService,
@ -57,6 +62,7 @@ export class SigninApiService {
private webAuthnService: WebAuthnService, private webAuthnService: WebAuthnService,
private captchaService: CaptchaService, private captchaService: CaptchaService,
) { ) {
this.logger = this.loggerService.getLogger('Signin');
} }
@bindThis @bindThis
@ -89,10 +95,11 @@ export class SigninApiService {
return { error }; 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 // 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)); const rateLimit = await this.rateLimiterService.limit({ key: 'signin', duration: 60 * 60 * 1000, max: 10, minInterval: 1000 }, getIpHash(request.ip));
if (rateLimit != null) { if (rateLimit != null) {
reply.code(429); reply.code(429);

View File

@ -84,9 +84,11 @@ export class SigninWithPasskeyApiService {
return error(status ?? 500, failure ?? { id: '4e30e80c-e338-45a0-8c8f-44455efa3b76' }); return error(status ?? 500, failure ?? { id: '4e30e80c-e338-45a0-8c8f-44455efa3b76' });
}; };
if (request.ip === '::1' || request.ip === '127.0.0.1') { if (this.config.enableIpRateLimit) {
console.warn('request ip is localhost, maybe caused by misconfiguration of trustProxy or reverse proxy'); if (process.env.NODE_ENV === 'production' && (request.ip === '::1' || request.ip === '127.0.0.1')) {
} else { 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 { try {
// Not more than 1 API call per 250ms and not more than 100 attempts per 30min // Not more than 1 API call per 250ms and not more than 100 attempts per 30min
// NOTE: 1 Sign-in require 2 API calls // NOTE: 1 Sign-in require 2 API calls