Fix runner api rate limit bypass

This commit is contained in:
Chocobozzz 2023-06-20 14:17:34 +02:00
parent 923e41fa4f
commit e915cde30e
No known key found for this signature in database
GPG key ID: 583A612D890159BE
26 changed files with 122 additions and 31 deletions

View file

@ -16,6 +16,7 @@ import {
abusesSortValidator,
abuseUpdateValidator,
addAbuseMessageValidator,
apiRateLimiter,
asyncMiddleware,
asyncRetryTransactionMiddleware,
authenticate,
@ -32,6 +33,8 @@ import { AccountModel } from '../../models/account/account'
const abuseRouter = express.Router()
abuseRouter.use(apiRateLimiter)
abuseRouter.get('/',
openapiOperationDoc({ operationId: 'getAbuses' }),
authenticate,

View file

@ -9,6 +9,7 @@ import { getFormattedObjects } from '../../helpers/utils'
import { JobQueue } from '../../lib/job-queue'
import { Hooks } from '../../lib/plugins/hooks'
import {
apiRateLimiter,
asyncMiddleware,
authenticate,
commonVideosFiltersValidator,
@ -41,6 +42,8 @@ import { VideoPlaylistModel } from '../../models/video/video-playlist'
const accountsRouter = express.Router()
accountsRouter.use(apiRateLimiter)
accountsRouter.get('/',
paginationValidator,
accountsSortValidator,

View file

@ -1,15 +1,17 @@
import express from 'express'
import { handleToNameAndHost } from '@server/helpers/actors'
import { logger } from '@server/helpers/logger'
import { AccountBlocklistModel } from '@server/models/account/account-blocklist'
import { getServerActor } from '@server/models/application/application'
import { ServerBlocklistModel } from '@server/models/server/server-blocklist'
import { MActorAccountId, MUserAccountId } from '@server/types/models'
import { BlockStatus } from '@shared/models'
import { asyncMiddleware, blocklistStatusValidator, optionalAuthenticate } from '../../middlewares'
import { logger } from '@server/helpers/logger'
import { apiRateLimiter, asyncMiddleware, blocklistStatusValidator, optionalAuthenticate } from '../../middlewares'
const blocklistRouter = express.Router()
blocklistRouter.use(apiRateLimiter)
blocklistRouter.get('/status',
optionalAuthenticate,
blocklistStatusValidator,

View file

@ -4,10 +4,12 @@ import { bulkRemoveCommentsOfValidator } from '@server/middlewares/validators/bu
import { VideoCommentModel } from '@server/models/video/video-comment'
import { HttpStatusCode } from '@shared/models'
import { BulkRemoveCommentsOfBody } from '@shared/models/bulk/bulk-remove-comments-of-body.model'
import { asyncMiddleware, authenticate } from '../../middlewares'
import { apiRateLimiter, asyncMiddleware, authenticate } from '../../middlewares'
const bulkRouter = express.Router()
bulkRouter.use(apiRateLimiter)
bulkRouter.post('/remove-comments-of',
authenticate,
asyncMiddleware(bulkRemoveCommentsOfValidator),

View file

@ -8,11 +8,13 @@ import { auditLoggerFactory, CustomConfigAuditView, getAuditIdFromRes } from '..
import { objectConverter } from '../../helpers/core-utils'
import { CONFIG, reloadConfig } from '../../initializers/config'
import { ClientHtml } from '../../lib/client-html'
import { asyncMiddleware, authenticate, ensureUserHasRight, openapiOperationDoc } from '../../middlewares'
import { apiRateLimiter, asyncMiddleware, authenticate, ensureUserHasRight, openapiOperationDoc } from '../../middlewares'
import { customConfigUpdateValidator, ensureConfigIsEditable } from '../../middlewares/validators/config'
const configRouter = express.Router()
configRouter.use(apiRateLimiter)
const auditLogger = auditLoggerFactory('config')
configRouter.get('/',

View file

@ -2,10 +2,12 @@ import express from 'express'
import { ServerConfigManager } from '@server/lib/server-config-manager'
import { ActorCustomPageModel } from '@server/models/account/actor-custom-page'
import { HttpStatusCode, UserRight } from '@shared/models'
import { asyncMiddleware, authenticate, ensureUserHasRight } from '../../middlewares'
import { apiRateLimiter, asyncMiddleware, authenticate, ensureUserHasRight } from '../../middlewares'
const customPageRouter = express.Router()
customPageRouter.use(apiRateLimiter)
customPageRouter.get('/homepage/instance',
asyncMiddleware(getInstanceHomepage)
)

View file

@ -1,9 +1,8 @@
import cors from 'cors'
import express from 'express'
import { buildRateLimiter } from '@server/middlewares'
import { HttpStatusCode } from '../../../shared/models'
import { badRequest } from '../../helpers/express-utils'
import { CONFIG } from '../../initializers/config'
import { abuseRouter } from './abuse'
import { accountsRouter } from './accounts'
import { blocklistRouter } from './blocklist'
@ -32,12 +31,6 @@ apiRouter.use(cors({
credentials: true
}))
const apiRateLimiter = buildRateLimiter({
windowMs: CONFIG.RATES_LIMIT.API.WINDOW_MS,
max: CONFIG.RATES_LIMIT.API.MAX
})
apiRouter.use(apiRateLimiter)
apiRouter.use('/server', serverRouter)
apiRouter.use('/abuses', abuseRouter)
apiRouter.use('/bulk', bulkRouter)
@ -57,6 +50,8 @@ apiRouter.use('/plugins', pluginRouter)
apiRouter.use('/custom-pages', customPageRouter)
apiRouter.use('/blocklist', blocklistRouter)
apiRouter.use('/runners', runnersRouter)
// apiRouter.use(apiRateLimiter)
apiRouter.use('/ping', pong)
apiRouter.use('/*', badRequest)

View file

@ -4,6 +4,7 @@ import { HttpStatusCode, Job, JobState, JobType, ResultList, UserRight } from '@
import { isArray } from '../../helpers/custom-validators/misc'
import { JobQueue } from '../../lib/job-queue'
import {
apiRateLimiter,
asyncMiddleware,
authenticate,
ensureUserHasRight,
@ -17,6 +18,8 @@ import { listJobsValidator } from '../../middlewares/validators/jobs'
const jobsRouter = express.Router()
jobsRouter.use(apiRateLimiter)
jobsRouter.post('/pause',
authenticate,
ensureUserHasRight(UserRight.MANAGE_JOBS),

View file

@ -1,11 +1,13 @@
import express from 'express'
import { CONFIG } from '@server/initializers/config'
import { OpenTelemetryMetrics } from '@server/lib/opentelemetry/metrics'
import { HttpStatusCode, PlaybackMetricCreate } from '@shared/models'
import { addPlaybackMetricValidator, asyncMiddleware } from '../../middlewares'
import { CONFIG } from '@server/initializers/config'
import { addPlaybackMetricValidator, apiRateLimiter, asyncMiddleware } from '../../middlewares'
const metricsRouter = express.Router()
metricsRouter.use(apiRateLimiter)
metricsRouter.post('/playback',
asyncMiddleware(addPlaybackMetricValidator),
addPlaybackMetric

View file

@ -4,10 +4,12 @@ import { OAuthClientModel } from '@server/models/oauth/oauth-client'
import { HttpStatusCode, OAuthClientLocal } from '@shared/models'
import { logger } from '../../helpers/logger'
import { CONFIG } from '../../initializers/config'
import { asyncMiddleware, openapiOperationDoc } from '../../middlewares'
import { apiRateLimiter, asyncMiddleware, openapiOperationDoc } from '../../middlewares'
const oauthClientsRouter = express.Router()
oauthClientsRouter.use(apiRateLimiter)
oauthClientsRouter.get('/local',
openapiOperationDoc({ operationId: 'getOAuthClient' }),
asyncMiddleware(getLocalClient)

View file

@ -2,16 +2,18 @@ import express from 'express'
import memoizee from 'memoizee'
import { logger } from '@server/helpers/logger'
import { Hooks } from '@server/lib/plugins/hooks'
import { getServerActor } from '@server/models/application/application'
import { VideoModel } from '@server/models/video/video'
import { CategoryOverview, ChannelOverview, TagOverview, VideosOverview } from '../../../shared/models/overviews'
import { buildNSFWFilter } from '../../helpers/express-utils'
import { MEMOIZE_TTL, OVERVIEWS } from '../../initializers/constants'
import { asyncMiddleware, optionalAuthenticate, videosOverviewValidator } from '../../middlewares'
import { apiRateLimiter, asyncMiddleware, optionalAuthenticate, videosOverviewValidator } from '../../middlewares'
import { TagModel } from '../../models/video/tag'
import { getServerActor } from '@server/models/application/application'
const overviewsRouter = express.Router()
overviewsRouter.use(apiRateLimiter)
overviewsRouter.get('/videos',
videosOverviewValidator,
optionalAuthenticate,

View file

@ -4,6 +4,7 @@ import { getFormattedObjects } from '@server/helpers/utils'
import { listAvailablePluginsFromIndex } from '@server/lib/plugins/plugin-index'
import { PluginManager } from '@server/lib/plugins/plugin-manager'
import {
apiRateLimiter,
asyncMiddleware,
authenticate,
availablePluginsSortValidator,
@ -35,6 +36,8 @@ import {
const pluginRouter = express.Router()
pluginRouter.use(apiRateLimiter)
pluginRouter.get('/available',
openapiOperationDoc({ operationId: 'getAvailablePlugins' }),
authenticate,

View file

@ -6,6 +6,8 @@ import { runnerRegistrationTokensRouter } from './registration-tokens'
const runnersRouter = express.Router()
// No api route limiter here, they are defined in child routers
runnersRouter.use('/', manageRunnersRouter)
runnersRouter.use('/', runnerJobsRouter)
runnersRouter.use('/', runnerJobFilesRouter)

View file

@ -3,7 +3,7 @@ import { logger, loggerTagsFactory } from '@server/helpers/logger'
import { proxifyHLS, proxifyWebTorrentFile } from '@server/lib/object-storage'
import { VideoPathManager } from '@server/lib/video-path-manager'
import { getStudioTaskFilePath } from '@server/lib/video-studio'
import { asyncMiddleware } from '@server/middlewares'
import { apiRateLimiter, asyncMiddleware } from '@server/middlewares'
import { jobOfRunnerGetValidator } from '@server/middlewares/validators/runners'
import {
runnerJobGetVideoStudioTaskFileValidator,
@ -16,18 +16,21 @@ const lTags = loggerTagsFactory('api', 'runner')
const runnerJobFilesRouter = express.Router()
runnerJobFilesRouter.post('/jobs/:jobUUID/files/videos/:videoId/max-quality',
apiRateLimiter,
asyncMiddleware(jobOfRunnerGetValidator),
asyncMiddleware(runnerJobGetVideoTranscodingFileValidator),
asyncMiddleware(getMaxQualityVideoFile)
)
runnerJobFilesRouter.post('/jobs/:jobUUID/files/videos/:videoId/previews/max-quality',
apiRateLimiter,
asyncMiddleware(jobOfRunnerGetValidator),
asyncMiddleware(runnerJobGetVideoTranscodingFileValidator),
getMaxQualityVideoPreview
)
runnerJobFilesRouter.post('/jobs/:jobUUID/files/videos/:videoId/studio/task-files/:filename',
apiRateLimiter,
asyncMiddleware(jobOfRunnerGetValidator),
asyncMiddleware(runnerJobGetVideoTranscodingFileValidator),
runnerJobGetVideoStudioTaskFileValidator,

View file

@ -7,6 +7,7 @@ import { MIMETYPES } from '@server/initializers/constants'
import { sequelizeTypescript } from '@server/initializers/database'
import { getRunnerJobHandlerClass, updateLastRunnerContact } from '@server/lib/runners'
import {
apiRateLimiter,
asyncMiddleware,
authenticate,
ensureUserHasRight,
@ -69,11 +70,13 @@ const runnerJobsRouter = express.Router()
// ---------------------------------------------------------------------------
runnerJobsRouter.post('/jobs/request',
apiRateLimiter,
asyncMiddleware(getRunnerFromTokenValidator),
asyncMiddleware(requestRunnerJob)
)
runnerJobsRouter.post('/jobs/:jobUUID/accept',
apiRateLimiter,
asyncMiddleware(runnerJobGetValidator),
acceptRunnerJobValidator,
asyncMiddleware(getRunnerFromTokenValidator),
@ -81,6 +84,7 @@ runnerJobsRouter.post('/jobs/:jobUUID/accept',
)
runnerJobsRouter.post('/jobs/:jobUUID/abort',
apiRateLimiter,
asyncMiddleware(jobOfRunnerGetValidator),
abortRunnerJobValidator,
asyncMiddleware(abortRunnerJob)
@ -88,6 +92,7 @@ runnerJobsRouter.post('/jobs/:jobUUID/abort',
runnerJobsRouter.post('/jobs/:jobUUID/update',
runnerJobUpdateVideoFiles,
apiRateLimiter, // Has to be after multer middleware to parse runner token
asyncMiddleware(jobOfRunnerGetValidator),
updateRunnerJobValidator,
asyncMiddleware(updateRunnerJobController)
@ -101,6 +106,7 @@ runnerJobsRouter.post('/jobs/:jobUUID/error',
runnerJobsRouter.post('/jobs/:jobUUID/success',
postRunnerJobSuccessVideoFiles,
apiRateLimiter, // Has to be after multer middleware to parse runner token
asyncMiddleware(jobOfRunnerGetValidator),
successRunnerJobValidator,
asyncMiddleware(postRunnerJobSuccess)

View file

@ -2,6 +2,7 @@ import express from 'express'
import { logger, loggerTagsFactory } from '@server/helpers/logger'
import { generateRunnerToken } from '@server/helpers/token-generator'
import {
apiRateLimiter,
asyncMiddleware,
authenticate,
ensureUserHasRight,
@ -19,15 +20,18 @@ const lTags = loggerTagsFactory('api', 'runner')
const manageRunnersRouter = express.Router()
manageRunnersRouter.post('/register',
apiRateLimiter,
asyncMiddleware(registerRunnerValidator),
asyncMiddleware(registerRunner)
)
manageRunnersRouter.post('/unregister',
apiRateLimiter,
asyncMiddleware(getRunnerFromTokenValidator),
asyncMiddleware(unregisterRunner)
)
manageRunnersRouter.delete('/:runnerId',
apiRateLimiter,
authenticate,
ensureUserHasRight(UserRight.MANAGE_RUNNERS),
asyncMiddleware(deleteRunnerValidator),
@ -35,6 +39,7 @@ manageRunnersRouter.delete('/:runnerId',
)
manageRunnersRouter.get('/',
apiRateLimiter,
authenticate,
ensureUserHasRight(UserRight.MANAGE_RUNNERS),
paginationValidator,

View file

@ -1,6 +1,8 @@
import express from 'express'
import { logger, loggerTagsFactory } from '@server/helpers/logger'
import { generateRunnerRegistrationToken } from '@server/helpers/token-generator'
import {
apiRateLimiter,
asyncMiddleware,
authenticate,
ensureUserHasRight,
@ -12,19 +14,20 @@ import {
import { deleteRegistrationTokenValidator } from '@server/middlewares/validators/runners'
import { RunnerRegistrationTokenModel } from '@server/models/runner/runner-registration-token'
import { HttpStatusCode, ListRunnerRegistrationTokensQuery, UserRight } from '@shared/models'
import { logger, loggerTagsFactory } from '@server/helpers/logger'
const lTags = loggerTagsFactory('api', 'runner')
const runnerRegistrationTokensRouter = express.Router()
runnerRegistrationTokensRouter.post('/registration-tokens/generate',
apiRateLimiter,
authenticate,
ensureUserHasRight(UserRight.MANAGE_RUNNERS),
asyncMiddleware(generateRegistrationToken)
)
runnerRegistrationTokensRouter.delete('/registration-tokens/:id',
apiRateLimiter,
authenticate,
ensureUserHasRight(UserRight.MANAGE_RUNNERS),
asyncMiddleware(deleteRegistrationTokenValidator),
@ -32,6 +35,7 @@ runnerRegistrationTokensRouter.delete('/registration-tokens/:id',
)
runnerRegistrationTokensRouter.get('/registration-tokens',
apiRateLimiter,
authenticate,
ensureUserHasRight(UserRight.MANAGE_RUNNERS),
paginationValidator,

View file

@ -1,10 +1,13 @@
import express from 'express'
import { apiRateLimiter } from '@server/middlewares'
import { searchChannelsRouter } from './search-video-channels'
import { searchPlaylistsRouter } from './search-video-playlists'
import { searchVideosRouter } from './search-videos'
const searchRouter = express.Router()
searchRouter.use(apiRateLimiter)
searchRouter.use('/', searchVideosRouter)
searchRouter.use('/', searchChannelsRouter)
searchRouter.use('/', searchPlaylistsRouter)

View file

@ -1,4 +1,5 @@
import express from 'express'
import { apiRateLimiter } from '@server/middlewares'
import { contactRouter } from './contact'
import { debugRouter } from './debug'
import { serverFollowsRouter } from './follows'
@ -9,6 +10,8 @@ import { statsRouter } from './stats'
const serverRouter = express.Router()
serverRouter.use(apiRateLimiter)
serverRouter.use('/', serverFollowsRouter)
serverRouter.use('/', serverRedundancyRouter)
serverRouter.use('/', statsRouter)

View file

@ -15,6 +15,7 @@ import { Redis } from '../../../lib/redis'
import { buildUser, createUserAccountAndChannelAndPlaylist } from '../../../lib/user'
import {
adminUsersSortValidator,
apiRateLimiter,
asyncMiddleware,
asyncRetryTransactionMiddleware,
authenticate,
@ -50,6 +51,9 @@ import { twoFactorRouter } from './two-factor'
const auditLogger = auditLoggerFactory('users')
const usersRouter = express.Router()
usersRouter.use(apiRateLimiter)
usersRouter.use('/', emailVerificationRouter)
usersRouter.use('/', registrationsRouter)
usersRouter.use('/', twoFactorRouter)

View file

@ -2,6 +2,7 @@ import express from 'express'
import { auditLoggerFactory, getAuditIdFromRes, VideoChannelSyncAuditView } from '@server/helpers/audit-logger'
import { logger } from '@server/helpers/logger'
import {
apiRateLimiter,
asyncMiddleware,
asyncRetryTransactionMiddleware,
authenticate,
@ -17,6 +18,8 @@ import { HttpStatusCode, VideoChannelSyncState } from '@shared/models'
const videoChannelSyncRouter = express.Router()
const auditLogger = auditLoggerFactory('channel-syncs')
videoChannelSyncRouter.use(apiRateLimiter)
videoChannelSyncRouter.post('/',
authenticate,
ensureSyncIsEnabled,

View file

@ -19,6 +19,7 @@ import { JobQueue } from '../../lib/job-queue'
import { deleteLocalActorImageFile, updateLocalActorImageFiles } from '../../lib/local-actor'
import { createLocalVideoChannel, federateAllVideosOfChannel } from '../../lib/video-channel'
import {
apiRateLimiter,
asyncMiddleware,
asyncRetryTransactionMiddleware,
authenticate,
@ -57,6 +58,8 @@ const reqBannerFile = createReqFiles([ 'bannerfile' ], MIMETYPES.IMAGE.MIMETYPE_
const videoChannelRouter = express.Router()
videoChannelRouter.use(apiRateLimiter)
videoChannelRouter.get('/',
paginationValidator,
videoChannelsSortValidator,

View file

@ -25,6 +25,7 @@ import { sendCreateVideoPlaylist, sendDeleteVideoPlaylist, sendUpdateVideoPlayli
import { getLocalVideoPlaylistActivityPubUrl, getLocalVideoPlaylistElementActivityPubUrl } from '../../lib/activitypub/url'
import { updatePlaylistMiniatureFromExisting } from '../../lib/thumbnail'
import {
apiRateLimiter,
asyncMiddleware,
asyncRetryTransactionMiddleware,
authenticate,
@ -52,6 +53,8 @@ const reqThumbnailFile = createReqFiles([ 'thumbnailfile' ], MIMETYPES.IMAGE.MIM
const videoPlaylistRouter = express.Router()
videoPlaylistRouter.use(apiRateLimiter)
videoPlaylistRouter.get('/privacies', listVideoPlaylistPrivacies)
videoPlaylistRouter.get('/',

View file

@ -15,6 +15,7 @@ import { sequelizeTypescript } from '../../../initializers/database'
import { JobQueue } from '../../../lib/job-queue'
import { Hooks } from '../../../lib/plugins/hooks'
import {
apiRateLimiter,
asyncMiddleware,
asyncRetryTransactionMiddleware,
authenticate,
@ -50,6 +51,8 @@ import { viewRouter } from './view'
const auditLogger = auditLoggerFactory('videos')
const videosRouter = express.Router()
videosRouter.use(apiRateLimiter)
videosRouter.use('/', blacklistRouter)
videosRouter.use('/', statsRouter)
videosRouter.use('/', rateVideoRouter)

View file

@ -1,5 +1,6 @@
import express from 'express'
import RateLimit, { Options as RateLimitHandlerOptions } from 'express-rate-limit'
import { CONFIG } from '@server/initializers/config'
import { RunnerModel } from '@server/models/runner/runner'
import { UserRole } from '@shared/models'
import { optionalAuthenticate } from './auth'
@ -39,6 +40,11 @@ export function buildRateLimiter (options: {
})
}
export const apiRateLimiter = buildRateLimiter({
windowMs: CONFIG.RATES_LIMIT.API.WINDOW_MS,
max: CONFIG.RATES_LIMIT.API.MAX
})
// ---------------------------------------------------------------------------
// Private
// ---------------------------------------------------------------------------

View file

@ -14,7 +14,6 @@ import {
import {
cleanupTests,
createSingleServer,
makePostBodyRequest,
PeerTubeServer,
setAccessTokensToServers,
setDefaultVideoChannel,
@ -641,24 +640,47 @@ describe('Test runner common actions', function () {
})
})
it('Should rate limit an unknown runner', async function () {
const path = '/api/v1/ping'
const fields = { runnerToken: 'toto' }
it('Should rate limit an unknown runner, but not a registered one', async function () {
this.timeout(60000)
await server.videos.quickUpload({ name: 'video' })
await waitJobs([ server ])
const { job } = await server.runnerJobs.autoAccept({ runnerToken })
for (let i = 0; i < 20; i++) {
try {
await makePostBodyRequest({ url: server.url, path, fields, expectedStatus: HttpStatusCode.OK_200 })
await server.runnerJobs.request({ runnerToken })
await server.runnerJobs.update({ runnerToken, jobToken: job.jobToken, jobUUID: job.uuid })
} catch {}
}
await makePostBodyRequest({ url: server.url, path, fields, expectedStatus: HttpStatusCode.TOO_MANY_REQUESTS_429 })
})
// Invalid
{
await server.runnerJobs.request({ runnerToken: 'toto', expectedStatus: HttpStatusCode.TOO_MANY_REQUESTS_429 })
await server.runnerJobs.update({
runnerToken: 'toto',
jobToken: job.jobToken,
jobUUID: job.uuid,
expectedStatus: HttpStatusCode.TOO_MANY_REQUESTS_429
})
}
it('Should not rate limit a registered runner', async function () {
const path = '/api/v1/ping'
// Not provided
{
await server.runnerJobs.request({ runnerToken: undefined, expectedStatus: HttpStatusCode.TOO_MANY_REQUESTS_429 })
await server.runnerJobs.update({
runnerToken: undefined,
jobToken: job.jobToken,
jobUUID: job.uuid,
expectedStatus: HttpStatusCode.TOO_MANY_REQUESTS_429
})
}
for (let i = 0; i < 20; i++) {
await makePostBodyRequest({ url: server.url, path, fields: { runnerToken }, expectedStatus: HttpStatusCode.OK_200 })
// Registered
{
await server.runnerJobs.request({ runnerToken })
await server.runnerJobs.update({ runnerToken, jobToken: job.jobToken, jobUUID: job.uuid })
}
})
})