Skip to content

Security: Path traversal/arbitrary file write risk in replay cache key handling#999

Open
tomaioo wants to merge 1 commit into
Lumiwealth:devfrom
tomaioo:fix/security/path-traversal-arbitrary-file-write-risk
Open

Security: Path traversal/arbitrary file write risk in replay cache key handling#999
tomaioo wants to merge 1 commit into
Lumiwealth:devfrom
tomaioo:fix/security/path-traversal-arbitrary-file-write-risk

Conversation

@tomaioo

@tomaioo tomaioo commented Apr 25, 2026

Copy link
Copy Markdown

Summary

Security: Path traversal/arbitrary file write risk in replay cache key handling

Problem

Severity: High | File: lumibot/components/agents/replay_cache.py:L58

_path_for builds file paths directly from key (self.root / key[:2] / f"{key}.json.gz") without validating allowed characters. If load()/save() are called with attacker-controlled keys, .. or path separators can escape the cache directory, enabling unintended file read/write locations and potential overwrite of arbitrary files.

Solution

Validate key strictly (e.g., ^[a-f0-9]{64}$ for SHA-256 hex), reject any path separators/dots, and enforce a post-resolution check (resolved_path.is_relative_to(self.root.resolve())) before any file operation.

Changes

  • lumibot/components/agents/replay_cache.py (modified)

Summary by CodeRabbit

  • Bug Fixes
    • Enhanced replay cache validation to reject malformed cache keys and prevent unauthorized path access, improving system security and stability.

`_path_for` builds file paths directly from `key` (`self.root / key[:2] / f"{key}.json.gz"`) without validating allowed characters. If `load()`/`save()` are called with attacker-controlled keys, `..` or path separators can escape the cache directory, enabling unintended file read/write locations and potential overwrite of arbitrary files.

Signed-off-by: tomaioo <203048277+tomaioo@users.noreply.github.com>
@tomaioo tomaioo requested a review from grzesir as a code owner April 25, 2026 06:13
@coderabbitai

coderabbitai Bot commented Apr 25, 2026

Copy link
Copy Markdown
📝 Walkthrough

Walkthrough

Replay cache key handling now includes validation to ensure keys match a 64-character lowercase hex format and that resolved file paths remain contained within the cache directory. Invalid keys or paths that escape the cache root raise ValueError.

Changes

Cohort / File(s) Summary
Replay Cache Validation
lumibot/components/agents/replay_cache.py
Added module-level _KEY_RE regex pattern and enhanced _path_for method with key format validation (64-char lowercase hex) and path containment verification using resolve()/relative_to() to prevent directory traversal.

Estimated code review effort

🎯 2 (Simple) | ⏱️ ~10 minutes

Poem

🐰 A cache key checked, format tight,
Hex patterns validated right,
No paths escape our burrow's hold,
Security measures, strong and bold!

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The pull request title accurately and specifically describes the main security fix: addressing path traversal and arbitrary file write risks in replay cache key handling, which directly corresponds to the changeset.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Warning

There were issues while running some tools. Please review the errors and either fix the tool's configuration or disable the tool if it's a critical failure.

🔧 Pylint (4.0.5)
lumibot/components/agents/replay_cache.py

************* Module pylintrc
pylintrc:1:0: F0011: error while parsing the configuration: File contains no section headers.
file: 'pylintrc', line: 1
'known-third-party=lumibot' (config-parse-error)
[
{
"type": "convention",
"module": "lumibot.components.agents.replay_cache",
"obj": "",
"line": 22,
"column": 0,
"endLine": null,
"endColumn": null,
"path": "lumibot/components/agents/replay_cache.py",
"symbol": "line-too-long",
"message": "Line too long (116/100)",
"message-id": "C0301"
},
{
"type": "convention",
"module": "lumibot.components.agents.replay_cache",
"obj": "",
"line": 35,
"column": 0,
"endLine": null,
"endColumn": null,
"path": "lumibot/components/agents/replay_cache.py",
"symbol": "line-too-long",
"message": "Line too long (115/100)",
"message-id": "C0301"
},
{
"type": "co

... [truncated 2543 characters] ...

t.components.agents.replay_cache",
"obj": "AgentReplayCache.load",
"line": 79,
"column": 4,
"endLine": 79,
"endColumn": 12,
"path": "lumibot/components/agents/replay_cache.py",
"symbol": "missing-function-docstring",
"message": "Missing function or method docstring",
"message-id": "C0116"
},
{
"type": "convention",
"module": "lumibot.components.agents.replay_cache",
"obj": "AgentReplayCache.save",
"line": 88,
"column": 4,
"endLine": 88,
"endColumn": 12,
"path": "lumibot/components/agents/replay_cache.py",
"symbol": "missing-function-docstring",
"message": "Missing function or method docstring",
"message-id": "C0116"
}
]


Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
lumibot/components/agents/replay_cache.py (1)

53-77: ⚠️ Potential issue | 🟠 Major

Add regression tests, explanatory comment, and documentation for this security fix.

The defense-in-depth approach (strict regex + post-resolution relative_to containment check) correctly mitigates path traversal risk. The regex ^[a-f0-9]{64}$ matches the lowercase output of hashlib.sha256(...).hexdigest() from compute_key, so legitimate callers are unaffected.

