Fix IP address rate limit (#8758)
* Fix IP address rate limit * CHANGELOG * Tune getIpHash
This commit is contained in:
parent
a98194bf1b
commit
c05723ca6a
4 changed files with 15 additions and 10 deletions
|
@ -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
|
||||||
|
|
9
packages/backend/src/misc/get-ip-hash.ts
Normal file
9
packages/backend/src/misc/get-ip-hash.ts
Normal 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);
|
||||||
|
}
|
|
@ -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);
|
||||||
|
|
|
@ -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 = {
|
||||||
|
|
Loading…
Reference in a new issue