diff options
author | Chocobozzz <me@florianbigard.com> | 2018-03-29 10:58:24 +0200 |
---|---|---|
committer | Chocobozzz <me@florianbigard.com> | 2018-03-29 11:03:30 +0200 |
commit | 490b595a01c5824ff63ffb87f0efdfca95f4bf3b (patch) | |
tree | 3ad716fbb97a8b4ee946ad907202b82934a33d7c /server | |
parent | 23f4c3d412974fa5fda52589d1192e098e260f1a (diff) | |
download | PeerTube-490b595a01c5824ff63ffb87f0efdfca95f4bf3b.tar.gz PeerTube-490b595a01c5824ff63ffb87f0efdfca95f4bf3b.tar.zst PeerTube-490b595a01c5824ff63ffb87f0efdfca95f4bf3b.zip |
Prevent brute force login attack
Diffstat (limited to 'server')
-rw-r--r-- | server/controllers/api/users.ts | 14 | ||||
-rw-r--r-- | server/controllers/api/videos/index.ts | 2 | ||||
-rw-r--r-- | server/initializers/checker.ts | 1 | ||||
-rw-r--r-- | server/initializers/constants.ts | 9 | ||||
-rw-r--r-- | server/initializers/installer.ts | 2 | ||||
-rw-r--r-- | server/tests/api/server/reverse-proxy.ts | 82 | ||||
-rw-r--r-- | server/tests/utils/videos/videos.ts | 11 |
7 files changed, 114 insertions, 7 deletions
diff --git a/server/controllers/api/users.ts b/server/controllers/api/users.ts index 583376c38..5e96d789e 100644 --- a/server/controllers/api/users.ts +++ b/server/controllers/api/users.ts | |||
@@ -2,12 +2,13 @@ import * as express from 'express' | |||
2 | import 'multer' | 2 | import 'multer' |
3 | import { extname, join } from 'path' | 3 | import { extname, join } from 'path' |
4 | import * as uuidv4 from 'uuid/v4' | 4 | import * as uuidv4 from 'uuid/v4' |
5 | import * as RateLimit from 'express-rate-limit' | ||
5 | import { UserCreate, UserRight, UserRole, UserUpdate, UserUpdateMe, UserVideoRate as FormattedUserVideoRate } from '../../../shared' | 6 | import { UserCreate, UserRight, UserRole, UserUpdate, UserUpdateMe, UserVideoRate as FormattedUserVideoRate } from '../../../shared' |
6 | import { retryTransactionWrapper } from '../../helpers/database-utils' | 7 | import { retryTransactionWrapper } from '../../helpers/database-utils' |
7 | import { processImage } from '../../helpers/image-utils' | 8 | import { processImage } from '../../helpers/image-utils' |
8 | import { logger } from '../../helpers/logger' | 9 | import { logger } from '../../helpers/logger' |
9 | import { createReqFiles, getFormattedObjects } from '../../helpers/utils' | 10 | import { createReqFiles, getFormattedObjects } from '../../helpers/utils' |
10 | import { AVATARS_SIZE, CONFIG, IMAGE_MIMETYPE_EXT, sequelizeTypescript } from '../../initializers' | 11 | import { AVATARS_SIZE, CONFIG, IMAGE_MIMETYPE_EXT, RATES_LIMIT, sequelizeTypescript } from '../../initializers' |
11 | import { updateActorAvatarInstance } from '../../lib/activitypub' | 12 | import { updateActorAvatarInstance } from '../../lib/activitypub' |
12 | import { sendUpdateActor } from '../../lib/activitypub/send' | 13 | import { sendUpdateActor } from '../../lib/activitypub/send' |
13 | import { Emailer } from '../../lib/emailer' | 14 | import { Emailer } from '../../lib/emailer' |
@@ -43,6 +44,11 @@ import { OAuthTokenModel } from '../../models/oauth/oauth-token' | |||
43 | import { VideoModel } from '../../models/video/video' | 44 | import { VideoModel } from '../../models/video/video' |
44 | 45 | ||
45 | const reqAvatarFile = createReqFiles([ 'avatarfile' ], IMAGE_MIMETYPE_EXT, { avatarfile: CONFIG.STORAGE.AVATARS_DIR }) | 46 | const reqAvatarFile = createReqFiles([ 'avatarfile' ], IMAGE_MIMETYPE_EXT, { avatarfile: CONFIG.STORAGE.AVATARS_DIR }) |
47 | const loginRateLimiter = new RateLimit({ | ||
48 | windowMs: RATES_LIMIT.LOGIN.WINDOW_MS, | ||
49 | max: RATES_LIMIT.LOGIN.MAX, | ||
50 | delayMs: 0 | ||
51 | }) | ||
46 | 52 | ||
47 | const usersRouter = express.Router() | 53 | const usersRouter = express.Router() |
48 | 54 | ||
@@ -136,7 +142,11 @@ usersRouter.post('/:id/reset-password', | |||
136 | asyncMiddleware(resetUserPassword) | 142 | asyncMiddleware(resetUserPassword) |
137 | ) | 143 | ) |
138 | 144 | ||
139 | usersRouter.post('/token', token, success) | 145 | usersRouter.post('/token', |
146 | loginRateLimiter, | ||
147 | token, | ||
148 | success | ||
149 | ) | ||
140 | // TODO: Once https://github.com/oauthjs/node-oauth2-server/pull/289 is merged, implement revoke token route | 150 | // TODO: Once https://github.com/oauthjs/node-oauth2-server/pull/289 is merged, implement revoke token route |
141 | 151 | ||
142 | // --------------------------------------------------------------------------- | 152 | // --------------------------------------------------------------------------- |
diff --git a/server/controllers/api/videos/index.ts b/server/controllers/api/videos/index.ts index c0a8ac118..552e5edac 100644 --- a/server/controllers/api/videos/index.ts +++ b/server/controllers/api/videos/index.ts | |||
@@ -353,7 +353,7 @@ function getVideo (req: express.Request, res: express.Response) { | |||
353 | async function viewVideo (req: express.Request, res: express.Response) { | 353 | async function viewVideo (req: express.Request, res: express.Response) { |
354 | const videoInstance = res.locals.video | 354 | const videoInstance = res.locals.video |
355 | 355 | ||
356 | const ip = req.headers['x-real-ip'] as string || req.ip | 356 | const ip = req.ip |
357 | const exists = await Redis.Instance.isViewExists(ip, videoInstance.uuid) | 357 | const exists = await Redis.Instance.isViewExists(ip, videoInstance.uuid) |
358 | if (exists) { | 358 | if (exists) { |
359 | logger.debug('View for ip %s and video %s already exists.', ip, videoInstance.uuid) | 359 | logger.debug('View for ip %s and video %s already exists.', ip, videoInstance.uuid) |
diff --git a/server/initializers/checker.ts b/server/initializers/checker.ts index cd93f19a9..45f1d79c3 100644 --- a/server/initializers/checker.ts +++ b/server/initializers/checker.ts | |||
@@ -20,6 +20,7 @@ function checkConfig () { | |||
20 | function checkMissedConfig () { | 20 | function checkMissedConfig () { |
21 | const required = [ 'listen.port', | 21 | const required = [ 'listen.port', |
22 | 'webserver.https', 'webserver.hostname', 'webserver.port', | 22 | 'webserver.https', 'webserver.hostname', 'webserver.port', |
23 | 'trust_proxy', | ||
23 | 'database.hostname', 'database.port', 'database.suffix', 'database.username', 'database.password', | 24 | 'database.hostname', 'database.port', 'database.suffix', 'database.username', 'database.password', |
24 | 'redis.hostname', 'redis.port', 'redis.auth', | 25 | 'redis.hostname', 'redis.port', 'redis.auth', |
25 | 'smtp.hostname', 'smtp.port', 'smtp.username', 'smtp.password', 'smtp.tls', 'smtp.from_address', | 26 | 'smtp.hostname', 'smtp.port', 'smtp.username', 'smtp.password', 'smtp.tls', 'smtp.from_address', |
diff --git a/server/initializers/constants.ts b/server/initializers/constants.ts index 284acf8f3..986fed099 100644 --- a/server/initializers/constants.ts +++ b/server/initializers/constants.ts | |||
@@ -127,6 +127,7 @@ const CONFIG = { | |||
127 | URL: '', | 127 | URL: '', |
128 | HOST: '' | 128 | HOST: '' |
129 | }, | 129 | }, |
130 | TRUST_PROXY: config.get<string[]>('trust_proxy'), | ||
130 | LOG: { | 131 | LOG: { |
131 | LEVEL: config.get<string>('log.level') | 132 | LEVEL: config.get<string>('log.level') |
132 | }, | 133 | }, |
@@ -234,6 +235,13 @@ const CONSTRAINTS_FIELDS = { | |||
234 | } | 235 | } |
235 | } | 236 | } |
236 | 237 | ||
238 | const RATES_LIMIT = { | ||
239 | LOGIN: { | ||
240 | WINDOW_MS: 5 * 60 * 1000, // 5 minutes | ||
241 | MAX: 10 // 10 attempts | ||
242 | } | ||
243 | } | ||
244 | |||
237 | let VIDEO_VIEW_LIFETIME = 60000 * 60 // 1 hour | 245 | let VIDEO_VIEW_LIFETIME = 60000 * 60 // 1 hour |
238 | const VIDEO_TRANSCODING_FPS = { | 246 | const VIDEO_TRANSCODING_FPS = { |
239 | MIN: 10, | 247 | MIN: 10, |
@@ -468,6 +476,7 @@ export { | |||
468 | USER_PASSWORD_RESET_LIFETIME, | 476 | USER_PASSWORD_RESET_LIFETIME, |
469 | IMAGE_MIMETYPE_EXT, | 477 | IMAGE_MIMETYPE_EXT, |
470 | SCHEDULER_INTERVAL, | 478 | SCHEDULER_INTERVAL, |
479 | RATES_LIMIT, | ||
471 | JOB_COMPLETED_LIFETIME, | 480 | JOB_COMPLETED_LIFETIME, |
472 | VIDEO_VIEW_LIFETIME | 481 | VIDEO_VIEW_LIFETIME |
473 | } | 482 | } |
diff --git a/server/initializers/installer.ts b/server/initializers/installer.ts index d2f6c7c8c..f0adf8c9e 100644 --- a/server/initializers/installer.ts +++ b/server/initializers/installer.ts | |||
@@ -112,7 +112,7 @@ async function createOAuthAdminIfNotExist () { | |||
112 | // Our password is weak so do not validate it | 112 | // Our password is weak so do not validate it |
113 | validatePassword = false | 113 | validatePassword = false |
114 | } else { | 114 | } else { |
115 | password = passwordGenerator(8, true) | 115 | password = passwordGenerator(16, true) |
116 | } | 116 | } |
117 | 117 | ||
118 | const userData = { | 118 | const userData = { |
diff --git a/server/tests/api/server/reverse-proxy.ts b/server/tests/api/server/reverse-proxy.ts new file mode 100644 index 000000000..aa4b3ae81 --- /dev/null +++ b/server/tests/api/server/reverse-proxy.ts | |||
@@ -0,0 +1,82 @@ | |||
1 | /* tslint:disable:no-unused-expression */ | ||
2 | |||
3 | import 'mocha' | ||
4 | import * as chai from 'chai' | ||
5 | import { About } from '../../../../shared/models/server/about.model' | ||
6 | import { CustomConfig } from '../../../../shared/models/server/custom-config.model' | ||
7 | import { deleteCustomConfig, getAbout, getVideo, killallServers, login, reRunServer, uploadVideo, userLogin, viewVideo } from '../../utils' | ||
8 | const expect = chai.expect | ||
9 | |||
10 | import { | ||
11 | getConfig, | ||
12 | flushTests, | ||
13 | runServer, | ||
14 | registerUser, getCustomConfig, setAccessTokensToServers, updateCustomConfig | ||
15 | } from '../../utils/index' | ||
16 | |||
17 | describe('Test application behind a reverse proxy', function () { | ||
18 | let server = null | ||
19 | let videoId | ||
20 | |||
21 | before(async function () { | ||
22 | this.timeout(30000) | ||
23 | |||
24 | await flushTests() | ||
25 | server = await runServer(1) | ||
26 | await setAccessTokensToServers([ server ]) | ||
27 | |||
28 | const { body } = await uploadVideo(server.url, server.accessToken, {}) | ||
29 | videoId = body.video.uuid | ||
30 | }) | ||
31 | |||
32 | it('Should view a video only once with the same IP by default', async function () { | ||
33 | await viewVideo(server.url, videoId) | ||
34 | await viewVideo(server.url, videoId) | ||
35 | |||
36 | const { body } = await getVideo(server.url, videoId) | ||
37 | expect(body.views).to.equal(1) | ||
38 | }) | ||
39 | |||
40 | it('Should view a video 2 times with the X-Forwarded-For header set', async function () { | ||
41 | await viewVideo(server.url, videoId, 204, '0.0.0.1,127.0.0.1') | ||
42 | await viewVideo(server.url, videoId, 204, '0.0.0.2,127.0.0.1') | ||
43 | |||
44 | const { body } = await getVideo(server.url, videoId) | ||
45 | expect(body.views).to.equal(3) | ||
46 | }) | ||
47 | |||
48 | it('Should view a video only once with the same client IP in the X-Forwarded-For header', async function () { | ||
49 | await viewVideo(server.url, videoId, 204, '0.0.0.4,0.0.0.3,::ffff:127.0.0.1') | ||
50 | await viewVideo(server.url, videoId, 204, '0.0.0.5,0.0.0.3,127.0.0.1') | ||
51 | |||
52 | const { body } = await getVideo(server.url, videoId) | ||
53 | expect(body.views).to.equal(4) | ||
54 | }) | ||
55 | |||
56 | it('Should view a video two times with a different client IP in the X-Forwarded-For header', async function () { | ||
57 | await viewVideo(server.url, videoId, 204, '0.0.0.8,0.0.0.6,127.0.0.1') | ||
58 | await viewVideo(server.url, videoId, 204, '0.0.0.8,0.0.0.7,127.0.0.1') | ||
59 | |||
60 | const { body } = await getVideo(server.url, videoId) | ||
61 | expect(body.views).to.equal(6) | ||
62 | }) | ||
63 | |||
64 | it('Should rate limit logins', async function () { | ||
65 | const user = { username: 'root', password: 'fail' } | ||
66 | |||
67 | for (let i = 0; i < 9; i++) { | ||
68 | await userLogin(server, user, 400) | ||
69 | } | ||
70 | |||
71 | await userLogin(server, user, 429) | ||
72 | }) | ||
73 | |||
74 | after(async function () { | ||
75 | process.kill(-server.app.pid) | ||
76 | |||
77 | // Keep the logs if the test failed | ||
78 | if (this['ok']) { | ||
79 | await flushTests() | ||
80 | } | ||
81 | }) | ||
82 | }) | ||
diff --git a/server/tests/utils/videos/videos.ts b/server/tests/utils/videos/videos.ts index 424f41ed8..9bda53371 100644 --- a/server/tests/utils/videos/videos.ts +++ b/server/tests/utils/videos/videos.ts | |||
@@ -85,13 +85,18 @@ function getVideo (url: string, id: number | string, expectedStatus = 200) { | |||
85 | .expect(expectedStatus) | 85 | .expect(expectedStatus) |
86 | } | 86 | } |
87 | 87 | ||
88 | function viewVideo (url: string, id: number | string, expectedStatus = 204) { | 88 | function viewVideo (url: string, id: number | string, expectedStatus = 204, xForwardedFor?: string) { |
89 | const path = '/api/v1/videos/' + id + '/views' | 89 | const path = '/api/v1/videos/' + id + '/views' |
90 | 90 | ||
91 | return request(url) | 91 | const req = request(url) |
92 | .post(path) | 92 | .post(path) |
93 | .set('Accept', 'application/json') | 93 | .set('Accept', 'application/json') |
94 | .expect(expectedStatus) | 94 | |
95 | if (xForwardedFor) { | ||
96 | req.set('X-Forwarded-For', xForwardedFor) | ||
97 | } | ||
98 | |||
99 | return req.expect(expectedStatus) | ||
95 | } | 100 | } |
96 | 101 | ||
97 | function getVideoWithToken (url: string, token: string, id: number | string, expectedStatus = 200) { | 102 | function getVideoWithToken (url: string, token: string, id: number | string, expectedStatus = 200) { |