fix(exporter): refuse symlinks at export targets (#1156) - #1405
Merged
Merged
Conversation
A symlink pre-placed at the export output_dir or any wing subdirectory would redirect markdown writes to wherever the symlink points. The miner already rejects symlinked inputs via Path.is_symlink(); the exporter should apply the same caution to outputs. Add _reject_symlink() helper and call it before makedirs on both output_dir and each wing_dir. Refusal raises ValueError with a clear message rather than silently falling through. Closes #1156
Contributor
There was a problem hiding this comment.
Pull request overview
This PR hardens the exporter against a symlink-based write redirection by refusing to export into an output directory (or wing subdirectory) that is itself a symlink, aligning exporter behavior with the miner’s existing input-side symlink avoidance.
Changes:
- Add a new
mempalace.exporter._reject_symlink()helper and call it before creatingoutput_dirand eachwing_dir. - Add tests asserting export refuses a symlinked output root and a symlinked wing directory, and that the decoy target remains untouched.
Reviewed changes
Copilot reviewed 2 out of 2 changed files in this pull request and generated 3 comments.
| File | Description |
|---|---|
mempalace/exporter.py |
Adds symlink rejection checks for output root and wing directories prior to directory creation. |
tests/test_exporter.py |
Adds regression tests for refusing symlinked export targets (root and wing). |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
Comment on lines
63
to
67
| return {"wings": 0, "rooms": 0, "drawers": 0} | ||
|
|
||
| _reject_symlink(output_dir, "output_dir") | ||
| os.makedirs(output_dir, exist_ok=True) | ||
| try: |
Comment on lines
+143
to
+152
| tmpdir = tempfile.mkdtemp() | ||
| try: | ||
| palace_path = _setup_palace(tmpdir) | ||
| decoy_target = os.path.join(tmpdir, "decoy_target") | ||
| os.makedirs(decoy_target) | ||
| output_dir = os.path.join(tmpdir, "export") | ||
| os.symlink(decoy_target, output_dir) | ||
|
|
||
| with pytest.raises(ValueError, match="symbolic link"): | ||
| export_palace(palace_path, output_dir) |
Comment on lines
+168
to
+175
| os.makedirs(decoy_target) | ||
| output_dir = os.path.join(tmpdir, "export") | ||
| os.makedirs(output_dir) | ||
| os.symlink(decoy_target, os.path.join(output_dir, "alpha")) | ||
|
|
||
| with pytest.raises(ValueError, match="symbolic link"): | ||
| export_palace(palace_path, output_dir) | ||
|
|
Address Copilot review on #1156: - Per-file symlink check via new _safe_open_for_write() helper. Uses O_NOFOLLOW on POSIX (close TOCTOU window between islink check and open) and falls back to islink + open on Windows. Applied to room files and index.md, mirroring the existing dir-level check. - Tests now wrap os.symlink() in _try_symlink_or_skip() so Windows without Developer Mode and restricted CI sandboxes skip rather than hard-fail. Added two regression tests for the file-level cases (room file, index.md).
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
A symlink pre-placed at the export
output_diror any wing subdirectory would redirect markdown writes to wherever the symlink points (e.g., system directories). The miner already rejects symlinked inputs viaPath.is_symlink(); the exporter should apply the same caution to outputs.This is defense-in-depth, not an actively exploited path — but it closes the obvious surface and matches the existing input-side hardening.
Changes
mempalace/exporter.py: new_reject_symlink()helper raisesValueErrorif the target path is a symlink. Called beforemakedirsonoutput_dirand on eachwing_dir.tests/test_exporter.py: two new tests covering pre-placed symlink at output root and at a wing subdirectory. Both verify the decoy target stays empty.Test plan
uv run pytest tests/test_exporter.py -v— 6 passed (2 new)uvx --from 'ruff>=0.4.0,<0.5' ruff check+ format check — cleanCloses #1156
Split from #934 (finding 7).