Skip to content

Commit 0e493b0

Browse files
authored
Merge pull request #148 from CyberSecDef/copilot/fix-environment-variable-paths
fix: reject absolute paths in directory env vars
2 parents 33a438c + 9a44598 commit 0e493b0

2 files changed

Lines changed: 66 additions & 4 deletions

File tree

novelforge/config.py

Lines changed: 22 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -233,17 +233,35 @@ def _parse_llm_providers() -> list[ProviderConfig]:
233233
# Flask secret key – override via SECRET_KEY environment variable in production
234234
SECRET_KEY = os.environ.get("SECRET_KEY", "change-me-in-production")
235235

236+
237+
def _resolve_dir(env_var: str, default: str) -> str:
238+
"""Return the absolute path for a directory env var, anchored to PROJECT_ROOT.
239+
240+
Raises:
241+
ValueError: if the env var is set to an absolute path. All directory
242+
env vars must be relative to *PROJECT_ROOT* to prevent arbitrary
243+
filesystem access.
244+
"""
245+
raw = os.environ.get(env_var, default)
246+
if os.path.isabs(raw):
247+
raise ValueError(
248+
f"{env_var} must be a relative path (got {raw!r}). "
249+
"Directory env vars are resolved relative to the project root."
250+
)
251+
return str(PROJECT_ROOT / raw)
252+
253+
236254
# Directory where Flask-Session stores server-side session files
237-
SESSION_FILE_DIR = str(PROJECT_ROOT / os.environ.get("SESSION_FILE_DIR", "sessions/flask"))
255+
SESSION_FILE_DIR = _resolve_dir("SESSION_FILE_DIR", "sessions/flask")
238256

239257
# Directory where exported novel files are stored temporarily
240-
EXPORT_DIR = str(PROJECT_ROOT / os.environ.get("EXPORT_DIR", "exports"))
258+
EXPORT_DIR = _resolve_dir("EXPORT_DIR", "exports")
241259

242260
# Directory where novel session JSON files are stored
243-
NOVELS_DIR = str(PROJECT_ROOT / os.environ.get("NOVELS_DIR", "sessions/novels"))
261+
NOVELS_DIR = _resolve_dir("NOVELS_DIR", "sessions/novels")
244262

245263
# Directory for log files
246-
LOGS_DIR = str(PROJECT_ROOT / os.environ.get("LOGS_DIR", "logs"))
264+
LOGS_DIR = _resolve_dir("LOGS_DIR", "logs")
247265

248266

249267
class ConfigurationError(Exception):

tests/test_validate_config.py

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,50 @@
44
import novelforge.config as cfg
55

66

7+
# ---------------------------------------------------------------------------
8+
# _resolve_dir helper
9+
# ---------------------------------------------------------------------------
10+
11+
class TestResolveDir:
12+
"""Unit tests for the _resolve_dir() helper."""
13+
14+
def test_returns_project_root_relative_path_when_unset(self, monkeypatch):
15+
monkeypatch.delenv("NF_TEST_DIR", raising=False)
16+
result = cfg._resolve_dir("NF_TEST_DIR", "my/subdir")
17+
assert result == str(cfg.PROJECT_ROOT / "my/subdir")
18+
19+
def test_returns_project_root_relative_path_for_relative_env_var(self, monkeypatch):
20+
monkeypatch.setenv("NF_TEST_DIR", "custom/path")
21+
result = cfg._resolve_dir("NF_TEST_DIR", "default/path")
22+
assert result == str(cfg.PROJECT_ROOT / "custom/path")
23+
24+
def test_raises_for_absolute_path(self, monkeypatch):
25+
monkeypatch.setenv("NF_TEST_DIR", "/etc/passwd")
26+
with pytest.raises(ValueError, match="NF_TEST_DIR"):
27+
cfg._resolve_dir("NF_TEST_DIR", "default")
28+
29+
def test_error_message_contains_bad_value(self, monkeypatch):
30+
monkeypatch.setenv("NF_TEST_DIR", "/absolute/path")
31+
with pytest.raises(ValueError, match="/absolute/path"):
32+
cfg._resolve_dir("NF_TEST_DIR", "default")
33+
34+
def test_raises_for_root_path(self, monkeypatch):
35+
monkeypatch.setenv("NF_TEST_DIR", "/")
36+
with pytest.raises(ValueError):
37+
cfg._resolve_dir("NF_TEST_DIR", "default")
38+
39+
def test_relative_default_accepted_when_env_var_unset(self, monkeypatch):
40+
monkeypatch.delenv("NF_TEST_DIR", raising=False)
41+
# Should not raise; default is relative
42+
result = cfg._resolve_dir("NF_TEST_DIR", "logs")
43+
assert result.endswith("logs")
44+
45+
def test_result_is_string(self, monkeypatch):
46+
monkeypatch.delenv("NF_TEST_DIR", raising=False)
47+
result = cfg._resolve_dir("NF_TEST_DIR", "data")
48+
assert isinstance(result, str)
49+
50+
751
# ---------------------------------------------------------------------------
852
# get_env_int helper
953
# ---------------------------------------------------------------------------

0 commit comments

Comments
 (0)