mirror of
https://github.com/laurent22/joplin.git
synced 2024-12-24 10:27:10 +02:00
Server: Add request rate limter on session and login end points
This commit is contained in:
parent
2c79ce25fa
commit
543413d64b
11
packages/server/package-lock.json
generated
11
packages/server/package-lock.json
generated
@ -31,6 +31,7 @@
|
||||
"pg": "^8.5.1",
|
||||
"pretty-bytes": "^5.6.0",
|
||||
"query-string": "^6.8.3",
|
||||
"rate-limiter-flexible": "^2.2.4",
|
||||
"raw-body": "^2.4.1",
|
||||
"sqlite3": "^4.1.0",
|
||||
"stripe": "^8.150.0",
|
||||
@ -8802,6 +8803,11 @@
|
||||
"node": ">= 0.6"
|
||||
}
|
||||
},
|
||||
"node_modules/rate-limiter-flexible": {
|
||||
"version": "2.2.4",
|
||||
"resolved": "https://registry.npmjs.org/rate-limiter-flexible/-/rate-limiter-flexible-2.2.4.tgz",
|
||||
"integrity": "sha512-8u4k5b1afuBcfydX0L0l3J2PNjgcuo3zua8plhvIisyDqOBldrCwfSFut/Fj00LAB1nxJYVM9jeszr2rZyDhQw=="
|
||||
},
|
||||
"node_modules/raw-body": {
|
||||
"version": "2.4.1",
|
||||
"resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.4.1.tgz",
|
||||
@ -17770,6 +17776,11 @@
|
||||
"integrity": "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==",
|
||||
"dev": true
|
||||
},
|
||||
"rate-limiter-flexible": {
|
||||
"version": "2.2.4",
|
||||
"resolved": "https://registry.npmjs.org/rate-limiter-flexible/-/rate-limiter-flexible-2.2.4.tgz",
|
||||
"integrity": "sha512-8u4k5b1afuBcfydX0L0l3J2PNjgcuo3zua8plhvIisyDqOBldrCwfSFut/Fj00LAB1nxJYVM9jeszr2rZyDhQw=="
|
||||
},
|
||||
"raw-body": {
|
||||
"version": "2.4.1",
|
||||
"resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.4.1.tgz",
|
||||
|
@ -43,6 +43,7 @@
|
||||
"pg": "^8.5.1",
|
||||
"pretty-bytes": "^5.6.0",
|
||||
"query-string": "^6.8.3",
|
||||
"rate-limiter-flexible": "^2.2.4",
|
||||
"raw-body": "^2.4.1",
|
||||
"sqlite3": "^4.1.0",
|
||||
"stripe": "^8.150.0",
|
||||
|
@ -38,6 +38,8 @@ export default async function(ctx: AppContext) {
|
||||
|
||||
const responseFormat = routeResponseFormat(ctx);
|
||||
|
||||
if (error.retryAfterMs) ctx.set('Retry-After', Math.ceil(error.retryAfterMs / 1000).toString());
|
||||
|
||||
if (error.code === 'invalidOrigin') {
|
||||
ctx.response.body = error.message;
|
||||
} else if (responseFormat === RouteResponseFormat.Html) {
|
||||
|
@ -5,12 +5,15 @@ import { ErrorForbidden } from '../../utils/errors';
|
||||
import { AppContext } from '../../utils/types';
|
||||
import { bodyFields } from '../../utils/requestUtils';
|
||||
import { User } from '../../db';
|
||||
import limiterLoginBruteForce from '../../utils/request/limiterLoginBruteForce';
|
||||
|
||||
const router = new Router(RouteType.Api);
|
||||
|
||||
router.public = true;
|
||||
|
||||
router.post('api/sessions', async (_path: SubPath, ctx: AppContext) => {
|
||||
await limiterLoginBruteForce(ctx.ip);
|
||||
|
||||
const fields: User = await bodyFields(ctx.req);
|
||||
const user = await ctx.joplin.models.user().login(fields.email, fields.password);
|
||||
if (!user) throw new ErrorForbidden('Invalid username or password');
|
||||
|
@ -6,6 +6,7 @@ import { formParse } from '../../utils/requestUtils';
|
||||
import config from '../../config';
|
||||
import defaultView from '../../utils/defaultView';
|
||||
import { View } from '../../services/MustacheService';
|
||||
import limiterLoginBruteForce from '../../utils/request/limiterLoginBruteForce';
|
||||
|
||||
function makeView(error: any = null): View {
|
||||
const view = defaultView('login', 'Login');
|
||||
@ -25,6 +26,8 @@ router.get('login', async (_path: SubPath, _ctx: AppContext) => {
|
||||
});
|
||||
|
||||
router.post('login', async (_path: SubPath, ctx: AppContext) => {
|
||||
await limiterLoginBruteForce(ctx.ip);
|
||||
|
||||
try {
|
||||
const body = await formParse(ctx.req);
|
||||
|
||||
|
@ -97,6 +97,17 @@ export class ErrorPayloadTooLarge extends ApiError {
|
||||
}
|
||||
}
|
||||
|
||||
export class ErrorTooManyRequests extends ApiError {
|
||||
public static httpCode: number = 429;
|
||||
public retryAfterMs: number = 0;
|
||||
|
||||
public constructor(message: string = null, retryAfterMs: number = 0) {
|
||||
super(message === null ? 'Too Many Requests' : message, ErrorTooManyRequests.httpCode);
|
||||
this.retryAfterMs = retryAfterMs;
|
||||
Object.setPrototypeOf(this, ErrorTooManyRequests.prototype);
|
||||
}
|
||||
}
|
||||
|
||||
export function errorToString(error: Error): string {
|
||||
const msg: string[] = [];
|
||||
msg.push(error.message ? error.message : 'Unknown error');
|
||||
|
16
packages/server/src/utils/request/limiterLoginBruteForce.ts
Normal file
16
packages/server/src/utils/request/limiterLoginBruteForce.ts
Normal file
@ -0,0 +1,16 @@
|
||||
import { RateLimiterMemory, RateLimiterRes } from 'rate-limiter-flexible';
|
||||
import { ErrorTooManyRequests } from '../errors';
|
||||
|
||||
const limiterSlowBruteByIP = new RateLimiterMemory({
|
||||
points: 3, // Up to 3 request per IP
|
||||
duration: 30, // Per 30 seconds
|
||||
});
|
||||
|
||||
export default async function(ip: string) {
|
||||
try {
|
||||
await limiterSlowBruteByIP.consume(ip);
|
||||
} catch (error) {
|
||||
const result = error as RateLimiterRes;
|
||||
throw new ErrorTooManyRequests(`Too many login attempts. Please try again in ${Math.ceil(result.msBeforeNext / 1000)} seconds.`, result.msBeforeNext);
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue
Block a user