From e85925f827ce7d0d1cc439e92a871a17cca158c8 Mon Sep 17 00:00:00 2001 From: carfeii Date: Mon, 5 Oct 2026 23:12:37 +0800 Subject: [PATCH] fix(config): do not follow repository-controlled symlinks when indexing iterate_files() walked a repository with Path.rglob() and called is_file(), which follows symlinks, with no is_symlink() guard. A repository containing a symlink pointing outside the clone (to any file the server process can read) would have that symlink listed alongside real files, and the RAG indexer's open() on it dereferences straight to the external target, embedding and persisting its content. Skip any symlink encountered during the walk instead of following it. Adds a regression test in tests/test_config.py, including fail-before coverage run against the unpatched code. --- api/config.py | 5 +++++ tests/test_config.py | 25 +++++++++++++++++++++++++ 2 files changed, 30 insertions(+) diff --git a/api/config.py b/api/config.py index ab9877271..f136063fd 100644 --- a/api/config.py +++ b/api/config.py @@ -505,6 +505,11 @@ def iterate_files( results: list[str] = [] for p in Path(root_dir).rglob("*"): + if p.is_symlink(): + # A repository-controlled symlink can point outside the clone; + # following it would read/embed a host file the repository + # author does not own. Skip it rather than resolve+open it. + continue if not p.is_file(): continue if extensions and p.suffix.lower() not in extensions: diff --git a/tests/test_config.py b/tests/test_config.py index a4987bcd3..830977fe7 100644 --- a/tests/test_config.py +++ b/tests/test_config.py @@ -1,3 +1,5 @@ +import os + import pytest from api.config import iterate_files @@ -60,3 +62,26 @@ def test_iterate_files_excluded_dirs(exclude_test_config, tmp_path, excluded_dir make_repo(tmp_path) files = set(iterate_files(root_dir=str(tmp_path), excluded_dirs=excluded_dirs)) assert files == {"README.md", "CHANGELOG.md"} + + +def test_iterate_files_does_not_follow_symlink_out_of_repository( + exclude_test_config, tmp_path +): + """A repository-controlled symlink pointing outside the clone must not + be listed for indexing. Regression for the symlink-escape bug: the + walker used to call is_file() (which follows the link) with no + is_symlink() guard, so the indexer would open and embed the external + target's content.""" + make_repo(tmp_path) + + outside_dir = tmp_path.parent / "outside_the_clone" + outside_dir.mkdir(exist_ok=True) + outside_secret = outside_dir / "secret.py" + outside_secret.write_text("DB_PASSWORD = 'outside-secret'") + + alias = tmp_path / "innocuous_link.py" + os.symlink(outside_secret, alias) + + files = set(iterate_files(root_dir=str(tmp_path))) + assert "innocuous_link.py" not in files + assert files == {"README.md", "CHANGELOG.md", "folder/code.py"}