mirror of
https://github.com/Dvorinka/swingmusic-extended.git
synced 2026-06-03 20:13:02 +00:00
security: fix fetching an arbirtrary file from the host server on stream endpoint
+ fix path traversal + check if requested file is outside root dirs + confirm resolved track hash matches the requested trackhash
This commit is contained in:
@@ -11,6 +11,7 @@ from typing import Literal
|
|||||||
from pydantic import BaseModel, Field
|
from pydantic import BaseModel, Field
|
||||||
from flask_openapi3 import APIBlueprint, Tag
|
from flask_openapi3 import APIBlueprint, Tag
|
||||||
from swingmusic.api.apischemas import TrackHashSchema
|
from swingmusic.api.apischemas import TrackHashSchema
|
||||||
|
from swingmusic.config import UserConfig
|
||||||
from swingmusic.lib.transcoder import start_transcoding
|
from swingmusic.lib.transcoder import start_transcoding
|
||||||
from flask import request, Response, send_from_directory
|
from flask import request, Response, send_from_directory
|
||||||
from swingmusic.lib.trackslib import get_silence_paddings
|
from swingmusic.lib.trackslib import get_silence_paddings
|
||||||
@@ -59,22 +60,45 @@ def send_track_file_legacy(path: TrackHashSchema, query: SendTrackFileQuery):
|
|||||||
|
|
||||||
NOTE: Does not support range requests or transcoding.
|
NOTE: Does not support range requests or transcoding.
|
||||||
"""
|
"""
|
||||||
trackhash = path.trackhash
|
requested_trackhash = path.trackhash.strip()
|
||||||
filepath = query.filepath
|
filepath = query.filepath.strip()
|
||||||
|
|
||||||
msg = {"msg": "File Not Found"}
|
msg = {"msg": "File Not Found"}
|
||||||
|
|
||||||
|
# prevent path traversal
|
||||||
|
if "/../" in filepath:
|
||||||
|
return {"msg": "Invalid filepath", "error": "Path traversal detected"}, 400
|
||||||
|
|
||||||
|
requested_filepath = Path(filepath).resolve()
|
||||||
|
|
||||||
|
# check if filepath is a child of any of the root dirs
|
||||||
|
for root_dir in UserConfig().rootDirs:
|
||||||
|
if root_dir == "$home":
|
||||||
|
root_dir = Path.home()
|
||||||
|
else:
|
||||||
|
root_dir = Path(root_dir).resolve()
|
||||||
|
|
||||||
|
if root_dir not in requested_filepath.parents:
|
||||||
|
return {
|
||||||
|
"msg": "Invalid filepath",
|
||||||
|
"error": "File not inside root directories",
|
||||||
|
}, 400
|
||||||
|
|
||||||
track = None
|
track = None
|
||||||
tracks = TrackStore.get_tracks_by_filepaths([filepath])
|
tracks = TrackStore.get_tracks_by_filepaths([filepath])
|
||||||
|
|
||||||
if len(tracks) > 0 and os.path.exists(filepath):
|
if len(tracks) > 0 and os.path.exists(tracks[0].filepath):
|
||||||
track = tracks[0]
|
for t in tracks:
|
||||||
|
if os.path.exists(t.filepath) and t.trackhash == requested_trackhash:
|
||||||
|
track = t
|
||||||
|
break
|
||||||
else:
|
else:
|
||||||
res = TrackStore.trackhashmap.get(trackhash)
|
group = TrackStore.trackhashmap.get(requested_trackhash)
|
||||||
|
|
||||||
# When finding by trackhash, sort by bitrate
|
# When finding by trackhash, sort by bitrate
|
||||||
# and get the first track that exists
|
# and get the first track that exists
|
||||||
if res is not None:
|
if group is not None:
|
||||||
tracks = sorted(res.tracks, key=lambda x: x.bitrate, reverse=True)
|
tracks = sorted(group.tracks, key=lambda x: x.bitrate, reverse=True)
|
||||||
|
|
||||||
for t in tracks:
|
for t in tracks:
|
||||||
if os.path.exists(t.filepath):
|
if os.path.exists(t.filepath):
|
||||||
@@ -82,10 +106,10 @@ def send_track_file_legacy(path: TrackHashSchema, query: SendTrackFileQuery):
|
|||||||
break
|
break
|
||||||
|
|
||||||
if track is not None:
|
if track is not None:
|
||||||
audio_type = guess_mime_type(filepath)
|
audio_type = guess_mime_type(track.filepath)
|
||||||
return send_from_directory(
|
return send_from_directory(
|
||||||
Path(filepath).parent,
|
Path(track.filepath).parent,
|
||||||
Path(filepath).name,
|
Path(track.filepath).name,
|
||||||
mimetype=audio_type,
|
mimetype=audio_type,
|
||||||
conditional=True,
|
conditional=True,
|
||||||
as_attachment=True,
|
as_attachment=True,
|
||||||
@@ -94,59 +118,59 @@ def send_track_file_legacy(path: TrackHashSchema, query: SendTrackFileQuery):
|
|||||||
return msg, 404
|
return msg, 404
|
||||||
|
|
||||||
|
|
||||||
@api.get("/<trackhash>")
|
# @api.get("/<trackhash>")
|
||||||
def send_track_file(path: TrackHashSchema, query: SendTrackFileQuery):
|
# def send_track_file(path: TrackHashSchema, query: SendTrackFileQuery):
|
||||||
"""
|
# """
|
||||||
Get a playable audio file with Range headers support
|
# Get a playable audio file with Range headers support
|
||||||
|
|
||||||
Returns a playable audio file that corresponds to the given filepath. Falls back to track hash if filepath is not found.
|
# Returns a playable audio file that corresponds to the given filepath. Falls back to track hash if filepath is not found.
|
||||||
|
|
||||||
Transcoding can be done by sending the quality and container query parameters.
|
# Transcoding can be done by sending the quality and container query parameters.
|
||||||
|
|
||||||
**NOTES:**
|
# **NOTES:**
|
||||||
- Transcoded streams report incorrect duration during playback (idk why! FFMPEG gurus we need your help here).
|
# - Transcoded streams report incorrect duration during playback (idk why! FFMPEG gurus we need your help here).
|
||||||
- The quality parameter is the desired bitrate in kbps.
|
# - The quality parameter is the desired bitrate in kbps.
|
||||||
- The mp3 container is the best container for upto 320kbps (and has better duration reporting). The flac container allows for higher bitrates but it produces dramatically larger files (when transcoding from lossy formats).
|
# - The mp3 container is the best container for upto 320kbps (and has better duration reporting). The flac container allows for higher bitrates but it produces dramatically larger files (when transcoding from lossy formats).
|
||||||
- You can get the transcoded bitrate by checking the X-Transcoded-Bitrate header on the first request's response.
|
# - You can get the transcoded bitrate by checking the X-Transcoded-Bitrate header on the first request's response.
|
||||||
"""
|
# """
|
||||||
trackhash = path.trackhash
|
# trackhash = path.trackhash
|
||||||
filepath = query.filepath
|
# filepath = query.filepath
|
||||||
|
|
||||||
# If filepath is provided, try to send that
|
# # If filepath is provided, try to send that
|
||||||
track = None
|
# track = None
|
||||||
tracks = TrackStore.get_tracks_by_filepaths([filepath])
|
# tracks = TrackStore.get_tracks_by_filepaths([filepath])
|
||||||
|
|
||||||
if len(tracks) > 0 and os.path.exists(filepath):
|
# if len(tracks) > 0 and os.path.exists(filepath):
|
||||||
track = tracks[0]
|
# track = tracks[0]
|
||||||
else:
|
# else:
|
||||||
res = TrackStore.trackhashmap.get(trackhash)
|
# res = TrackStore.trackhashmap.get(trackhash)
|
||||||
|
|
||||||
# When finding by trackhash, sort by bitrate
|
# # When finding by trackhash, sort by bitrate
|
||||||
# and get the first track that exists
|
# # and get the first track that exists
|
||||||
if res is not None:
|
# if res is not None:
|
||||||
tracks = sorted(res.tracks, key=lambda x: x.bitrate, reverse=True)
|
# tracks = sorted(res.tracks, key=lambda x: x.bitrate, reverse=True)
|
||||||
|
|
||||||
for t in tracks:
|
# for t in tracks:
|
||||||
if os.path.exists(t.filepath):
|
# if os.path.exists(t.filepath):
|
||||||
track = t
|
# track = t
|
||||||
break
|
# break
|
||||||
|
|
||||||
if track is not None:
|
# if track is not None:
|
||||||
if query.quality == "original":
|
# if query.quality == "original":
|
||||||
return send_file_as_chunks(track.filepath)
|
# return send_file_as_chunks(track.filepath)
|
||||||
|
|
||||||
# prevent requesting over transcoding
|
# # prevent requesting over transcoding
|
||||||
max_bitrate = track.bitrate
|
# max_bitrate = track.bitrate
|
||||||
requested_bitrate = int(query.quality)
|
# requested_bitrate = int(query.quality)
|
||||||
|
|
||||||
if query.container != "flac":
|
# if query.container != "flac":
|
||||||
# drop to 320 for non-flac containers
|
# # drop to 320 for non-flac containers
|
||||||
requested_bitrate = min(320, requested_bitrate)
|
# requested_bitrate = min(320, requested_bitrate)
|
||||||
|
|
||||||
quality = f"{min(max_bitrate, requested_bitrate)}k"
|
# quality = f"{min(max_bitrate, requested_bitrate)}k"
|
||||||
return transcode_and_stream(trackhash, track.filepath, quality, query.container)
|
# return transcode_and_stream(trackhash, track.filepath, quality, query.container)
|
||||||
|
|
||||||
return {"msg": "File Not Found"}, 404
|
# return {"msg": "File Not Found"}, 404
|
||||||
|
|
||||||
|
|
||||||
def transcode_and_stream(trackhash: str, filepath: str, bitrate: str, container: str):
|
def transcode_and_stream(trackhash: str, filepath: str, bitrate: str, container: str):
|
||||||
|
|||||||
Reference in New Issue
Block a user