diff --git a/backend/services/github.py b/backend/services/github.py index c581ded..cd98634 100644 --- a/backend/services/github.py +++ b/backend/services/github.py @@ -423,16 +423,17 @@ def _extract_zip_locked(upload_path: Path): def iter_files(root: Path): # Never follow a repository-controlled symbolic link outside its tree. trusted_root = root.resolve() - for p in root.rglob("*"): - if p.is_symlink() or not p.is_file(): continue - if trusted_root not in p.resolve().parents: continue - rel = p.relative_to(root) - if any(part.casefold() in SKIP_DIRS for part in rel.parts[:-1]): continue - try: size = p.stat().st_size - except OSError: continue - if size > 300_000: continue - if p.suffix.lower() in TEXT_EXTENSIONS or p.name.lower().endswith(".env.example") or p.name.lower() in {"dockerfile","makefile",".gitignore","go.mod","gemfile"}: - yield p + for directory, subdirs, files in os.walk(root, followlinks=False): + subdirs[:] = [name for name in subdirs if name.casefold() not in SKIP_DIRS] + for name in files: + p = Path(directory) / name + if p.is_symlink() or not p.is_file(): continue + if trusted_root not in p.resolve().parents: continue + try: size = p.stat().st_size + except OSError: continue + if size > 300_000: continue + if p.suffix.lower() in TEXT_EXTENSIONS or p.name.lower().endswith(".env.example") or p.name.lower() in {"dockerfile","makefile",".gitignore","go.mod","gemfile"}: + yield p def read_text(path: Path): return path.read_text(encoding="utf-8", errors="replace") diff --git a/backend/tests/test_source_traversal.py b/backend/tests/test_source_traversal.py new file mode 100644 index 0000000..4c3a724 --- /dev/null +++ b/backend/tests/test_source_traversal.py @@ -0,0 +1,28 @@ +import tempfile +import unittest +from pathlib import Path +from unittest.mock import patch + +from services.github import iter_files + + +class SourceTraversalTests(unittest.TestCase): + def test_walk_skips_file_checks_in_ignored_subtrees(self): + with tempfile.TemporaryDirectory() as directory: + root = Path(directory) + ignored = root / "src" / "Node_Modules" + ignored.mkdir(parents=True) + dependency = ignored / "vendor.js" + dependency.touch() + source = root / "src" / "app.js" + source.touch() + inspected = [] + is_file = Path.is_file + + def track_file_check(path): + inspected.append(path) + return is_file(path) + + with patch.object(Path, "is_file", track_file_check): + self.assertEqual(list(iter_files(root)), [source]) + self.assertNotIn(dependency, inspected)