mirror of
https://github.com/Dvorinka/swingmusic-extended.git
synced 2026-06-03 20:13:02 +00:00
fix: directory traversal as reported by @d-virtuosa
This commit is contained in:
@@ -1,30 +1,53 @@
|
|||||||
"""
|
"""
|
||||||
Contains all the folder routes.
|
Contains all the folder routes.
|
||||||
"""
|
"""
|
||||||
import pathlib
|
|
||||||
from datetime import datetime
|
|
||||||
import os
|
import os
|
||||||
|
import pathlib
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
from datetime import datetime
|
||||||
|
|
||||||
import psutil
|
import psutil
|
||||||
from pydantic import BaseModel, Field
|
|
||||||
from flask_openapi3 import Tag
|
from flask_openapi3 import Tag
|
||||||
|
from pydantic import BaseModel, Field
|
||||||
from flask_openapi3 import APIBlueprint
|
from flask_openapi3 import APIBlueprint
|
||||||
from showinfm import show_in_file_manager
|
from showinfm import show_in_file_manager
|
||||||
|
|
||||||
from swingmusic import settings
|
from swingmusic import settings
|
||||||
from swingmusic.config import UserConfig
|
from swingmusic.config import UserConfig
|
||||||
from swingmusic.db.libdata import TrackTable
|
from swingmusic.db.libdata import TrackTable
|
||||||
|
from swingmusic.api.auth import admin_required
|
||||||
|
from swingmusic.store.tracks import TrackStore
|
||||||
|
from swingmusic.utils.wintools import is_windows
|
||||||
from swingmusic.db.userdata import FavoritesTable, PlaylistTable
|
from swingmusic.db.userdata import FavoritesTable, PlaylistTable
|
||||||
from swingmusic.lib.folderslib import get_files_and_dirs, get_folders
|
from swingmusic.lib.folderslib import get_files_and_dirs, get_folders
|
||||||
from swingmusic.serializers.track import serialize_track, serialize_tracks
|
from swingmusic.serializers.track import serialize_track, serialize_tracks
|
||||||
from swingmusic.store.tracks import TrackStore
|
|
||||||
from swingmusic.utils.wintools import is_windows
|
|
||||||
|
|
||||||
tag = Tag(name="Folders", description="Get folders and tracks in a directory")
|
tag = Tag(name="Folders", description="Get folders and tracks in a directory")
|
||||||
api = APIBlueprint("folder", __name__, url_prefix="/folder", abp_tags=[tag])
|
api = APIBlueprint("folder", __name__, url_prefix="/folder", abp_tags=[tag])
|
||||||
|
|
||||||
|
|
||||||
|
def is_path_within_root_dirs(filepath: str) -> bool:
|
||||||
|
"""
|
||||||
|
Check if a filepath is within one of the configured root directories.
|
||||||
|
Prevents directory traversal attacks.
|
||||||
|
"""
|
||||||
|
config = UserConfig()
|
||||||
|
resolved_path = Path(filepath).resolve()
|
||||||
|
|
||||||
|
for root_dir in config.rootDirs:
|
||||||
|
if root_dir == "$home":
|
||||||
|
root_path = Path.home().resolve()
|
||||||
|
else:
|
||||||
|
root_path = Path(root_dir).resolve()
|
||||||
|
|
||||||
|
# Check if resolved_path is the root or a child of root
|
||||||
|
if resolved_path == root_path or root_path in resolved_path.parents:
|
||||||
|
return True
|
||||||
|
|
||||||
|
return False
|
||||||
|
|
||||||
|
|
||||||
class FolderTree(BaseModel):
|
class FolderTree(BaseModel):
|
||||||
folder: str = Field("$home", description="The folder to things from")
|
folder: str = Field("$home", description="The folder to things from")
|
||||||
sorttracksby: str = Field(
|
sorttracksby: str = Field(
|
||||||
@@ -142,14 +165,26 @@ def get_folder_tree(body: FolderTree):
|
|||||||
"path": req_dir,
|
"path": req_dir,
|
||||||
}
|
}
|
||||||
|
|
||||||
# TODO: currently only fixed on unix. Windows/Mac still pending.
|
# Resolve path to prevent directory traversal attacks
|
||||||
# note
|
resolved_path = pathlib.Path(req_dir).resolve()
|
||||||
|
|
||||||
if not pathlib.Path(req_dir).exists():
|
# Validate path is within configured root directories
|
||||||
req_dir = "/" + req_dir
|
if not is_path_within_root_dirs(str(resolved_path)):
|
||||||
|
return {
|
||||||
|
"folders": [],
|
||||||
|
"tracks": [],
|
||||||
|
"error": "Path not within allowed directories",
|
||||||
|
}, 403
|
||||||
|
|
||||||
|
if not resolved_path.exists() or not resolved_path.is_dir():
|
||||||
|
return {
|
||||||
|
"folders": [],
|
||||||
|
"tracks": [],
|
||||||
|
"error": "Invalid directory",
|
||||||
|
}, 400
|
||||||
|
|
||||||
results = get_files_and_dirs(
|
results = get_files_and_dirs(
|
||||||
pathlib.Path(req_dir),
|
resolved_path,
|
||||||
start=body.start,
|
start=body.start,
|
||||||
limit=body.limit,
|
limit=body.limit,
|
||||||
tracks_only=tracks_only,
|
tracks_only=tracks_only,
|
||||||
@@ -213,12 +248,13 @@ class DirBrowserBody(BaseModel):
|
|||||||
|
|
||||||
|
|
||||||
@api.post("/dir-browser")
|
@api.post("/dir-browser")
|
||||||
|
@admin_required()
|
||||||
def list_folders(body: DirBrowserBody):
|
def list_folders(body: DirBrowserBody):
|
||||||
"""
|
"""
|
||||||
List folders
|
List folders
|
||||||
|
|
||||||
Returns a list of all the folders in the given folder.
|
Returns a list of all the folders in the given folder.
|
||||||
Used when selecting root dirs.
|
Used when selecting root dirs. Admin only.
|
||||||
"""
|
"""
|
||||||
req_dir = body.folder
|
req_dir = body.folder
|
||||||
is_win = is_windows()
|
is_win = is_windows()
|
||||||
@@ -228,11 +264,11 @@ def list_folders(body: DirBrowserBody):
|
|||||||
"folders": [{"name": d, "path": d} for d in get_all_drives(is_win=is_win)]
|
"folders": [{"name": d, "path": d} for d in get_all_drives(is_win=is_win)]
|
||||||
}
|
}
|
||||||
|
|
||||||
|
# Resolve path to prevent directory traversal attacks
|
||||||
|
req_dir = pathlib.Path(req_dir).resolve()
|
||||||
|
|
||||||
req_dir = pathlib.Path(req_dir)
|
if not req_dir.exists() or not req_dir.is_dir():
|
||||||
|
return {"folders": [], "error": "Invalid directory"}, 400
|
||||||
if not req_dir.exists():
|
|
||||||
req_dir = "/" / req_dir
|
|
||||||
|
|
||||||
try:
|
try:
|
||||||
entries = os.scandir(req_dir)
|
entries = os.scandir(req_dir)
|
||||||
@@ -245,17 +281,14 @@ def list_folders(body: DirBrowserBody):
|
|||||||
entry = pathlib.Path(entry)
|
entry = pathlib.Path(entry)
|
||||||
name = entry.name
|
name = entry.name
|
||||||
|
|
||||||
if name.startswith("$"): # ignore windows system folder
|
if name.startswith("$"):
|
||||||
continue
|
continue
|
||||||
|
|
||||||
if name.startswith("."): # ignore unix hidden folder
|
if name.startswith("."):
|
||||||
continue
|
continue
|
||||||
|
|
||||||
if entry.is_dir(): # lastly, check if is dir
|
if entry.is_dir():
|
||||||
dirs.append({
|
dirs.append({"name": name, "path": entry.resolve().as_posix()})
|
||||||
"name": name,
|
|
||||||
"path": entry.as_posix()
|
|
||||||
})
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
"folders": sorted(dirs, key=lambda i: i["name"]),
|
"folders": sorted(dirs, key=lambda i: i["name"]),
|
||||||
@@ -274,8 +307,19 @@ def open_in_file_manager(query: FolderOpenInFileManagerQuery):
|
|||||||
Open in file manager
|
Open in file manager
|
||||||
|
|
||||||
Opens the given path in the file manager on the host machine.
|
Opens the given path in the file manager on the host machine.
|
||||||
|
Path must be within configured root directories.
|
||||||
"""
|
"""
|
||||||
show_in_file_manager(query.path)
|
# Resolve path to prevent directory traversal
|
||||||
|
resolved_path = Path(query.path).resolve()
|
||||||
|
|
||||||
|
# Validate path is within root directories
|
||||||
|
if not is_path_within_root_dirs(query.path):
|
||||||
|
return {"success": False, "error": "Path not within allowed directories"}, 403
|
||||||
|
|
||||||
|
if not resolved_path.exists():
|
||||||
|
return {"success": False, "error": "Path does not exist"}, 404
|
||||||
|
|
||||||
|
show_in_file_manager(str(resolved_path))
|
||||||
|
|
||||||
return {"success": True}
|
return {"success": True}
|
||||||
|
|
||||||
@@ -295,7 +339,14 @@ def get_tracks_in_path(query: GetTracksInPathQuery):
|
|||||||
|
|
||||||
Used when adding tracks to the queue.
|
Used when adding tracks to the queue.
|
||||||
"""
|
"""
|
||||||
tracks = TrackTable.get_tracks_in_path(query.path)
|
# Resolve path to prevent directory traversal
|
||||||
|
resolved_path = Path(query.path).resolve()
|
||||||
|
|
||||||
|
# Validate path is within root directories
|
||||||
|
if not is_path_within_root_dirs(str(resolved_path)):
|
||||||
|
return {"tracks": [], "error": "Path not within allowed directories"}, 403
|
||||||
|
|
||||||
|
tracks = TrackTable.get_tracks_in_path(str(resolved_path))
|
||||||
tracks = (serialize_track(t) for t in tracks if Path(t.filepath).exists())
|
tracks = (serialize_track(t) for t in tracks if Path(t.filepath).exists())
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
|||||||
Reference in New Issue
Block a user