fix(exporter): refuse symlinks at export targets

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
This commit is contained in:
Igor Lins e Silva
2026-05-07 12:40:26 -03:00
parent 03ed4c45cf
commit 40e2c8b056
2 changed files with 58 additions and 0 deletions
+42
View File
@@ -134,3 +134,45 @@ def test_export_empty_palace():
assert stats == {"wings": 0, "rooms": 0, "drawers": 0}
finally:
shutil.rmtree(tmpdir, ignore_errors=True)
def test_export_refuses_symlinked_output_dir():
"""A symlink at the output path must not be followed (defense-in-depth)."""
import pytest
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)
# Decoy target must remain empty — nothing followed the symlink.
assert os.listdir(decoy_target) == []
finally:
shutil.rmtree(tmpdir, ignore_errors=True)
def test_export_refuses_symlinked_wing_dir():
"""A symlink pre-placed at a wing subdirectory must also be refused."""
import pytest
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.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)
assert os.listdir(decoy_target) == []
finally:
shutil.rmtree(tmpdir, ignore_errors=True)