Skip to content
Snippets Groups Projects
Unverified Commit c1340a6a authored by Chocobozzz's avatar Chocobozzz
Browse files

Add rate limit to registration and API endpoints

parent fd0bfc3a
No related branches found
No related tags found
No related merge requests found
......@@ -10,10 +10,18 @@ webserver:
port: 9000
rates_limit:
api:
# 50 attempts in 10 seconds
window: 10 seconds
max: 50
login:
# 15 attempts in 5 min
window: 5 minutes
max: 15
signup:
# 2 attempts in 5 min (only succeeded attempts are taken into account)
window: 5 minutes
max: 2
ask_send_email:
# 3 attempts in 5 min
window: 5 minutes
......
......@@ -9,10 +9,18 @@ webserver:
port: 443
rates_limit:
api:
# 50 attempts in 10 seconds
window: 10 seconds
max: 50
login:
# 15 attempts in 5 min
window: 5 minutes
max: 15
signup:
# 2 attempts in 5 min (only succeeded attempts are taken into account)
window: 5 minutes
max: 2
ask_send_email:
# 3 attempts in 5 min
window: 5 minutes
......
......@@ -5,6 +5,14 @@ listen:
webserver:
https: false
rates_limit:
signup:
window: 10 minutes
max: 50
login:
window: 5 minutes
max: 20
database:
hostname: 'localhost'
port: 5432
......
......@@ -27,9 +27,9 @@ const app = express()
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)
import { logger } from './server/helpers/logger'
import { API_VERSION, FILES_CACHE, WEBSERVER, loadLanguages } from './server/initializers/constants'
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()
if (missed.length !== 0) {
......
import * as express from 'express'
import * as RateLimit from 'express-rate-limit'
import { configRouter } from './config'
import { jobsRouter } from './jobs'
import { oauthClientsRouter } from './oauth-clients'
......@@ -12,6 +13,7 @@ import * as cors from 'cors'
import { searchRouter } from './search'
import { overviewsRouter } from './overviews'
import { videoPlaylistRouter } from './video-playlist'
import { CONFIG } from '../../initializers/config'
const apiRouter = express.Router()
......@@ -21,6 +23,14 @@ apiRouter.use(cors({
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('/oauth-clients', oauthClientsRouter)
apiRouter.use('/config', configRouter)
......
......@@ -3,7 +3,7 @@ import * as RateLimit from 'express-rate-limit'
import { UserCreate, UserRight, UserRole, UserUpdate } from '../../../../shared'
import { logger } from '../../../helpers/logger'
import { getFormattedObjects } from '../../../helpers/utils'
import { RATES_LIMIT, WEBSERVER } from '../../../initializers/constants'
import { WEBSERVER } from '../../../initializers/constants'
import { Emailer } from '../../../lib/emailer'
import { Redis } from '../../../lib/redis'
import { createUserAccountAndChannelAndPlaylist, sendVerifyUserEmail } from '../../../lib/user'
......@@ -53,14 +53,21 @@ const auditLogger = auditLoggerFactory('users')
// FIXME: https://github.com/nfriedly/express-rate-limit/issues/138
// @ts-ignore
const loginRateLimiter = RateLimit({
windowMs: RATES_LIMIT.LOGIN.WINDOW_MS,
max: RATES_LIMIT.LOGIN.MAX
windowMs: CONFIG.RATES_LIMIT.LOGIN.WINDOW_MS,
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
const askSendEmailLimiter = new RateLimit({
windowMs: RATES_LIMIT.ASK_SEND_EMAIL.WINDOW_MS,
max: RATES_LIMIT.ASK_SEND_EMAIL.MAX
windowMs: CONFIG.RATES_LIMIT.ASK_SEND_EMAIL.WINDOW_MS,
max: CONFIG.RATES_LIMIT.ASK_SEND_EMAIL.MAX
})
const usersRouter = express.Router()
......@@ -114,6 +121,7 @@ usersRouter.post('/',
)
usersRouter.post('/register',
signupRateLimiter,
asyncMiddleware(ensureUserRegistrationAllowed),
ensureUserRegistrationAllowedForIP,
asyncMiddleware(usersRegisterValidator),
......
......@@ -72,6 +72,14 @@ const CONFIG = {
PORT: config.get<number>('webserver.port')
},
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: {
WINDOW_MS: parseDurationToMs(config.get<string>('rates_limit.login.window')),
MAX: config.get<number>('rates_limit.login.max')
......
......@@ -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 CONTACT_FORM_LIFETIME = 60000 * 60 // 1 hour
......@@ -624,8 +613,6 @@ if (isTestInstance() === true) {
FILES_CACHE.VIDEO_CAPTIONS.MAX_AGE = 3000
MEMOIZE_TTL.OVERVIEWS_SAMPLE = 1
ROUTE_CACHE_LIFETIME.OVERVIEWS.VIDEOS = '0ms'
RATES_LIMIT.LOGIN.MAX = 20
}
updateWebserverUrls()
......@@ -696,7 +683,6 @@ export {
SCHEDULER_INTERVALS_MS,
REPEAT_JOBS,
STATIC_DOWNLOAD_PATHS,
RATES_LIMIT,
MIMETYPES,
CRAWL_REQUEST_CONCURRENCY,
DEFAULT_AUDIO_RESOLUTION,
......
......@@ -2,7 +2,7 @@
import 'mocha'
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'
const expect = chai.expect
......@@ -13,7 +13,27 @@ describe('Test application behind a reverse proxy', function () {
before(async function () {
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 ])
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)
})
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 () {
await cleanupTests([ server ])
})
......
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment