diff options
author | Chocobozzz <me@florianbigard.com> | 2019-07-04 16:42:40 +0200 |
---|---|---|
committer | Chocobozzz <me@florianbigard.com> | 2019-07-04 16:42:40 +0200 |
commit | c1340a6ac35f924161e6ec2a1d728e20c89e55c8 (patch) | |
tree | 8f0a6b72b36be586422002039720d3a08309cbea | |
parent | fd0bfc3ac43eb0c0c2ac0b21bc2e0670f546384f (diff) | |
download | PeerTube-c1340a6ac35f924161e6ec2a1d728e20c89e55c8.tar.gz PeerTube-c1340a6ac35f924161e6ec2a1d728e20c89e55c8.tar.zst PeerTube-c1340a6ac35f924161e6ec2a1d728e20c89e55c8.zip |
Add rate limit to registration and API endpoints
-rw-r--r-- | config/default.yaml | 8 | ||||
-rw-r--r-- | config/production.yaml.example | 8 | ||||
-rw-r--r-- | config/test.yaml | 8 | ||||
-rw-r--r-- | server.ts | 4 | ||||
-rw-r--r-- | server/controllers/api/index.ts | 10 | ||||
-rw-r--r-- | server/controllers/api/users/index.ts | 18 | ||||
-rw-r--r-- | server/initializers/config.ts | 8 | ||||
-rw-r--r-- | server/initializers/constants.ts | 14 | ||||
-rw-r--r-- | server/tests/api/server/reverse-proxy.ts | 57 |
9 files changed, 112 insertions, 23 deletions
diff --git a/config/default.yaml b/config/default.yaml index a213d5b0a..be5c8993c 100644 --- a/config/default.yaml +++ b/config/default.yaml | |||
@@ -10,10 +10,18 @@ webserver: | |||
10 | port: 9000 | 10 | port: 9000 |
11 | 11 | ||
12 | rates_limit: | 12 | rates_limit: |
13 | api: | ||
14 | # 50 attempts in 10 seconds | ||
15 | window: 10 seconds | ||
16 | max: 50 | ||
13 | login: | 17 | login: |
14 | # 15 attempts in 5 min | 18 | # 15 attempts in 5 min |
15 | window: 5 minutes | 19 | window: 5 minutes |
16 | max: 15 | 20 | max: 15 |
21 | signup: | ||
22 | # 2 attempts in 5 min (only succeeded attempts are taken into account) | ||
23 | window: 5 minutes | ||
24 | max: 2 | ||
17 | ask_send_email: | 25 | ask_send_email: |
18 | # 3 attempts in 5 min | 26 | # 3 attempts in 5 min |
19 | window: 5 minutes | 27 | window: 5 minutes |
diff --git a/config/production.yaml.example b/config/production.yaml.example index cdf6136d8..f55f5c096 100644 --- a/config/production.yaml.example +++ b/config/production.yaml.example | |||
@@ -9,10 +9,18 @@ webserver: | |||
9 | port: 443 | 9 | port: 443 |
10 | 10 | ||
11 | rates_limit: | 11 | rates_limit: |
12 | api: | ||
13 | # 50 attempts in 10 seconds | ||
14 | window: 10 seconds | ||
15 | max: 50 | ||
12 | login: | 16 | login: |
13 | # 15 attempts in 5 min | 17 | # 15 attempts in 5 min |
14 | window: 5 minutes | 18 | window: 5 minutes |
15 | max: 15 | 19 | max: 15 |
20 | signup: | ||
21 | # 2 attempts in 5 min (only succeeded attempts are taken into account) | ||
22 | window: 5 minutes | ||
23 | max: 2 | ||
16 | ask_send_email: | 24 | ask_send_email: |
17 | # 3 attempts in 5 min | 25 | # 3 attempts in 5 min |
18 | window: 5 minutes | 26 | window: 5 minutes |
diff --git a/config/test.yaml b/config/test.yaml index 8d3921614..0a5df75be 100644 --- a/config/test.yaml +++ b/config/test.yaml | |||
@@ -5,6 +5,14 @@ listen: | |||
5 | webserver: | 5 | webserver: |
6 | https: false | 6 | https: false |
7 | 7 | ||
8 | rates_limit: | ||
9 | signup: | ||
10 | window: 10 minutes | ||
11 | max: 50 | ||
12 | login: | ||
13 | window: 5 minutes | ||
14 | max: 20 | ||
15 | |||
8 | database: | 16 | database: |
9 | hostname: 'localhost' | 17 | hostname: 'localhost' |
10 | port: 5432 | 18 | port: 5432 |
@@ -27,9 +27,9 @@ const app = express() | |||
27 | import { checkMissedConfig, checkFFmpeg } from './server/initializers/checker-before-init' | 27 | import { checkMissedConfig, checkFFmpeg } from './server/initializers/checker-before-init' |
28 | 28 | ||
29 | // Do not use barrels because we don't want to load all modules here (we need to initialize database first) | 29 | // Do not use barrels because we don't want to load all modules here (we need to initialize database first) |
30 | import { logger } from './server/helpers/logger' | ||
31 | import { API_VERSION, FILES_CACHE, WEBSERVER, loadLanguages } from './server/initializers/constants' | ||
32 | import { CONFIG } from './server/initializers/config' | 30 | import { CONFIG } from './server/initializers/config' |
31 | import { API_VERSION, FILES_CACHE, WEBSERVER, loadLanguages } from './server/initializers/constants' | ||
32 | import { logger } from './server/helpers/logger' | ||
33 | 33 | ||
34 | const missed = checkMissedConfig() | 34 | const missed = checkMissedConfig() |
35 | if (missed.length !== 0) { | 35 | if (missed.length !== 0) { |
diff --git a/server/controllers/api/index.ts b/server/controllers/api/index.ts index 60a84036e..ea2615e28 100644 --- a/server/controllers/api/index.ts +++ b/server/controllers/api/index.ts | |||
@@ -1,4 +1,5 @@ | |||
1 | import * as express from 'express' | 1 | import * as express from 'express' |
2 | import * as RateLimit from 'express-rate-limit' | ||
2 | import { configRouter } from './config' | 3 | import { configRouter } from './config' |
3 | import { jobsRouter } from './jobs' | 4 | import { jobsRouter } from './jobs' |
4 | import { oauthClientsRouter } from './oauth-clients' | 5 | import { oauthClientsRouter } from './oauth-clients' |
@@ -12,6 +13,7 @@ import * as cors from 'cors' | |||
12 | import { searchRouter } from './search' | 13 | import { searchRouter } from './search' |
13 | import { overviewsRouter } from './overviews' | 14 | import { overviewsRouter } from './overviews' |
14 | import { videoPlaylistRouter } from './video-playlist' | 15 | import { videoPlaylistRouter } from './video-playlist' |
16 | import { CONFIG } from '../../initializers/config' | ||
15 | 17 | ||
16 | const apiRouter = express.Router() | 18 | const apiRouter = express.Router() |
17 | 19 | ||
@@ -21,6 +23,14 @@ apiRouter.use(cors({ | |||
21 | credentials: true | 23 | credentials: true |
22 | })) | 24 | })) |
23 | 25 | ||
26 | // FIXME: https://github.com/nfriedly/express-rate-limit/issues/138 | ||
27 | // @ts-ignore | ||
28 | const apiRateLimiter = RateLimit({ | ||
29 | windowMs: CONFIG.RATES_LIMIT.API.WINDOW_MS, | ||
30 | max: CONFIG.RATES_LIMIT.API.MAX | ||
31 | }) | ||
32 | apiRouter.use(apiRateLimiter) | ||
33 | |||
24 | apiRouter.use('/server', serverRouter) | 34 | apiRouter.use('/server', serverRouter) |
25 | apiRouter.use('/oauth-clients', oauthClientsRouter) | 35 | apiRouter.use('/oauth-clients', oauthClientsRouter) |
26 | apiRouter.use('/config', configRouter) | 36 | apiRouter.use('/config', configRouter) |
diff --git a/server/controllers/api/users/index.ts b/server/controllers/api/users/index.ts index c1d72087c..63747a0a9 100644 --- a/server/controllers/api/users/index.ts +++ b/server/controllers/api/users/index.ts | |||
@@ -3,7 +3,7 @@ import * as RateLimit from 'express-rate-limit' | |||
3 | import { UserCreate, UserRight, UserRole, UserUpdate } from '../../../../shared' | 3 | import { UserCreate, UserRight, UserRole, UserUpdate } from '../../../../shared' |
4 | import { logger } from '../../../helpers/logger' | 4 | import { logger } from '../../../helpers/logger' |
5 | import { getFormattedObjects } from '../../../helpers/utils' | 5 | import { getFormattedObjects } from '../../../helpers/utils' |
6 | import { RATES_LIMIT, WEBSERVER } from '../../../initializers/constants' | 6 | import { WEBSERVER } from '../../../initializers/constants' |
7 | import { Emailer } from '../../../lib/emailer' | 7 | import { Emailer } from '../../../lib/emailer' |
8 | import { Redis } from '../../../lib/redis' | 8 | import { Redis } from '../../../lib/redis' |
9 | import { createUserAccountAndChannelAndPlaylist, sendVerifyUserEmail } from '../../../lib/user' | 9 | import { createUserAccountAndChannelAndPlaylist, sendVerifyUserEmail } from '../../../lib/user' |
@@ -53,14 +53,21 @@ const auditLogger = auditLoggerFactory('users') | |||
53 | // FIXME: https://github.com/nfriedly/express-rate-limit/issues/138 | 53 | // FIXME: https://github.com/nfriedly/express-rate-limit/issues/138 |
54 | // @ts-ignore | 54 | // @ts-ignore |
55 | const loginRateLimiter = RateLimit({ | 55 | const loginRateLimiter = RateLimit({ |
56 | windowMs: RATES_LIMIT.LOGIN.WINDOW_MS, | 56 | windowMs: CONFIG.RATES_LIMIT.LOGIN.WINDOW_MS, |
57 | max: RATES_LIMIT.LOGIN.MAX | 57 | max: CONFIG.RATES_LIMIT.LOGIN.MAX |
58 | }) | ||
59 | |||
60 | // @ts-ignore | ||
61 | const signupRateLimiter = RateLimit({ | ||
62 | windowMs: CONFIG.RATES_LIMIT.SIGNUP.WINDOW_MS, | ||
63 | max: CONFIG.RATES_LIMIT.SIGNUP.MAX, | ||
64 | skipFailedRequests: true | ||
58 | }) | 65 | }) |
59 | 66 | ||
60 | // @ts-ignore | 67 | // @ts-ignore |
61 | const askSendEmailLimiter = new RateLimit({ | 68 | const askSendEmailLimiter = new RateLimit({ |
62 | windowMs: RATES_LIMIT.ASK_SEND_EMAIL.WINDOW_MS, | 69 | windowMs: CONFIG.RATES_LIMIT.ASK_SEND_EMAIL.WINDOW_MS, |
63 | max: RATES_LIMIT.ASK_SEND_EMAIL.MAX | 70 | max: CONFIG.RATES_LIMIT.ASK_SEND_EMAIL.MAX |
64 | }) | 71 | }) |
65 | 72 | ||
66 | const usersRouter = express.Router() | 73 | const usersRouter = express.Router() |
@@ -114,6 +121,7 @@ usersRouter.post('/', | |||
114 | ) | 121 | ) |
115 | 122 | ||
116 | usersRouter.post('/register', | 123 | usersRouter.post('/register', |
124 | signupRateLimiter, | ||
117 | asyncMiddleware(ensureUserRegistrationAllowed), | 125 | asyncMiddleware(ensureUserRegistrationAllowed), |
118 | ensureUserRegistrationAllowedForIP, | 126 | ensureUserRegistrationAllowedForIP, |
119 | asyncMiddleware(usersRegisterValidator), | 127 | asyncMiddleware(usersRegisterValidator), |
diff --git a/server/initializers/config.ts b/server/initializers/config.ts index bb278ba43..eefb45fb9 100644 --- a/server/initializers/config.ts +++ b/server/initializers/config.ts | |||
@@ -72,6 +72,14 @@ const CONFIG = { | |||
72 | PORT: config.get<number>('webserver.port') | 72 | PORT: config.get<number>('webserver.port') |
73 | }, | 73 | }, |
74 | RATES_LIMIT: { | 74 | RATES_LIMIT: { |
75 | API: { | ||
76 | WINDOW_MS: parseDurationToMs(config.get<string>('rates_limit.api.window')), | ||
77 | MAX: config.get<number>('rates_limit.api.max') | ||
78 | }, | ||
79 | SIGNUP: { | ||
80 | WINDOW_MS: parseDurationToMs(config.get<string>('rates_limit.signup.window')), | ||
81 | MAX: config.get<number>('rates_limit.signup.max') | ||
82 | }, | ||
75 | LOGIN: { | 83 | LOGIN: { |
76 | WINDOW_MS: parseDurationToMs(config.get<string>('rates_limit.login.window')), | 84 | WINDOW_MS: parseDurationToMs(config.get<string>('rates_limit.login.window')), |
77 | MAX: config.get<number>('rates_limit.login.max') | 85 | MAX: config.get<number>('rates_limit.login.max') |
diff --git a/server/initializers/constants.ts b/server/initializers/constants.ts index 500f8770a..abd9c2003 100644 --- a/server/initializers/constants.ts +++ b/server/initializers/constants.ts | |||
@@ -280,17 +280,6 @@ let CONSTRAINTS_FIELDS = { | |||
280 | } | 280 | } |
281 | } | 281 | } |
282 | 282 | ||
283 | const RATES_LIMIT = { | ||
284 | LOGIN: { | ||
285 | WINDOW_MS: CONFIG.RATES_LIMIT.LOGIN.WINDOW_MS, | ||
286 | MAX: CONFIG.RATES_LIMIT.LOGIN.MAX | ||
287 | }, | ||
288 | ASK_SEND_EMAIL: { | ||
289 | WINDOW_MS: CONFIG.RATES_LIMIT.ASK_SEND_EMAIL.WINDOW_MS, | ||
290 | MAX: CONFIG.RATES_LIMIT.ASK_SEND_EMAIL.MAX | ||
291 | } | ||
292 | } | ||
293 | |||
294 | let VIDEO_VIEW_LIFETIME = 60000 * 60 // 1 hour | 283 | let VIDEO_VIEW_LIFETIME = 60000 * 60 // 1 hour |
295 | let CONTACT_FORM_LIFETIME = 60000 * 60 // 1 hour | 284 | let CONTACT_FORM_LIFETIME = 60000 * 60 // 1 hour |
296 | 285 | ||
@@ -624,8 +613,6 @@ if (isTestInstance() === true) { | |||
624 | FILES_CACHE.VIDEO_CAPTIONS.MAX_AGE = 3000 | 613 | FILES_CACHE.VIDEO_CAPTIONS.MAX_AGE = 3000 |
625 | MEMOIZE_TTL.OVERVIEWS_SAMPLE = 1 | 614 | MEMOIZE_TTL.OVERVIEWS_SAMPLE = 1 |
626 | ROUTE_CACHE_LIFETIME.OVERVIEWS.VIDEOS = '0ms' | 615 | ROUTE_CACHE_LIFETIME.OVERVIEWS.VIDEOS = '0ms' |
627 | |||
628 | RATES_LIMIT.LOGIN.MAX = 20 | ||
629 | } | 616 | } |
630 | 617 | ||
631 | updateWebserverUrls() | 618 | updateWebserverUrls() |
@@ -696,7 +683,6 @@ export { | |||
696 | SCHEDULER_INTERVALS_MS, | 683 | SCHEDULER_INTERVALS_MS, |
697 | REPEAT_JOBS, | 684 | REPEAT_JOBS, |
698 | STATIC_DOWNLOAD_PATHS, | 685 | STATIC_DOWNLOAD_PATHS, |
699 | RATES_LIMIT, | ||
700 | MIMETYPES, | 686 | MIMETYPES, |
701 | CRAWL_REQUEST_CONCURRENCY, | 687 | CRAWL_REQUEST_CONCURRENCY, |
702 | DEFAULT_AUDIO_RESOLUTION, | 688 | DEFAULT_AUDIO_RESOLUTION, |
diff --git a/server/tests/api/server/reverse-proxy.ts b/server/tests/api/server/reverse-proxy.ts index 987538237..00d9fca23 100644 --- a/server/tests/api/server/reverse-proxy.ts +++ b/server/tests/api/server/reverse-proxy.ts | |||
@@ -2,7 +2,7 @@ | |||
2 | 2 | ||
3 | import 'mocha' | 3 | import 'mocha' |
4 | import * as chai from 'chai' | 4 | import * as chai from 'chai' |
5 | import { cleanupTests, getVideo, uploadVideo, userLogin, viewVideo, wait } from '../../../../shared/extra-utils' | 5 | import { cleanupTests, getVideo, registerUser, uploadVideo, userLogin, viewVideo, wait } from '../../../../shared/extra-utils' |
6 | import { flushAndRunServer, setAccessTokensToServers } from '../../../../shared/extra-utils/index' | 6 | import { flushAndRunServer, setAccessTokensToServers } from '../../../../shared/extra-utils/index' |
7 | 7 | ||
8 | const expect = chai.expect | 8 | const expect = chai.expect |
@@ -13,7 +13,27 @@ describe('Test application behind a reverse proxy', function () { | |||
13 | 13 | ||
14 | before(async function () { | 14 | before(async function () { |
15 | this.timeout(30000) | 15 | this.timeout(30000) |
16 | server = await flushAndRunServer(1) | 16 | |
17 | const config = { | ||
18 | rates_limit: { | ||
19 | api: { | ||
20 | max: 50, | ||
21 | window: 5000 | ||
22 | }, | ||
23 | signup: { | ||
24 | max: 3, | ||
25 | window: 5000 | ||
26 | }, | ||
27 | login: { | ||
28 | max: 20 | ||
29 | } | ||
30 | }, | ||
31 | signup: { | ||
32 | limit: 20 | ||
33 | } | ||
34 | } | ||
35 | |||
36 | server = await flushAndRunServer(1, config) | ||
17 | await setAccessTokensToServers([ server ]) | 37 | await setAccessTokensToServers([ server ]) |
18 | 38 | ||
19 | const { body } = await uploadVideo(server.url, server.accessToken, {}) | 39 | const { body } = await uploadVideo(server.url, server.accessToken, {}) |
@@ -82,6 +102,39 @@ describe('Test application behind a reverse proxy', function () { | |||
82 | await userLogin(server, user, 429) | 102 | await userLogin(server, user, 429) |
83 | }) | 103 | }) |
84 | 104 | ||
105 | it('Should rate limit signup', async function () { | ||
106 | for (let i = 0; i < 3; i++) { | ||
107 | await registerUser(server.url, 'test' + i, 'password') | ||
108 | } | ||
109 | |||
110 | await registerUser(server.url, 'test42', 'password', 429) | ||
111 | }) | ||
112 | |||
113 | it('Should not rate limit failed signup', async function () { | ||
114 | this.timeout(30000) | ||
115 | |||
116 | await wait(7000) | ||
117 | |||
118 | for (let i = 0; i < 3; i++) { | ||
119 | await registerUser(server.url, 'test' + i, 'password', 409) | ||
120 | } | ||
121 | |||
122 | await registerUser(server.url, 'test43', 'password', 204) | ||
123 | |||
124 | }) | ||
125 | |||
126 | it('Should rate limit API calls', async function () { | ||
127 | this.timeout(30000) | ||
128 | |||
129 | await wait(7000) | ||
130 | |||
131 | for (let i = 0; i < 50; i++) { | ||
132 | await getVideo(server.url, videoId) | ||
133 | } | ||
134 | |||
135 | await getVideo(server.url, videoId, 429) | ||
136 | }) | ||
137 | |||
85 | after(async function () { | 138 | after(async function () { |
86 | await cleanupTests([ server ]) | 139 | await cleanupTests([ server ]) |
87 | }) | 140 | }) |