|
1 | 1 | import logging |
| 2 | +import os |
2 | 3 | from concurrent.futures import ThreadPoolExecutor |
3 | 4 | from contextlib import ExitStack |
| 5 | +from pathlib import Path |
| 6 | +from tempfile import NamedTemporaryFile |
4 | 7 | from unittest import TestCase, mock |
5 | 8 | from uuid import uuid4 |
6 | 9 |
|
@@ -63,3 +66,37 @@ def test_implicit_cleanup(self): |
63 | 66 | with self.assertWarns(ResourceWarning): |
64 | 67 | del memfs |
65 | 68 | self.assertFalse(path.exists()) |
| 69 | + |
| 70 | + def test_symlink_to_file_outside_output_is_not_collected(self): |
| 71 | + """A symlink pointing at a file outside output must not be read.""" |
| 72 | + with NamedTemporaryFile(mode="w", suffix=".secret", delete=False) as f: |
| 73 | + f.write("host-secret-content") |
| 74 | + secret_path = Path(f.name) |
| 75 | + |
| 76 | + try: |
| 77 | + with MemFS(1024) as memfs: |
| 78 | + os.symlink(secret_path, memfs.output / "leak.txt") |
| 79 | + |
| 80 | + results = memfs.files_list(limit=10, pattern="**/*") |
| 81 | + leaked = [r for r in results if r.content == b"host-secret-content"] |
| 82 | + |
| 83 | + self.assertEqual( |
| 84 | + leaked, |
| 85 | + [], |
| 86 | + "Symlink to a file outside the output dir was followed " |
| 87 | + "and its content was exfiltrated", |
| 88 | + ) |
| 89 | + finally: |
| 90 | + secret_path.unlink(missing_ok=True) |
| 91 | + |
| 92 | + def test_symlink_within_output_dir_still_collected(self): |
| 93 | + """Symlinks pointing to files within the output dir should still be collected.""" |
| 94 | + with MemFS(1024) as memfs: |
| 95 | + real = memfs.output / "real.txt" |
| 96 | + real.write_text("normal-output-content") |
| 97 | + os.symlink(real, memfs.output / "link.txt") |
| 98 | + |
| 99 | + results = {r.path: r.content for r in memfs.files_list(limit=10, pattern="**/*")} |
| 100 | + |
| 101 | + self.assertIn("link.txt", results) |
| 102 | + self.assertEqual(results["link.txt"], b"normal-output-content") |
0 commit comments