Add rate limit to registration and API endpoints
This commit is contained in:
parent
fd0bfc3ac4
commit
c1340a6ac3
9 changed files with 112 additions and 23 deletions
|
@ -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
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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) {
|
||||||
|
|
|
@ -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)
|
||||||
|
|
|
@ -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),
|
||||||
|
|
|
@ -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')
|
||||||
|
|
|
@ -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,
|
||||||
|
|
|
@ -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 ])
|
||||||
})
|
})
|
||||||
|
|
Loading…
Reference in a new issue