From 889352564725440c3fde888c0138f59e76e3f345 Mon Sep 17 00:00:00 2001 From: Jared Miller Date: Mon, 9 Feb 2026 16:43:09 -0500 Subject: [PATCH] Sanitize player names in IF save paths to prevent path traversal --- src/mudlib/if_session.py | 6 +++++- tests/test_if_session.py | 18 ++++++++++++++++++ 2 files changed, 23 insertions(+), 1 deletion(-) diff --git a/src/mudlib/if_session.py b/src/mudlib/if_session.py index 59c36c7..14dec5b 100644 --- a/src/mudlib/if_session.py +++ b/src/mudlib/if_session.py @@ -1,6 +1,7 @@ """Interactive fiction session management via dfrotz subprocess.""" import asyncio +import re from dataclasses import dataclass from pathlib import Path from typing import TYPE_CHECKING @@ -31,7 +32,10 @@ class IFSession: @property def save_path(self) -> Path: """Return path to save file for this player/game combo.""" - return self._data_dir / "if_saves" / self.player.name / f"{self.game_name}.qzl" + # Sanitize player name to prevent path traversal attacks + # Account creation doesn't validate names, so defensive check here + safe_name = re.sub(r'[^a-zA-Z0-9_-]', '_', self.player.name) + return self._data_dir / "if_saves" / safe_name / f"{self.game_name}.qzl" def _ensure_save_dir(self) -> None: """Create save directory if it doesn't exist.""" diff --git a/tests/test_if_session.py b/tests/test_if_session.py index 8e2904d..db27b9c 100644 --- a/tests/test_if_session.py +++ b/tests/test_if_session.py @@ -268,6 +268,24 @@ def test_save_path_property(tmp_path): assert save_path == tmp_path / "if_saves" / "tester" / "zork.qzl" +def test_save_path_sanitizes_malicious_names(tmp_path): + """save_path sanitizes player names to prevent path traversal.""" + player = MagicMock() + player.name = "../../etc/passwd" + session = IFSession(player, "/path/to/zork.z5", "zork") + + # Override data_dir for testing + session._data_dir = tmp_path + + save_path = session.save_path + # Should sanitize to replace non-alphanumeric chars with underscores + # "../../etc/passwd" becomes "______etc_passwd" + assert ".." not in str(save_path) + assert save_path == tmp_path / "if_saves" / "______etc_passwd" / "zork.qzl" + # Verify it's still within the if_saves directory + assert tmp_path / "if_saves" in save_path.parents + + def test_ensure_save_dir_creates_directories(tmp_path): """_ensure_save_dir() creates parent directories.""" player = MagicMock()