However, per coding guidelines, this behavioral change requires:

  1. Regression tests in the same commit exercising malicious keys ("../etc/passwd", "a"*63, uppercase hex, embedded /, NUL byte) and asserting ValueError("Invalid replay cache key"), plus a happy-path test for a valid 64-char hex key.

  2. Inline comment explaining the why and invariant:

Suggested invariant comment
     def _path_for(self, key: str) -> Path:
+        # Invariant: ``key`` must be a SHA-256 hex digest produced by ``compute_key``.
+        # The regex rejects path separators and ``..`` segments; the resolved-path
+        # containment check is defense-in-depth against symlinked roots or future
+        # regex relaxation.
         if not _KEY_RE.fullmatch(key):
             raise ValueError("Invalid replay cache key")
  1. Documentation update if cache key format is part of any public contract (e.g., docs/ or docstrings).
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@lumibot/components/agents/replay_cache.py` around lines 53 - 77, The change
tightens AgentReplayCache key validation (see _KEY_RE and _path_for) to prevent
path traversal, so add regression tests that call AgentReplayCache._path_for (or
compute_key where appropriate) asserting ValueError("Invalid replay cache key")
for malicious inputs ("../etc/passwd", "a"*63, uppercase hex, strings with "/",
NUL byte) and a happy-path test using a 64-char lowercase hex produced by
compute_key; add a short inline comment above _KEY_RE/_path_for explaining the
defense-in-depth invariant (regex + post-resolution containment check against
self.root) and why legitimate keys from compute_key remain valid; and if the key
format is part of any public API surface, update the relevant docstring or docs
to state the 64-char lowercase hex requirement.
🧹 Nitpick comments (1)
lumibot/components/agents/replay_cache.py (1)

67-77: Optional: cache self.root.resolve() once in __init__.

self.root is fixed after construction, so resolved_root = self.root.resolve() recomputes the same value on every _path_for call (which is on every load/save). Resolving once in __init__ and reusing it slightly reduces per-call syscalls and makes intent clearer. Non-blocking.

♻️ Proposed refactor
     def __init__(self) -> None:
         self.root = _cache_root() / "agent_runtime" / "replay"
         self.root.mkdir(parents=True, exist_ok=True)
+        self._resolved_root = self.root.resolve()
         self.remote_cache = get_backtest_cache()
@@
     def _path_for(self, key: str) -> Path:
         if not _KEY_RE.fullmatch(key):
             raise ValueError("Invalid replay cache key")
         path = self.root / key[:2] / f"{key}.json.gz"
-        resolved_root = self.root.resolve()
         resolved_path = path.resolve()
         try:
-            resolved_path.relative_to(resolved_root)
+            resolved_path.relative_to(self._resolved_root)
         except ValueError as exc:
             raise ValueError("Invalid replay cache key") from exc
         return resolved_path
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@lumibot/components/agents/replay_cache.py` around lines 67 - 77, Cache the
resolved root path once in the constructor and reuse it in _path_for to avoid
repeated filesystem resolves: add an attribute (e.g., self._resolved_root =
self.root.resolve()) in __init__ and then update _path_for to use that cached
self._resolved_root instead of calling self.root.resolve() each time, while
keeping the same validation logic (KEY_RE check, building path via self.root /
key[:2] / f"{key}.json.gz", resolving the candidate path and using
candidate.relative_to(self._resolved_root) to detect escapes and raise
ValueError as before).
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Outside diff comments:
In `@lumibot/components/agents/replay_cache.py`:
- Around line 53-77: The change tightens AgentReplayCache key validation (see
_KEY_RE and _path_for) to prevent path traversal, so add regression tests that
call AgentReplayCache._path_for (or compute_key where appropriate) asserting
ValueError("Invalid replay cache key") for malicious inputs ("../etc/passwd",
"a"*63, uppercase hex, strings with "/", NUL byte) and a happy-path test using a
64-char lowercase hex produced by compute_key; add a short inline comment above
_KEY_RE/_path_for explaining the defense-in-depth invariant (regex +
post-resolution containment check against self.root) and why legitimate keys
from compute_key remain valid; and if the key format is part of any public API
surface, update the relevant docstring or docs to state the 64-char lowercase
hex requirement.

---

Nitpick comments:
In `@lumibot/components/agents/replay_cache.py`:
- Around line 67-77: Cache the resolved root path once in the constructor and
reuse it in _path_for to avoid repeated filesystem resolves: add an attribute
(e.g., self._resolved_root = self.root.resolve()) in __init__ and then update
_path_for to use that cached self._resolved_root instead of calling
self.root.resolve() each time, while keeping the same validation logic (KEY_RE
check, building path via self.root / key[:2] / f"{key}.json.gz", resolving the
candidate path and using candidate.relative_to(self._resolved_root) to detect
escapes and raise ValueError as before).

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: de6f8765-824f-4ecf-9666-fc804f1987fa

📥 Commits

Reviewing files that changed from the base of the PR and between 2d51f9c and e592d2e.

📒 Files selected for processing (1)
  • lumibot/components/agents/replay_cache.py

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.

1 participant