/
/
/
1"""Tests that the filesystem provider rejects path-traversal outside its base path."""
2
3import os
4from pathlib import Path
5from unittest.mock import MagicMock
6
7import pytest
8from music_assistant_models.errors import MediaNotFoundError
9
10from music_assistant.providers.filesystem_local import LocalFileSystemProvider, helpers
11
12INSTANCE_ID = "filesystem_local--test"
13
14
15def _make_provider(base_path: str) -> LocalFileSystemProvider:
16 provider = LocalFileSystemProvider.__new__(LocalFileSystemProvider)
17 provider.base_path = base_path
18 provider.logger = MagicMock()
19 provider.write_access = False
20 provider.media_content_type = "music"
21 provider.config = MagicMock()
22 provider.config.instance_id = INSTANCE_ID
23 provider.manifest = MagicMock()
24 provider.manifest.domain = "filesystem_local"
25 return provider
26
27
28@pytest.fixture
29def music_tree(tmp_path: Path) -> Path:
30 """Create a base dir with one in-base track and a secret file outside it."""
31 base = tmp_path / "music"
32 base.mkdir()
33 (base / "track.mp3").write_bytes(b"\x00" * 128)
34 (tmp_path / "secret.mp3").write_bytes(b"\x00" * 128)
35 return base
36
37
38def test_get_absolute_path_rejects_traversal(music_tree: Path) -> None:
39 """A `../`-escaping relative path is rejected by the helper."""
40 with pytest.raises(MediaNotFoundError):
41 helpers.get_absolute_path(str(music_tree), "../secret.mp3")
42
43
44def test_get_absolute_path_rejects_absolute_outside_base(music_tree: Path) -> None:
45 """An absolute path outside the base is rejected by the helper."""
46 outside = str(music_tree.parent / "secret.mp3")
47 with pytest.raises(MediaNotFoundError):
48 helpers.get_absolute_path(str(music_tree), outside)
49
50
51def test_get_absolute_path_allows_in_base(music_tree: Path) -> None:
52 """A legitimate in-base path still resolves."""
53 result = helpers.get_absolute_path(str(music_tree), "track.mp3")
54 assert result == str(music_tree / "track.mp3")
55
56
57def test_get_absolute_path_allows_absolute_in_base(music_tree: Path) -> None:
58 """An already-absolute in-base path still resolves (used by internal scans)."""
59 abs_in_base = str(music_tree / "track.mp3")
60 assert helpers.get_absolute_path(str(music_tree), abs_in_base) == abs_in_base
61
62
63async def test_resolve_rejects_traversal(music_tree: Path) -> None:
64 """provider.resolve() (used by get_track/preview) rejects `../` escapes."""
65 provider = _make_provider(str(music_tree))
66 with pytest.raises(MediaNotFoundError):
67 await provider.resolve("../secret.mp3")
68
69
70async def test_exists_rejects_traversal(music_tree: Path) -> None:
71 """provider.exists() returns False for a `../`-escaping path instead of leaking it."""
72 provider = _make_provider(str(music_tree))
73 assert await provider.exists("../secret.mp3") is False
74
75
76async def test_scandir_rejects_traversal(music_tree: Path) -> None:
77 """provider._scandir() (used by browse) rejects listing a dir outside the base."""
78 provider = _make_provider(str(music_tree))
79 with pytest.raises(MediaNotFoundError):
80 await provider._scandir("..")
81
82
83async def test_resolve_allows_in_base(music_tree: Path) -> None:
84 """A legitimate in-base file still resolves through provider.resolve()."""
85 provider = _make_provider(str(music_tree))
86 file_item = await provider.resolve("track.mp3")
87 assert file_item.absolute_path == os.path.join(str(music_tree), "track.mp3")
88
89
90def test_get_absolute_path_allows_symlink_inside_base(music_tree: Path) -> None:
91 """A symlink inside the base still resolves (check is lexical, symlinks are admin-created)."""
92 link = music_tree / "linked.mp3"
93 link.symlink_to(music_tree.parent / "secret.mp3")
94 assert helpers.get_absolute_path(str(music_tree), "linked.mp3") == str(link)
95