Skip to content

Commit d7e2710

Browse files
committed
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
1 parent 5d200ff commit d7e2710

File tree

1 file changed

+75
-51
lines changed

1 file changed

+75
-51
lines changed

src/swingmusic/api/stream.py

Lines changed: 75 additions & 51 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111
from pydantic import BaseModel, Field
1212
from flask_openapi3 import APIBlueprint, Tag
1313
from swingmusic.api.apischemas import TrackHashSchema
14+
from swingmusic.config import UserConfig
1415
from swingmusic.lib.transcoder import start_transcoding
1516
from flask import request, Response, send_from_directory
1617
from swingmusic.lib.trackslib import get_silence_paddings
@@ -59,33 +60,56 @@ def send_track_file_legacy(path: TrackHashSchema, query: SendTrackFileQuery):
5960
6061
NOTE: Does not support range requests or transcoding.
6162
"""
62-
trackhash = path.trackhash
63-
filepath = query.filepath
63+
requested_trackhash = path.trackhash.strip()
64+
filepath = query.filepath.strip()
65+
6466
msg = {"msg": "File Not Found"}
6567

68+
# prevent path traversal
69+
if "/../" in filepath:
70+
return {"msg": "Invalid filepath", "error": "Path traversal detected"}, 400
71+
72+
requested_filepath = Path(filepath).resolve()
73+
74+
# check if filepath is a child of any of the root dirs
75+
for root_dir in UserConfig().rootDirs:
76+
if root_dir == "$home":
77+
root_dir = Path.home()
78+
else:
79+
root_dir = Path(root_dir).resolve()
80+
81+
if root_dir not in requested_filepath.parents:
82+
return {
83+
"msg": "Invalid filepath",
84+
"error": "File not inside root directories",
85+
}, 400
86+
6687
track = None
6788
tracks = TrackStore.get_tracks_by_filepaths([filepath])
6889

69-
if len(tracks) > 0 and os.path.exists(filepath):
70-
track = tracks[0]
90+
if len(tracks) > 0 and os.path.exists(tracks[0].filepath):
91+
for t in tracks:
92+
if os.path.exists(t.filepath) and t.trackhash == requested_trackhash:
93+
track = t
94+
break
7195
else:
72-
res = TrackStore.trackhashmap.get(trackhash)
96+
group = TrackStore.trackhashmap.get(requested_trackhash)
7397

7498
# When finding by trackhash, sort by bitrate
7599
# and get the first track that exists
76-
if res is not None:
77-
tracks = sorted(res.tracks, key=lambda x: x.bitrate, reverse=True)
100+
if group is not None:
101+
tracks = sorted(group.tracks, key=lambda x: x.bitrate, reverse=True)
78102

79103
for t in tracks:
80104
if os.path.exists(t.filepath):
81105
track = t
82106
break
83107

84108
if track is not None:
85-
audio_type = guess_mime_type(filepath)
109+
audio_type = guess_mime_type(track.filepath)
86110
return send_from_directory(
87-
Path(filepath).parent,
88-
Path(filepath).name,
111+
Path(track.filepath).parent,
112+
Path(track.filepath).name,
89113
mimetype=audio_type,
90114
conditional=True,
91115
as_attachment=True,
@@ -94,59 +118,59 @@ def send_track_file_legacy(path: TrackHashSchema, query: SendTrackFileQuery):
94118
return msg, 404
95119

96120

97-
@api.get("/<trackhash>")
98-
def send_track_file(path: TrackHashSchema, query: SendTrackFileQuery):
99-
"""
100-
Get a playable audio file with Range headers support
121+
# @api.get("/<trackhash>")
122+
# def send_track_file(path: TrackHashSchema, query: SendTrackFileQuery):
123+
# """
124+
# Get a playable audio file with Range headers support
101125

102-
Returns a playable audio file that corresponds to the given filepath. Falls back to track hash if filepath is not found.
126+
# Returns a playable audio file that corresponds to the given filepath. Falls back to track hash if filepath is not found.
103127

104-
Transcoding can be done by sending the quality and container query parameters.
128+
# Transcoding can be done by sending the quality and container query parameters.
105129

106-
**NOTES:**
107-
- Transcoded streams report incorrect duration during playback (idk why! FFMPEG gurus we need your help here).
108-
- The quality parameter is the desired bitrate in kbps.
109-
- 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).
110-
- You can get the transcoded bitrate by checking the X-Transcoded-Bitrate header on the first request's response.
111-
"""
112-
trackhash = path.trackhash
113-
filepath = query.filepath
130+
# **NOTES:**
131+
# - Transcoded streams report incorrect duration during playback (idk why! FFMPEG gurus we need your help here).
132+
# - The quality parameter is the desired bitrate in kbps.
133+
# - 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).
134+
# - You can get the transcoded bitrate by checking the X-Transcoded-Bitrate header on the first request's response.
135+
# """
136+
# trackhash = path.trackhash
137+
# filepath = query.filepath
114138

115-
# If filepath is provided, try to send that
116-
track = None
117-
tracks = TrackStore.get_tracks_by_filepaths([filepath])
139+
# # If filepath is provided, try to send that
140+
# track = None
141+
# tracks = TrackStore.get_tracks_by_filepaths([filepath])
118142

119-
if len(tracks) > 0 and os.path.exists(filepath):
120-
track = tracks[0]
121-
else:
122-
res = TrackStore.trackhashmap.get(trackhash)
143+
# if len(tracks) > 0 and os.path.exists(filepath):
144+
# track = tracks[0]
145+
# else:
146+
# res = TrackStore.trackhashmap.get(trackhash)
123147

124-
# When finding by trackhash, sort by bitrate
125-
# and get the first track that exists
126-
if res is not None:
127-
tracks = sorted(res.tracks, key=lambda x: x.bitrate, reverse=True)
148+
# # When finding by trackhash, sort by bitrate
149+
# # and get the first track that exists
150+
# if res is not None:
151+
# tracks = sorted(res.tracks, key=lambda x: x.bitrate, reverse=True)
128152

129-
for t in tracks:
130-
if os.path.exists(t.filepath):
131-
track = t
132-
break
153+
# for t in tracks:
154+
# if os.path.exists(t.filepath):
155+
# track = t
156+
# break
133157

134-
if track is not None:
135-
if query.quality == "original":
136-
return send_file_as_chunks(track.filepath)
158+
# if track is not None:
159+
# if query.quality == "original":
160+
# return send_file_as_chunks(track.filepath)
137161

138-
# prevent requesting over transcoding
139-
max_bitrate = track.bitrate
140-
requested_bitrate = int(query.quality)
162+
# # prevent requesting over transcoding
163+
# max_bitrate = track.bitrate
164+
# requested_bitrate = int(query.quality)
141165

142-
if query.container != "flac":
143-
# drop to 320 for non-flac containers
144-
requested_bitrate = min(320, requested_bitrate)
166+
# if query.container != "flac":
167+
# # drop to 320 for non-flac containers
168+
# requested_bitrate = min(320, requested_bitrate)
145169

146-
quality = f"{min(max_bitrate, requested_bitrate)}k"
147-
return transcode_and_stream(trackhash, track.filepath, quality, query.container)
170+
# quality = f"{min(max_bitrate, requested_bitrate)}k"
171+
# return transcode_and_stream(trackhash, track.filepath, quality, query.container)
148172

149-
return {"msg": "File Not Found"}, 404
173+
# return {"msg": "File Not Found"}, 404
150174

151175

152176
def transcode_and_stream(trackhash: str, filepath: str, bitrate: str, container: str):

0 commit comments

Comments
 (0)