enhance: replace signin CAPTCHA with rate limit (#8740)
* enhance: rate limit works without signed in user * fix: make limit key required for limiter As before the fallback limiter key will be set from the endpoint name. * enhance: use limiter for signin * Revert "CAPTCHA求めるのは2fa認証が無効になっているときだけにした" This reverts commit02a43a310f. * Revert "feat: make captcha required when signin to improve security" This reverts commitb21b058005. * fix undefined reference * fix: better error message * enhance: only handle prefix of IPv6
This commit is contained in:
parent
cec3dcec8a
commit
161659de5c
7 changed files with 75 additions and 57 deletions
|
|
@ -2,10 +2,11 @@ import Koa from 'koa';
|
|||
import { performance } from 'perf_hooks';
|
||||
import { limiter } from './limiter.js';
|
||||
import { CacheableLocalUser, User } from '@/models/entities/user.js';
|
||||
import endpoints, { IEndpoint } from './endpoints.js';
|
||||
import endpoints, { IEndpointMeta } from './endpoints.js';
|
||||
import { ApiError } from './error.js';
|
||||
import { apiLogger } from './logger.js';
|
||||
import { AccessToken } from '@/models/entities/access-token.js';
|
||||
import IPCIDR from 'ip-cidr';
|
||||
|
||||
const accessDenied = {
|
||||
message: 'Access denied.',
|
||||
|
|
@ -15,6 +16,7 @@ const accessDenied = {
|
|||
|
||||
export default async (endpoint: string, user: CacheableLocalUser | null | undefined, token: AccessToken | null | undefined, data: any, ctx?: Koa.Context) => {
|
||||
const isSecure = user != null && token == null;
|
||||
const isModerator = user != null && (user.isModerator || user.isAdmin);
|
||||
|
||||
const ep = endpoints.find(e => e.name === endpoint);
|
||||
|
||||
|
|
@ -31,6 +33,37 @@ export default async (endpoint: string, user: CacheableLocalUser | null | undefi
|
|||
throw new ApiError(accessDenied);
|
||||
}
|
||||
|
||||
if (ep.meta.requireCredential && ep.meta.limit && !isModerator) {
|
||||
// koa will automatically load the `X-Forwarded-For` header if `proxy: true` is configured in the app.
|
||||
let limitActor: string;
|
||||
if (user) {
|
||||
limitActor = user.id;
|
||||
} else {
|
||||
// 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 ip = IPCIDR.createAddress(ctx.ip).mask(64);
|
||||
|
||||
limitActor = 'ip-' + parseInt(ip, 2).toString(36);
|
||||
}
|
||||
|
||||
const limit = Object.assign({}, ep.meta.limit);
|
||||
|
||||
if (!limit.key) {
|
||||
limit.key = ep.name;
|
||||
}
|
||||
|
||||
// Rate limit
|
||||
await limiter(limit as IEndpointMeta['limit'] & { key: NonNullable<string> }, limitActor).catch(e => {
|
||||
throw new ApiError({
|
||||
message: 'Rate limit exceeded. Please try again later.',
|
||||
code: 'RATE_LIMIT_EXCEEDED',
|
||||
id: 'd5826d14-3982-4d2e-8011-b9e9f02499ef',
|
||||
httpStatusCode: 429,
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
if (ep.meta.requireCredential && user == null) {
|
||||
throw new ApiError({
|
||||
message: 'Credential required.',
|
||||
|
|
@ -53,7 +86,7 @@ export default async (endpoint: string, user: CacheableLocalUser | null | undefi
|
|||
throw new ApiError(accessDenied, { reason: 'You are not the admin.' });
|
||||
}
|
||||
|
||||
if (ep.meta.requireModerator && !user!.isAdmin && !user!.isModerator) {
|
||||
if (ep.meta.requireModerator && !isModerator) {
|
||||
throw new ApiError(accessDenied, { reason: 'You are not a moderator.' });
|
||||
}
|
||||
|
||||
|
|
@ -65,18 +98,6 @@ export default async (endpoint: string, user: CacheableLocalUser | null | undefi
|
|||
});
|
||||
}
|
||||
|
||||
if (ep.meta.requireCredential && ep.meta.limit && !user!.isAdmin && !user!.isModerator) {
|
||||
// Rate limit
|
||||
await limiter(ep as IEndpoint & { meta: { limit: NonNullable<IEndpoint['meta']['limit']> } }, user!).catch(e => {
|
||||
throw new ApiError({
|
||||
message: 'Rate limit exceeded. Please try again later.',
|
||||
code: 'RATE_LIMIT_EXCEEDED',
|
||||
id: 'd5826d14-3982-4d2e-8011-b9e9f02499ef',
|
||||
httpStatusCode: 429,
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
// Cast non JSON input
|
||||
if (ep.meta.requireFile && ep.params.properties) {
|
||||
for (const k of Object.keys(ep.params.properties)) {
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue