1
0
Fork 0

Add rate limit to registration and API endpoints

This commit is contained in:
Chocobozzz 2019-07-04 16:42:40 +02:00
parent fd0bfc3ac4
commit c1340a6ac3
No known key found for this signature in database
GPG key ID: 583A612D890159BE
9 changed files with 112 additions and 23 deletions

View file

@ -10,10 +10,18 @@ webserver:
port: 9000 port: 9000
rates_limit: rates_limit:
api:
# 50 attempts in 10 seconds
window: 10 seconds
max: 50
login: login:
# 15 attempts in 5 min # 15 attempts in 5 min
window: 5 minutes window: 5 minutes
max: 15 max: 15
signup:
# 2 attempts in 5 min (only succeeded attempts are taken into account)
window: 5 minutes
max: 2
ask_send_email: ask_send_email:
# 3 attempts in 5 min # 3 attempts in 5 min
window: 5 minutes window: 5 minutes

View file

@ -9,10 +9,18 @@ webserver:
port: 443 port: 443
rates_limit: rates_limit:
api:
# 50 attempts in 10 seconds
window: 10 seconds
max: 50
login: login:
# 15 attempts in 5 min # 15 attempts in 5 min
window: 5 minutes window: 5 minutes
max: 15 max: 15
signup:
# 2 attempts in 5 min (only succeeded attempts are taken into account)
window: 5 minutes
max: 2
ask_send_email: ask_send_email:
# 3 attempts in 5 min # 3 attempts in 5 min
window: 5 minutes window: 5 minutes

View file

@ -5,6 +5,14 @@ listen:
webserver: webserver:
https: false https: false
rates_limit:
signup:
window: 10 minutes
max: 50
login:
window: 5 minutes
max: 20
database: database:
hostname: 'localhost' hostname: 'localhost'
port: 5432 port: 5432

View file

@ -27,9 +27,9 @@ const app = express()
import { checkMissedConfig, checkFFmpeg } from './server/initializers/checker-before-init' import { checkMissedConfig, checkFFmpeg } from './server/initializers/checker-before-init'
// Do not use barrels because we don't want to load all modules here (we need to initialize database first) // Do not use barrels because we don't want to load all modules here (we need to initialize database first)
import { logger } from './server/helpers/logger'
import { API_VERSION, FILES_CACHE, WEBSERVER, loadLanguages } from './server/initializers/constants'
import { CONFIG } from './server/initializers/config' import { CONFIG } from './server/initializers/config'
import { API_VERSION, FILES_CACHE, WEBSERVER, loadLanguages } from './server/initializers/constants'
import { logger } from './server/helpers/logger'
const missed = checkMissedConfig() const missed = checkMissedConfig()
if (missed.length !== 0) { if (missed.length !== 0) {

View file

@ -1,4 +1,5 @@
import * as express from 'express' import * as express from 'express'
import * as RateLimit from 'express-rate-limit'
import { configRouter } from './config' import { configRouter } from './config'
import { jobsRouter } from './jobs' import { jobsRouter } from './jobs'
import { oauthClientsRouter } from './oauth-clients' import { oauthClientsRouter } from './oauth-clients'
@ -12,6 +13,7 @@ import * as cors from 'cors'
import { searchRouter } from './search' import { searchRouter } from './search'
import { overviewsRouter } from './overviews' import { overviewsRouter } from './overviews'
import { videoPlaylistRouter } from './video-playlist' import { videoPlaylistRouter } from './video-playlist'
import { CONFIG } from '../../initializers/config'
const apiRouter = express.Router() const apiRouter = express.Router()
@ -21,6 +23,14 @@ apiRouter.use(cors({
credentials: true credentials: true
})) }))
// FIXME: https://github.com/nfriedly/express-rate-limit/issues/138
// @ts-ignore
const apiRateLimiter = RateLimit({
windowMs: CONFIG.RATES_LIMIT.API.WINDOW_MS,
max: CONFIG.RATES_LIMIT.API.MAX
})
apiRouter.use(apiRateLimiter)
apiRouter.use('/server', serverRouter) apiRouter.use('/server', serverRouter)
apiRouter.use('/oauth-clients', oauthClientsRouter) apiRouter.use('/oauth-clients', oauthClientsRouter)
apiRouter.use('/config', configRouter) apiRouter.use('/config', configRouter)

View file

@ -3,7 +3,7 @@ import * as RateLimit from 'express-rate-limit'
import { UserCreate, UserRight, UserRole, UserUpdate } from '../../../../shared' import { UserCreate, UserRight, UserRole, UserUpdate } from '../../../../shared'
import { logger } from '../../../helpers/logger' import { logger } from '../../../helpers/logger'
import { getFormattedObjects } from '../../../helpers/utils' import { getFormattedObjects } from '../../../helpers/utils'
import { RATES_LIMIT, WEBSERVER } from '../../../initializers/constants' import { WEBSERVER } from '../../../initializers/constants'
import { Emailer } from '../../../lib/emailer' import { Emailer } from '../../../lib/emailer'
import { Redis } from '../../../lib/redis' import { Redis } from '../../../lib/redis'
import { createUserAccountAndChannelAndPlaylist, sendVerifyUserEmail } from '../../../lib/user' import { createUserAccountAndChannelAndPlaylist, sendVerifyUserEmail } from '../../../lib/user'
@ -53,14 +53,21 @@ const auditLogger = auditLoggerFactory('users')
// FIXME: https://github.com/nfriedly/express-rate-limit/issues/138 // FIXME: https://github.com/nfriedly/express-rate-limit/issues/138
// @ts-ignore // @ts-ignore
const loginRateLimiter = RateLimit({ const loginRateLimiter = RateLimit({
windowMs: RATES_LIMIT.LOGIN.WINDOW_MS, windowMs: CONFIG.RATES_LIMIT.LOGIN.WINDOW_MS,
max: RATES_LIMIT.LOGIN.MAX max: CONFIG.RATES_LIMIT.LOGIN.MAX
})
// @ts-ignore
const signupRateLimiter = RateLimit({
windowMs: CONFIG.RATES_LIMIT.SIGNUP.WINDOW_MS,
max: CONFIG.RATES_LIMIT.SIGNUP.MAX,
skipFailedRequests: true
}) })
// @ts-ignore // @ts-ignore
const askSendEmailLimiter = new RateLimit({ const askSendEmailLimiter = new RateLimit({
windowMs: RATES_LIMIT.ASK_SEND_EMAIL.WINDOW_MS, windowMs: CONFIG.RATES_LIMIT.ASK_SEND_EMAIL.WINDOW_MS,
max: RATES_LIMIT.ASK_SEND_EMAIL.MAX max: CONFIG.RATES_LIMIT.ASK_SEND_EMAIL.MAX
}) })
const usersRouter = express.Router() const usersRouter = express.Router()
@ -114,6 +121,7 @@ usersRouter.post('/',
) )
usersRouter.post('/register', usersRouter.post('/register',
signupRateLimiter,
asyncMiddleware(ensureUserRegistrationAllowed), asyncMiddleware(ensureUserRegistrationAllowed),
ensureUserRegistrationAllowedForIP, ensureUserRegistrationAllowedForIP,
asyncMiddleware(usersRegisterValidator), asyncMiddleware(usersRegisterValidator),

View file

@ -72,6 +72,14 @@ const CONFIG = {
PORT: config.get<number>('webserver.port') PORT: config.get<number>('webserver.port')
}, },
RATES_LIMIT: { RATES_LIMIT: {
API: {
WINDOW_MS: parseDurationToMs(config.get<string>('rates_limit.api.window')),
MAX: config.get<number>('rates_limit.api.max')
},
SIGNUP: {
WINDOW_MS: parseDurationToMs(config.get<string>('rates_limit.signup.window')),
MAX: config.get<number>('rates_limit.signup.max')
},
LOGIN: { LOGIN: {
WINDOW_MS: parseDurationToMs(config.get<string>('rates_limit.login.window')), WINDOW_MS: parseDurationToMs(config.get<string>('rates_limit.login.window')),
MAX: config.get<number>('rates_limit.login.max') MAX: config.get<number>('rates_limit.login.max')

View file

@ -280,17 +280,6 @@ let CONSTRAINTS_FIELDS = {
} }
} }
const RATES_LIMIT = {
LOGIN: {
WINDOW_MS: CONFIG.RATES_LIMIT.LOGIN.WINDOW_MS,
MAX: CONFIG.RATES_LIMIT.LOGIN.MAX
},
ASK_SEND_EMAIL: {
WINDOW_MS: CONFIG.RATES_LIMIT.ASK_SEND_EMAIL.WINDOW_MS,
MAX: CONFIG.RATES_LIMIT.ASK_SEND_EMAIL.MAX
}
}
let VIDEO_VIEW_LIFETIME = 60000 * 60 // 1 hour let VIDEO_VIEW_LIFETIME = 60000 * 60 // 1 hour
let CONTACT_FORM_LIFETIME = 60000 * 60 // 1 hour let CONTACT_FORM_LIFETIME = 60000 * 60 // 1 hour
@ -624,8 +613,6 @@ if (isTestInstance() === true) {
FILES_CACHE.VIDEO_CAPTIONS.MAX_AGE = 3000 FILES_CACHE.VIDEO_CAPTIONS.MAX_AGE = 3000
MEMOIZE_TTL.OVERVIEWS_SAMPLE = 1 MEMOIZE_TTL.OVERVIEWS_SAMPLE = 1
ROUTE_CACHE_LIFETIME.OVERVIEWS.VIDEOS = '0ms' ROUTE_CACHE_LIFETIME.OVERVIEWS.VIDEOS = '0ms'
RATES_LIMIT.LOGIN.MAX = 20
} }
updateWebserverUrls() updateWebserverUrls()
@ -696,7 +683,6 @@ export {
SCHEDULER_INTERVALS_MS, SCHEDULER_INTERVALS_MS,
REPEAT_JOBS, REPEAT_JOBS,
STATIC_DOWNLOAD_PATHS, STATIC_DOWNLOAD_PATHS,
RATES_LIMIT,
MIMETYPES, MIMETYPES,
CRAWL_REQUEST_CONCURRENCY, CRAWL_REQUEST_CONCURRENCY,
DEFAULT_AUDIO_RESOLUTION, DEFAULT_AUDIO_RESOLUTION,

View file

@ -2,7 +2,7 @@
import 'mocha' import 'mocha'
import * as chai from 'chai' import * as chai from 'chai'
import { cleanupTests, getVideo, uploadVideo, userLogin, viewVideo, wait } from '../../../../shared/extra-utils' import { cleanupTests, getVideo, registerUser, uploadVideo, userLogin, viewVideo, wait } from '../../../../shared/extra-utils'
import { flushAndRunServer, setAccessTokensToServers } from '../../../../shared/extra-utils/index' import { flushAndRunServer, setAccessTokensToServers } from '../../../../shared/extra-utils/index'
const expect = chai.expect const expect = chai.expect
@ -13,7 +13,27 @@ describe('Test application behind a reverse proxy', function () {
before(async function () { before(async function () {
this.timeout(30000) this.timeout(30000)
server = await flushAndRunServer(1)
const config = {
rates_limit: {
api: {
max: 50,
window: 5000
},
signup: {
max: 3,
window: 5000
},
login: {
max: 20
}
},
signup: {
limit: 20
}
}
server = await flushAndRunServer(1, config)
await setAccessTokensToServers([ server ]) await setAccessTokensToServers([ server ])
const { body } = await uploadVideo(server.url, server.accessToken, {}) const { body } = await uploadVideo(server.url, server.accessToken, {})
@ -82,6 +102,39 @@ describe('Test application behind a reverse proxy', function () {
await userLogin(server, user, 429) await userLogin(server, user, 429)
}) })
it('Should rate limit signup', async function () {
for (let i = 0; i < 3; i++) {
await registerUser(server.url, 'test' + i, 'password')
}
await registerUser(server.url, 'test42', 'password', 429)
})
it('Should not rate limit failed signup', async function () {
this.timeout(30000)
await wait(7000)
for (let i = 0; i < 3; i++) {
await registerUser(server.url, 'test' + i, 'password', 409)
}
await registerUser(server.url, 'test43', 'password', 204)
})
it('Should rate limit API calls', async function () {
this.timeout(30000)
await wait(7000)
for (let i = 0; i < 50; i++) {
await getVideo(server.url, videoId)
}
await getVideo(server.url, videoId, 429)
})
after(async function () { after(async function () {
await cleanupTests([ server ]) await cleanupTests([ server ])
}) })