Fix IP address rate limit (#8758)

* Fix IP address rate limit

* CHANGELOG

* Tune getIpHash
This commit is contained in:
MeiMei 2022-05-31 17:44:22 +09:00 committed by GitHub
parent a98194bf1b
commit c05723ca6a
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
4 changed files with 15 additions and 10 deletions

View file

@ -26,7 +26,7 @@ You should also include the user name that made the change.
Your own theme color may be unset if it was in an invalid format. Your own theme color may be unset if it was in an invalid format.
Admins should check their instance settings if in doubt. Admins should check their instance settings if in doubt.
- Perform port diagnosis at startup only when Listen fails @mei23 - Perform port diagnosis at startup only when Listen fails @mei23
- Rate limiting is now also usable for non-authenticated users. @Johann150 - Rate limiting is now also usable for non-authenticated users. @Johann150 @mei23
Admins should make sure the reverse proxy sets the `X-Forwarded-For` header to the original address. Admins should make sure the reverse proxy sets the `X-Forwarded-For` header to the original address.
### Bugfixes ### Bugfixes

View file

@ -0,0 +1,9 @@
import IPCIDR from 'ip-cidr';
export function getIpHash(ip: string) {
// because a single person may control many IPv6 addresses,
// only a /64 subnet prefix of any IP will be taken into account.
// (this means for IPv4 the entire address is used)
const prefix = IPCIDR.createAddress(ip).mask(64);
return 'ip-' + BigInt('0b' + prefix).toString(36);
}

View file

@ -6,7 +6,7 @@ import endpoints, { IEndpointMeta } from './endpoints.js';
import { ApiError } from './error.js'; import { ApiError } from './error.js';
import { apiLogger } from './logger.js'; import { apiLogger } from './logger.js';
import { AccessToken } from '@/models/entities/access-token.js'; import { AccessToken } from '@/models/entities/access-token.js';
import IPCIDR from 'ip-cidr'; import { getIpHash } from '@/misc/get-ip-hash.js';
const accessDenied = { const accessDenied = {
message: 'Access denied.', message: 'Access denied.',
@ -33,18 +33,13 @@ export default async (endpoint: string, user: CacheableLocalUser | null | undefi
throw new ApiError(accessDenied); throw new ApiError(accessDenied);
} }
if (ep.meta.requireCredential && ep.meta.limit && !isModerator) { if (ep.meta.limit && !isModerator) {
// koa will automatically load the `X-Forwarded-For` header if `proxy: true` is configured in the app. // koa will automatically load the `X-Forwarded-For` header if `proxy: true` is configured in the app.
let limitActor: string; let limitActor: string;
if (user) { if (user) {
limitActor = user.id; limitActor = user.id;
} else { } else {
// because a single person may control many IPv6 addresses, limitActor = getIpHash(ctx!.ip);
// only a /64 subnet prefix of any IP will be taken into account.
// (this means for IPv4 the entire address is used)
const ip = IPCIDR.createAddress(ctx.ip).mask(64);
limitActor = 'ip-' + parseInt(ip, 2).toString(36);
} }
const limit = Object.assign({}, ep.meta.limit); const limit = Object.assign({}, ep.meta.limit);

View file

@ -10,6 +10,7 @@ import { verifyLogin, hash } from '../2fa.js';
import { randomBytes } from 'node:crypto'; import { randomBytes } from 'node:crypto';
import { IsNull } from 'typeorm'; import { IsNull } from 'typeorm';
import { limiter } from '../limiter.js'; import { limiter } from '../limiter.js';
import { getIpHash } from '@/misc/get-ip-hash.js';
export default async (ctx: Koa.Context) => { export default async (ctx: Koa.Context) => {
ctx.set('Access-Control-Allow-Origin', config.url); ctx.set('Access-Control-Allow-Origin', config.url);
@ -27,7 +28,7 @@ export default async (ctx: Koa.Context) => {
try { try {
// 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
await limiter({ key: 'signin', duration: 60 * 60 * 1000, max: 10, minInterval: 1000 }, ctx.ip); await limiter({ key: 'signin', duration: 60 * 60 * 1000, max: 10, minInterval: 1000 }, getIpHash(ctx.ip));
} catch (err) { } catch (err) {
ctx.status = 429; ctx.status = 429;
ctx.body = { ctx.body = {