Feb 16, 2026·6 min read·5 visits
Authenticated users on Swing Music versions < 2.1.4 can traverse the entire server filesystem using the `/folder/dir-browser` endpoint. The patch enforces admin checks and strict path resolution.
Swing Music, a popular self-hosted music streaming server, contained a critical directory traversal vulnerability in its filesystem browser. This flaw allowed authenticated users—regardless of their privilege level—to break out of the music library sandbox and map the entire host filesystem. While not a direct Remote Code Execution (RCE), the ability to enumerate files and directories provides attackers with a high-definition map of the server, exposing sensitive configuration locations, user home directories, and system binaries.
Self-hosted applications are the playgrounds of the digital age. We spin them up in Docker containers, give them access to our NAS, and trust them to just play the music. Swing Music is one such application—a slick, modern web player that reads your local audio files and streams them to your browser. To do this, it naturally needs access to the filesystem. And that, my friends, is where the trouble usually begins.
In the world of web security, any feature that says "let me look at your files" is a loaded gun. If the developer doesn't meticulously check where the barrel is pointing, the user ends up shooting the server in the foot. In this case, the dir-browser functionality—intended to let users pick folders for their library—was a little too permissive.
It assumed that if a user was logged in, they were trustworthy. It also assumed that Python's pathlib would magically protect it from malicious input. As we'll see, neither assumption held water. This vulnerability is a classic case of "Implicit Trust" meeting "Naive Implementation," resulting in a directory traversal flaw that turns a music player into a file system explorer.
The vulnerability resides in the list_folders() function backing the /folder/dir-browser endpoint. The purpose of this endpoint is simple: the frontend sends a path, and the backend returns the folders inside it. This allows the UI to render a nice tree view for selecting music directories.
Here is the logic failure: The application accepted a path from the user's JSON payload and essentially said, "Sure, let's go there." There was a check, but it was practically cosmetic. The code checked if the directory existed, and if not, prepended a slash. It did not resolve symbolic links, nor did it canonicalize the path to remove traversal sequences like ../.
> [!NOTE] > The Golden Rule of Input Validation: > Never trust a path provided by a client until you have resolved it to its absolute form and verified it starts with a trusted root directory.
Swing Music failed both parts of this rule. It treated the input path as a suggestion rather than a potential attack vector. By supplying ../../, an attacker effectively tells the server: "I know you want to look in /music, but let's take two steps back and look at / instead." Because the application didn't enforce a "chroot" or sandbox logic, the operating system happily obliged.
Let's get our hands dirty with the code. The vulnerable implementation relied on Python's pathlib, which is generally excellent, but it doesn't prevent you from shooting yourself in the foot if you use it wrong.
The Vulnerable Logic:
# The naive implementation
req_dir = Path(data.get("folder"))
# If it doesn't exist relative to CWD, try absolute?
if not req_dir.exists():
req_dir = "/" / req_dir
# Proceed to list files...
return list_folders(req_dir)The issue here is the / operator in pathlib. If req_dir is ../../etc, the result is just ../../etc. It doesn't strip the dots. It doesn't lock you into a jail.
The Fix (Commit 9a915ca62...):
The developer, wanji, implemented a textbook defense-in-depth fix. They didn't just patch the path; they patched the permission model.
@admin_required(). Now, even if you can traverse directories, you need to be an admin (who arguably has access anyway)..resolve().# The hardened implementation
def is_path_within_root_dirs(filepath: str) -> bool:
config = UserConfig()
# .resolve() removes all symlinks and '../' segments
resolved_path = Path(filepath).resolve()
for root_dir in config.rootDirs:
root_path = Path(root_dir).resolve()
# Check if the requested path is a child of a trusted root
if resolved_path == root_path or root_path in resolved_path.parents:
return True
return FalseThis is the difference between a "blacklist" (trying to block ..) and a "whitelist" (only allowing specific parents). Whitelists always win.
Exploiting this requires a valid session token. In a real-world scenario, this might be a low-privileged user account (e.g., a family member or guest with access to the music server). Once we have the Bearer token, the attack is trivial.
The Setup: We intercept the request the browser makes when clicking "Add Folder".
The Attack: We modify the JSON body to traverse up from the application directory to the system root.
curl -X POST http://target-ip:1970/folder/dir-browser \
-H "Content-Type: application/json" \
-H "Authorization: Bearer <LOW_PRIV_TOKEN>" \
-d '{"folder": "../../../../../../"}'The Response:
Instead of an error or a music folder, the server returns the contents of /:
[
{
"name": "bin",
"path": "/bin",
"is_dir": true
},
{
"name": "etc",
"path": "/etc",
"is_dir": true
},
{
"name": "root",
"path": "/root",
"is_dir": true
}
]From here, an attacker can map out the entire filesystem structure. While this endpoint lists directories (and files), it doesn't strictly read file content. However, knowing that a file named backup_credentials.txt exists in /opt/backups is 90% of the battle in a lateral movement phase.
Why does this matter if I can't read /etc/shadow directly? Because information is ammunition. This vulnerability falls under CWE-25 (Path Traversal) and T1083 (File and Directory Discovery) in the MITRE ATT&CK framework.
docker-compose.yml in a user's home folder)./root/.ssh).While the CVSS score is a moderate 5.3, the contextual risk is higher in self-hosted "homelab" environments where segregation is often poor and containers run with excessive privileges.
If you are running Swing Music, update to version 2.1.4 or later immediately. The developer has patched this effectively by enforcing strict boundary checks.
If you cannot update for some reason (perhaps you enjoy living dangerously), you should ensure your Swing Music instance is not exposed to the public internet and that you trust every user you've given an account to. But really, just update.
For developers reading this: take note of the is_path_within_root_dirs logic. This is the correct way to handle filesystem access. Do not trust strings. Resolve them to objects, canonicalize them, and check their lineage against a list of approved ancestors.
CVSS:4.0/AV:N/AC:L/AT:N/PR:L/UI:N/VC:L/VI:N/VA:N/SC:N/SI:N/SA:N| Product | Affected Versions | Fixed Version |
|---|---|---|
Swing Music swingmx | < 2.1.4 | 2.1.4 |
| Attribute | Detail |
|---|---|
| CWE ID | CWE-25 (Path Traversal) |
| CVSS v4.0 | 5.3 (Medium) |
| Attack Vector | Network (Authenticated) |
| EPSS Score | 0.22% (Low) |
| Impact | Information Disclosure (File Enumeration) |
| Patch Status | Fixed in 2.1.4 |
The software uses external input to construct a pathname that is intended to identify a file or directory that is located underneath a restricted parent directory, but the software does not properly neutralize special elements within the pathname that can cause the pathname to resolve to a location that is outside of the restricted directory.