1
0
Fork 0
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:
Chocobozzz 2025-04-03 10:54:13 +02:00
parent 71744313f0
commit 69c851c8e6
No known key found for this signature in database
GPG key ID: 583A612D890159BE
5 changed files with 119 additions and 20 deletions

View file

@ -21,6 +21,7 @@ import './registrations.js'
import './runners.js'
import './search.js'
import './services.js'
import './static.js'
import './transcoding.js'
import './two-factor.js'
import './upload-quota.js'

View 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 ])
})
})

View file

@ -12,7 +12,7 @@ import {
waitJobs
} from '@peertube/peertube-server-commands'
describe('Test videos files', function () {
describe('Test videos files API validators', function () {
let servers: PeerTubeServer[]
let userToken: string

View file

@ -55,7 +55,7 @@ const privateHLSStaticMiddlewares = CONFIG.STATIC_FILES.PRIVATE_FILES_REQUIRE_AU
: []
staticRouter.use(
STATIC_PATHS.STREAMING_PLAYLISTS.PRIVATE_HLS + ':videoUUID/:playlistName.m3u8',
STATIC_PATHS.STREAMING_PLAYLISTS.PRIVATE_HLS + ':videoUUID/:playlistNameWithoutExtension.m3u8',
...privateHLSStaticMiddlewares,
asyncMiddleware(servePrivateM3U8)
)
@ -81,8 +81,8 @@ export {
// ---------------------------------------------------------------------------
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 filename = req.params.playlistName + '.m3u8'
const path = join(DIRECTORIES.HLS_STREAMING_PLAYLIST.PRIVATE, req.params.videoUUID, req.params.playlistNameWithoutExtension + '.m3u8')
const filename = req.params.playlistNameWithoutExtension + '.m3u8'
let playlistContent: string

View file

@ -1,21 +1,27 @@
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 { LRU_CACHE } from '@server/initializers/constants.js'
import { VideoFileModel } from '@server/models/video/video-file.js'
import { VideoModel } from '@server/models/video/video.js'
import { MStreamingPlaylist, MVideoFile, MVideoThumbnailBlacklist } from '@server/types/models/index.js'
import express from 'express'
import { query } from 'express-validator'
import { param, query } from 'express-validator'
import { LRUCache } from 'lru-cache'
import { basename, dirname } from 'path'
import { basename } from 'path'
import { areValidationErrors, checkCanAccessVideoStaticFiles, isValidVideoPasswordHeader } from './shared/index.js'
type LRUValue = {
allowed: boolean
video?: MVideoThumbnailBlacklist
file?: MVideoFile
playlist?: MStreamingPlaylist }
playlist?: MStreamingPlaylist
}
const staticFileTokenBypass = new LRUCache<string, LRUValue>({
max: LRU_CACHE.STATIC_VIDEO_FILES_RIGHTS_CHECK.MAX_SIZE,
@ -62,6 +68,13 @@ const ensureCanAccessVideoPrivateWebVideoFiles = [
]
const ensureCanAccessPrivateVideoHLSFiles = [
param('videoUUID')
.custom(isUUIDValid),
param('playlistNameWithoutExtension')
.optional()
.custom(v => isSafePeerTubeFilenameWithoutExtension(v)),
query('videoFileToken')
.optional()
.custom(exists),
@ -71,22 +84,12 @@ const ensureCanAccessPrivateVideoHLSFiles = [
.customSanitizer(toBooleanOrNull)
.isBoolean().withMessage('Should be a valid reinjectVideoFileToken boolean'),
query('playlistName')
.optional()
.customSanitizer(isSafePeerTubeFilenameWithoutExtension),
isValidVideoPasswordHeader(),
async (req: express.Request, res: express.Response, next: express.NextFunction) => {
if (areValidationErrors(req, res)) return
const videoUUID = basename(dirname(req.originalUrl))
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 videoUUID = req.params.videoUUID
const token = extractTokenOrDie(req, res)
if (!token) return
@ -122,7 +125,8 @@ const ensureCanAccessPrivateVideoHLSFiles = [
]
export {
ensureCanAccessPrivateVideoHLSFiles, ensureCanAccessVideoPrivateWebVideoFiles
ensureCanAccessPrivateVideoHLSFiles,
ensureCanAccessVideoPrivateWebVideoFiles
}
// ---------------------------------------------------------------------------