mirror of
https://github.com/laurent22/joplin.git
synced 2025-01-11 18:24:43 +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",
|
"pg": "^8.5.1",
|
||||||
"pretty-bytes": "^5.6.0",
|
"pretty-bytes": "^5.6.0",
|
||||||
"query-string": "^6.8.3",
|
"query-string": "^6.8.3",
|
||||||
|
"rate-limiter-flexible": "^2.2.4",
|
||||||
"raw-body": "^2.4.1",
|
"raw-body": "^2.4.1",
|
||||||
"sqlite3": "^4.1.0",
|
"sqlite3": "^4.1.0",
|
||||||
"stripe": "^8.150.0",
|
"stripe": "^8.150.0",
|
||||||
@ -8802,6 +8803,11 @@
|
|||||||
"node": ">= 0.6"
|
"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": {
|
"node_modules/raw-body": {
|
||||||
"version": "2.4.1",
|
"version": "2.4.1",
|
||||||
"resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.4.1.tgz",
|
"resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.4.1.tgz",
|
||||||
@ -17770,6 +17776,11 @@
|
|||||||
"integrity": "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==",
|
"integrity": "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==",
|
||||||
"dev": true
|
"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": {
|
"raw-body": {
|
||||||
"version": "2.4.1",
|
"version": "2.4.1",
|
||||||
"resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.4.1.tgz",
|
"resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.4.1.tgz",
|
||||||
|
@ -43,6 +43,7 @@
|
|||||||
"pg": "^8.5.1",
|
"pg": "^8.5.1",
|
||||||
"pretty-bytes": "^5.6.0",
|
"pretty-bytes": "^5.6.0",
|
||||||
"query-string": "^6.8.3",
|
"query-string": "^6.8.3",
|
||||||
|
"rate-limiter-flexible": "^2.2.4",
|
||||||
"raw-body": "^2.4.1",
|
"raw-body": "^2.4.1",
|
||||||
"sqlite3": "^4.1.0",
|
"sqlite3": "^4.1.0",
|
||||||
"stripe": "^8.150.0",
|
"stripe": "^8.150.0",
|
||||||
|
@ -38,6 +38,8 @@ export default async function(ctx: AppContext) {
|
|||||||
|
|
||||||
const responseFormat = routeResponseFormat(ctx);
|
const responseFormat = routeResponseFormat(ctx);
|
||||||
|
|
||||||
|
if (error.retryAfterMs) ctx.set('Retry-After', Math.ceil(error.retryAfterMs / 1000).toString());
|
||||||
|
|
||||||
if (error.code === 'invalidOrigin') {
|
if (error.code === 'invalidOrigin') {
|
||||||
ctx.response.body = error.message;
|
ctx.response.body = error.message;
|
||||||
} else if (responseFormat === RouteResponseFormat.Html) {
|
} else if (responseFormat === RouteResponseFormat.Html) {
|
||||||
|
@ -5,12 +5,15 @@ import { ErrorForbidden } from '../../utils/errors';
|
|||||||
import { AppContext } from '../../utils/types';
|
import { AppContext } from '../../utils/types';
|
||||||
import { bodyFields } from '../../utils/requestUtils';
|
import { bodyFields } from '../../utils/requestUtils';
|
||||||
import { User } from '../../db';
|
import { User } from '../../db';
|
||||||
|
import limiterLoginBruteForce from '../../utils/request/limiterLoginBruteForce';
|
||||||
|
|
||||||
const router = new Router(RouteType.Api);
|
const router = new Router(RouteType.Api);
|
||||||
|
|
||||||
router.public = true;
|
router.public = true;
|
||||||
|
|
||||||
router.post('api/sessions', async (_path: SubPath, ctx: AppContext) => {
|
router.post('api/sessions', async (_path: SubPath, ctx: AppContext) => {
|
||||||
|
await limiterLoginBruteForce(ctx.ip);
|
||||||
|
|
||||||
const fields: User = await bodyFields(ctx.req);
|
const fields: User = await bodyFields(ctx.req);
|
||||||
const user = await ctx.joplin.models.user().login(fields.email, fields.password);
|
const user = await ctx.joplin.models.user().login(fields.email, fields.password);
|
||||||
if (!user) throw new ErrorForbidden('Invalid username or password');
|
if (!user) throw new ErrorForbidden('Invalid username or password');
|
||||||
|
@ -6,6 +6,7 @@ import { formParse } from '../../utils/requestUtils';
|
|||||||
import config from '../../config';
|
import config from '../../config';
|
||||||
import defaultView from '../../utils/defaultView';
|
import defaultView from '../../utils/defaultView';
|
||||||
import { View } from '../../services/MustacheService';
|
import { View } from '../../services/MustacheService';
|
||||||
|
import limiterLoginBruteForce from '../../utils/request/limiterLoginBruteForce';
|
||||||
|
|
||||||
function makeView(error: any = null): View {
|
function makeView(error: any = null): View {
|
||||||
const view = defaultView('login', 'Login');
|
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) => {
|
router.post('login', async (_path: SubPath, ctx: AppContext) => {
|
||||||
|
await limiterLoginBruteForce(ctx.ip);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const body = await formParse(ctx.req);
|
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 {
|
export function errorToString(error: Error): string {
|
||||||
const msg: string[] = [];
|
const msg: string[] = [];
|
||||||
msg.push(error.message ? error.message : 'Unknown error');
|
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