mirror of
https://github.com/Chocobozzz/PeerTube.git
synced 2025-10-03 17:59:37 +02:00
Fix path traversal when getting a private playlist
This commit is contained in:
parent
71744313f0
commit
69c851c8e6
5 changed files with 119 additions and 20 deletions
|
@ -21,6 +21,7 @@ import './registrations.js'
|
||||||
import './runners.js'
|
import './runners.js'
|
||||||
import './search.js'
|
import './search.js'
|
||||||
import './services.js'
|
import './services.js'
|
||||||
|
import './static.js'
|
||||||
import './transcoding.js'
|
import './transcoding.js'
|
||||||
import './two-factor.js'
|
import './two-factor.js'
|
||||||
import './upload-quota.js'
|
import './upload-quota.js'
|
||||||
|
|
94
packages/tests/src/api/check-params/static.ts
Normal file
94
packages/tests/src/api/check-params/static.ts
Normal file
|
@ -0,0 +1,94 @@
|
||||||
|
/* eslint-disable @typescript-eslint/no-unused-expressions,@typescript-eslint/require-await */
|
||||||
|
|
||||||
|
import { getHLS } from '@peertube/peertube-core-utils'
|
||||||
|
import { HttpStatusCode, VideoDetails, VideoPrivacy } from '@peertube/peertube-models'
|
||||||
|
import {
|
||||||
|
cleanupTests,
|
||||||
|
createSingleServer,
|
||||||
|
makeGetRequest,
|
||||||
|
PeerTubeServer,
|
||||||
|
setAccessTokensToServers,
|
||||||
|
waitJobs
|
||||||
|
} from '@peertube/peertube-server-commands'
|
||||||
|
import { expect } from 'chai'
|
||||||
|
import { basename } from 'path'
|
||||||
|
|
||||||
|
describe('Test static endpoints validators', function () {
|
||||||
|
let server: PeerTubeServer
|
||||||
|
|
||||||
|
let privateVideo: VideoDetails
|
||||||
|
let privateM3U8: string
|
||||||
|
|
||||||
|
let publicVideo: VideoDetails
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------
|
||||||
|
|
||||||
|
before(async function () {
|
||||||
|
this.timeout(300_000)
|
||||||
|
|
||||||
|
server = await createSingleServer(1)
|
||||||
|
await setAccessTokensToServers([ server ])
|
||||||
|
await server.config.enableMinimumTranscoding({ hls: true })
|
||||||
|
|
||||||
|
{
|
||||||
|
const { uuid } = await server.videos.quickUpload({ name: 'video 1', privacy: VideoPrivacy.PRIVATE })
|
||||||
|
await waitJobs([ server ])
|
||||||
|
|
||||||
|
privateVideo = await server.videos.getWithToken({ id: uuid })
|
||||||
|
privateM3U8 = basename(getHLS(privateVideo).playlistUrl)
|
||||||
|
}
|
||||||
|
|
||||||
|
{
|
||||||
|
const { uuid } = await server.videos.quickUpload({ name: 'video 2', privacy: VideoPrivacy.PUBLIC })
|
||||||
|
await waitJobs([ server ])
|
||||||
|
|
||||||
|
publicVideo = await server.videos.get({ id: uuid })
|
||||||
|
}
|
||||||
|
|
||||||
|
await waitJobs([ server ])
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('Getting m3u8 playlist', function () {
|
||||||
|
it('Should fail with an invalid video UUID', async function () {
|
||||||
|
await makeGetRequest({
|
||||||
|
url: server.url,
|
||||||
|
token: server.accessToken,
|
||||||
|
path: '/static/streaming-playlists/hls/private/toto/' + privateM3U8
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
it('Should fail with an invalid playlist name', async function () {
|
||||||
|
await makeGetRequest({
|
||||||
|
url: server.url,
|
||||||
|
token: server.accessToken,
|
||||||
|
path: '/static/streaming-playlists/hls/private/' + privateVideo.uuid + '/' + privateM3U8.replace('.m3u8', '.mp4')
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
it('Should fail with another m3u8 playlist of another video', async function () {
|
||||||
|
await makeGetRequest({
|
||||||
|
url: server.url,
|
||||||
|
headers: {
|
||||||
|
'x-peertube-video-password': 'fake'
|
||||||
|
},
|
||||||
|
path: '/static/streaming-playlists/hls/private/' + publicVideo.uuid + '/..%2f' + privateVideo.uuid + '%2f' + privateM3U8
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
it('Should succeed with the correct params', async function () {
|
||||||
|
const { text } = await makeGetRequest({
|
||||||
|
url: server.url,
|
||||||
|
token: server.accessToken,
|
||||||
|
path: '/static/streaming-playlists/hls/private/' + privateVideo.uuid + '/' + privateM3U8,
|
||||||
|
expectedStatus: HttpStatusCode.OK_200
|
||||||
|
})
|
||||||
|
|
||||||
|
expect(text).to.contain('#EXTM3U')
|
||||||
|
expect(text).to.contain(basename(getHLS(privateVideo).files[0].playlistUrl))
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
after(async function () {
|
||||||
|
await cleanupTests([ server ])
|
||||||
|
})
|
||||||
|
})
|
|
@ -12,7 +12,7 @@ import {
|
||||||
waitJobs
|
waitJobs
|
||||||
} from '@peertube/peertube-server-commands'
|
} from '@peertube/peertube-server-commands'
|
||||||
|
|
||||||
describe('Test videos files', function () {
|
describe('Test videos files API validators', function () {
|
||||||
let servers: PeerTubeServer[]
|
let servers: PeerTubeServer[]
|
||||||
|
|
||||||
let userToken: string
|
let userToken: string
|
||||||
|
|
|
@ -55,7 +55,7 @@ const privateHLSStaticMiddlewares = CONFIG.STATIC_FILES.PRIVATE_FILES_REQUIRE_AU
|
||||||
: []
|
: []
|
||||||
|
|
||||||
staticRouter.use(
|
staticRouter.use(
|
||||||
STATIC_PATHS.STREAMING_PLAYLISTS.PRIVATE_HLS + ':videoUUID/:playlistName.m3u8',
|
STATIC_PATHS.STREAMING_PLAYLISTS.PRIVATE_HLS + ':videoUUID/:playlistNameWithoutExtension.m3u8',
|
||||||
...privateHLSStaticMiddlewares,
|
...privateHLSStaticMiddlewares,
|
||||||
asyncMiddleware(servePrivateM3U8)
|
asyncMiddleware(servePrivateM3U8)
|
||||||
)
|
)
|
||||||
|
@ -81,8 +81,8 @@ export {
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
async function servePrivateM3U8 (req: express.Request, res: express.Response) {
|
async function servePrivateM3U8 (req: express.Request, res: express.Response) {
|
||||||
const path = join(DIRECTORIES.HLS_STREAMING_PLAYLIST.PRIVATE, req.params.videoUUID, req.params.playlistName + '.m3u8')
|
const path = join(DIRECTORIES.HLS_STREAMING_PLAYLIST.PRIVATE, req.params.videoUUID, req.params.playlistNameWithoutExtension + '.m3u8')
|
||||||
const filename = req.params.playlistName + '.m3u8'
|
const filename = req.params.playlistNameWithoutExtension + '.m3u8'
|
||||||
|
|
||||||
let playlistContent: string
|
let playlistContent: string
|
||||||
|
|
||||||
|
|
|
@ -1,21 +1,27 @@
|
||||||
import { HttpStatusCode } from '@peertube/peertube-models'
|
import { HttpStatusCode } from '@peertube/peertube-models'
|
||||||
import { exists, isSafePeerTubeFilenameWithoutExtension, isUUIDValid, toBooleanOrNull } from '@server/helpers/custom-validators/misc.js'
|
import {
|
||||||
|
exists,
|
||||||
|
isSafePeerTubeFilenameWithoutExtension,
|
||||||
|
isUUIDValid,
|
||||||
|
toBooleanOrNull
|
||||||
|
} from '@server/helpers/custom-validators/misc.js'
|
||||||
import { logger } from '@server/helpers/logger.js'
|
import { logger } from '@server/helpers/logger.js'
|
||||||
import { LRU_CACHE } from '@server/initializers/constants.js'
|
import { LRU_CACHE } from '@server/initializers/constants.js'
|
||||||
import { VideoFileModel } from '@server/models/video/video-file.js'
|
import { VideoFileModel } from '@server/models/video/video-file.js'
|
||||||
import { VideoModel } from '@server/models/video/video.js'
|
import { VideoModel } from '@server/models/video/video.js'
|
||||||
import { MStreamingPlaylist, MVideoFile, MVideoThumbnailBlacklist } from '@server/types/models/index.js'
|
import { MStreamingPlaylist, MVideoFile, MVideoThumbnailBlacklist } from '@server/types/models/index.js'
|
||||||
import express from 'express'
|
import express from 'express'
|
||||||
import { query } from 'express-validator'
|
import { param, query } from 'express-validator'
|
||||||
import { LRUCache } from 'lru-cache'
|
import { LRUCache } from 'lru-cache'
|
||||||
import { basename, dirname } from 'path'
|
import { basename } from 'path'
|
||||||
import { areValidationErrors, checkCanAccessVideoStaticFiles, isValidVideoPasswordHeader } from './shared/index.js'
|
import { areValidationErrors, checkCanAccessVideoStaticFiles, isValidVideoPasswordHeader } from './shared/index.js'
|
||||||
|
|
||||||
type LRUValue = {
|
type LRUValue = {
|
||||||
allowed: boolean
|
allowed: boolean
|
||||||
video?: MVideoThumbnailBlacklist
|
video?: MVideoThumbnailBlacklist
|
||||||
file?: MVideoFile
|
file?: MVideoFile
|
||||||
playlist?: MStreamingPlaylist }
|
playlist?: MStreamingPlaylist
|
||||||
|
}
|
||||||
|
|
||||||
const staticFileTokenBypass = new LRUCache<string, LRUValue>({
|
const staticFileTokenBypass = new LRUCache<string, LRUValue>({
|
||||||
max: LRU_CACHE.STATIC_VIDEO_FILES_RIGHTS_CHECK.MAX_SIZE,
|
max: LRU_CACHE.STATIC_VIDEO_FILES_RIGHTS_CHECK.MAX_SIZE,
|
||||||
|
@ -62,6 +68,13 @@ const ensureCanAccessVideoPrivateWebVideoFiles = [
|
||||||
]
|
]
|
||||||
|
|
||||||
const ensureCanAccessPrivateVideoHLSFiles = [
|
const ensureCanAccessPrivateVideoHLSFiles = [
|
||||||
|
param('videoUUID')
|
||||||
|
.custom(isUUIDValid),
|
||||||
|
|
||||||
|
param('playlistNameWithoutExtension')
|
||||||
|
.optional()
|
||||||
|
.custom(v => isSafePeerTubeFilenameWithoutExtension(v)),
|
||||||
|
|
||||||
query('videoFileToken')
|
query('videoFileToken')
|
||||||
.optional()
|
.optional()
|
||||||
.custom(exists),
|
.custom(exists),
|
||||||
|
@ -71,22 +84,12 @@ const ensureCanAccessPrivateVideoHLSFiles = [
|
||||||
.customSanitizer(toBooleanOrNull)
|
.customSanitizer(toBooleanOrNull)
|
||||||
.isBoolean().withMessage('Should be a valid reinjectVideoFileToken boolean'),
|
.isBoolean().withMessage('Should be a valid reinjectVideoFileToken boolean'),
|
||||||
|
|
||||||
query('playlistName')
|
|
||||||
.optional()
|
|
||||||
.customSanitizer(isSafePeerTubeFilenameWithoutExtension),
|
|
||||||
|
|
||||||
isValidVideoPasswordHeader(),
|
isValidVideoPasswordHeader(),
|
||||||
|
|
||||||
async (req: express.Request, res: express.Response, next: express.NextFunction) => {
|
async (req: express.Request, res: express.Response, next: express.NextFunction) => {
|
||||||
if (areValidationErrors(req, res)) return
|
if (areValidationErrors(req, res)) return
|
||||||
|
|
||||||
const videoUUID = basename(dirname(req.originalUrl))
|
const videoUUID = req.params.videoUUID
|
||||||
|
|
||||||
if (!isUUIDValid(videoUUID)) {
|
|
||||||
logger.debug('Path does not contain valid video UUID to serve static file %s', req.originalUrl)
|
|
||||||
|
|
||||||
return res.sendStatus(HttpStatusCode.FORBIDDEN_403)
|
|
||||||
}
|
|
||||||
|
|
||||||
const token = extractTokenOrDie(req, res)
|
const token = extractTokenOrDie(req, res)
|
||||||
if (!token) return
|
if (!token) return
|
||||||
|
@ -122,7 +125,8 @@ const ensureCanAccessPrivateVideoHLSFiles = [
|
||||||
]
|
]
|
||||||
|
|
||||||
export {
|
export {
|
||||||
ensureCanAccessPrivateVideoHLSFiles, ensureCanAccessVideoPrivateWebVideoFiles
|
ensureCanAccessPrivateVideoHLSFiles,
|
||||||
|
ensureCanAccessVideoPrivateWebVideoFiles
|
||||||
}
|
}
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue