Skip to content

fix: reject absolute paths in directory env vars#148

Merged
CyberSecDef merged 2 commits into
mainfrom
copilot/fix-environment-variable-paths
Apr 8, 2026
Merged

fix: reject absolute paths in directory env vars#148
CyberSecDef merged 2 commits into
mainfrom
copilot/fix-environment-variable-paths

Conversation

Copy link
Copy Markdown
Contributor

Copilot AI commented Apr 8, 2026

Path.__truediv__ silently discards the left operand when the right side is absolute (Path("/app") / "/etc/passwd"/etc/passwd), allowing NOVELS_DIR, EXPORT_DIR, LOGS_DIR, and SESSION_FILE_DIR to be redirected to arbitrary filesystem locations.

Changes

  • novelforge/config.py: Added _resolve_dir(env_var, default) helper that calls os.path.isabs() on the raw env var value and raises ValueError before any path join occurs. All four directory variables now use this helper instead of the inline PROJECT_ROOT / os.environ.get(...) pattern.
def _resolve_dir(env_var: str, default: str) -> str:
    raw = os.environ.get(env_var, default)
    if os.path.isabs(raw):
        raise ValueError(
            f"{env_var} must be a relative path (got {raw!r}). "
            "Directory env vars are resolved relative to the project root."
        )
    return str(PROJECT_ROOT / raw)
  • tests/test_validate_config.py: Added TestResolveDir covering relative default, relative override, absolute path rejection (/etc/passwd, /), error message content, and return type.

Warning

Firewall rules blocked me from connecting to one or more addresses (expand for details)

I tried to connect to the following addresses, but was blocked by firewall rules:

  • api.openai.com
    • Triggering command: /usr/bin/python python -m pytest --tb=short -q (dns block)
    • Triggering command: /usr/bin/python python -m pytest --tb=line -q (dns block)
    • Triggering command: /usr/bin/python python -m pytest (dns block)

If you need me to access, download, or install something from one of these locations, you can either:

Copilot AI requested review from Copilot and removed request for Copilot April 8, 2026 16:10
Copilot AI linked an issue Apr 8, 2026 that may be closed by this pull request
…XPORT_DIR, NOVELS_DIR, LOGS_DIR)

Agent-Logs-Url: https://github.com/CyberSecDef/NovelForge/sessions/f34e38e7-05d1-4794-beb6-ad4b72be76ae

Co-authored-by: CyberSecDef <17597068+CyberSecDef@users.noreply.github.com>
Copilot AI requested review from Copilot and removed request for Copilot April 8, 2026 16:17
Copilot AI changed the title [WIP] Fix environment variable paths to disallow absolute paths fix: reject absolute paths in directory env vars Apr 8, 2026
Copilot AI requested a review from CyberSecDef April 8, 2026 16:19
@CyberSecDef CyberSecDef marked this pull request as ready for review April 8, 2026 16:21
Copilot AI review requested due to automatic review settings April 8, 2026 16:21
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR hardens NovelForge’s configuration by preventing directory-related environment variables from being set to absolute paths, avoiding Path join behavior that can redirect storage/log/session directories outside the project root.

Changes:

  • Added a _resolve_dir(env_var, default) helper in novelforge/config.py to reject absolute paths and resolve directory env vars relative to PROJECT_ROOT.
  • Updated SESSION_FILE_DIR, EXPORT_DIR, NOVELS_DIR, and LOGS_DIR to use _resolve_dir.
  • Added unit tests for _resolve_dir behavior in tests/test_validate_config.py.

Reviewed changes

Copilot reviewed 2 out of 2 changed files in this pull request and generated 3 comments.

File Description
novelforge/config.py Introduces _resolve_dir and routes directory configuration through it to prevent absolute-path env var overrides.
tests/test_validate_config.py Adds unit tests validating _resolve_dir relative resolution and absolute-path rejection.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread novelforge/config.py
Comment on lines +236 to +250

def _resolve_dir(env_var: str, default: str) -> str:
"""Return the absolute path for a directory env var, anchored to PROJECT_ROOT.

Raises:
ValueError: if the env var is set to an absolute path. All directory
env vars must be relative to *PROJECT_ROOT* to prevent arbitrary
filesystem access.
"""
raw = os.environ.get(env_var, default)
if os.path.isabs(raw):
raise ValueError(
f"{env_var} must be a relative path (got {raw!r}). "
"Directory env vars are resolved relative to the project root."
)
Copy link

Copilot AI Apr 8, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Raising ValueError here happens at import time (these constants are set during module import). Since novelforge/init.py only catches ConfigurationError around validate_config(), an absolute dir env var will crash app startup with an uncaught exception and bypass the structured validation/logging path. Consider recording this as a config-parse error (similar to get_env_int) and surfacing it from validate_config() as ConfigurationError, or otherwise ensuring create_app/startup handles it cleanly.

Suggested change
def _resolve_dir(env_var: str, default: str) -> str:
"""Return the absolute path for a directory env var, anchored to PROJECT_ROOT.
Raises:
ValueError: if the env var is set to an absolute path. All directory
env vars must be relative to *PROJECT_ROOT* to prevent arbitrary
filesystem access.
"""
raw = os.environ.get(env_var, default)
if os.path.isabs(raw):
raise ValueError(
f"{env_var} must be a relative path (got {raw!r}). "
"Directory env vars are resolved relative to the project root."
)
# Configuration parse problems detected during module import. Callers can
# surface these later via the normal validation/startup path.
_CONFIG_PARSE_ERRORS: list[str] = []
def _resolve_dir(env_var: str, default: str) -> str:
"""Return the absolute path for a directory env var, anchored to PROJECT_ROOT.
Absolute paths are rejected because directory env vars must stay relative
to *PROJECT_ROOT*. To avoid crashing at import time, invalid values are
recorded and logged, and the safe default is used instead.
"""
raw = os.environ.get(env_var, default)
if os.path.isabs(raw):
message = (
f"{env_var} must be a relative path (got {raw!r}). "
"Directory env vars are resolved relative to the project root."
)
_CONFIG_PARSE_ERRORS.append(message)
logging.getLogger(__name__).error(message)
raw = default

Copilot uses AI. Check for mistakes.
Comment thread novelforge/config.py
f"{env_var} must be a relative path (got {raw!r}). "
"Directory env vars are resolved relative to the project root."
)
return str(PROJECT_ROOT / raw)
Copy link

Copilot AI Apr 8, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This still allows path traversal via relative values like "../.." (e.g., PROJECT_ROOT / "../etc"), which can escape PROJECT_ROOT while remaining non-absolute. If the goal is to prevent redirecting these dirs to arbitrary filesystem locations, normalize the joined path (e.g., resolve(strict=False)) and verify it stays within PROJECT_ROOT (Path.is_relative_to), rejecting anything that escapes.

Copilot uses AI. Check for mistakes.
monkeypatch.setenv("NF_TEST_DIR", "/etc/passwd")
with pytest.raises(ValueError, match="NF_TEST_DIR"):
cfg._resolve_dir("NF_TEST_DIR", "default")

Copy link

Copilot AI Apr 8, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Tests cover absolute-path rejection, but they don’t cover traversal/escape cases such as "../outside" (which is relative but can resolve outside PROJECT_ROOT). Adding a regression test for that case would lock in the intended security property if _resolve_dir is tightened to enforce PROJECT_ROOT containment.

Suggested change
def test_raises_for_relative_path_traversal_outside_project_root(self, monkeypatch):
monkeypatch.setenv("NF_TEST_DIR", "../outside")
with pytest.raises(ValueError, match="NF_TEST_DIR"):
cfg._resolve_dir("NF_TEST_DIR", "default")

Copilot uses AI. Check for mistakes.
@CyberSecDef CyberSecDef merged commit 0e493b0 into main Apr 8, 2026
12 checks passed
@CyberSecDef CyberSecDef deleted the copilot/fix-environment-variable-paths branch April 8, 2026 18:29
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Environment Variable Paths Accept Absolute Paths

3 participants