diff --git a/aider/commands.py b/aider/commands.py index 3881403c5c1..20145a81cc4 100644 --- a/aider/commands.py +++ b/aider/commands.py @@ -847,7 +847,7 @@ def cmd_add(self, args): abs_file_path = self.coder.abs_root_path(matched_file) if ( - not abs_file_path.startswith(self.coder.root) + not Path(abs_file_path).is_relative_to(self.coder.root) and not is_image_file(matched_file) and self.coder.auto_commits ): diff --git a/tests/basic/test_commands.py b/tests/basic/test_commands.py index 406e928ebf2..0d6b6372a3c 100644 --- a/tests/basic/test_commands.py +++ b/tests/basic/test_commands.py @@ -506,6 +506,40 @@ def test_cmd_add_from_outside_git(self): self.assertEqual(len(coder.abs_fnames), 0) + def test_cmd_add_abs_path_in_sibling_dir_with_prefix(self): + """An absolute path inside a sibling directory whose name starts with + the repo root's name must be rejected. + + Regression: the within-root guard used str.startswith(), so a path + like /tmp/x/repoXYZ/secret.env was wrongly accepted when the repo + root was /tmp/x/repo (no trailing path separator), leaking an + out-of-tree file into the chat / to the LLM. + See https://github.com/Aider-AI/aider/issues/178 + """ + with ChdirTemporaryDirectory() as tmp_dname: + root = Path("repo") + root.mkdir() + os.chdir(str(root)) + + make_repo() + + io = InputOutput(pretty=False, fancy_input=False, yes=False) + from aider.coders import Coder + + coder = Coder.create(self.GPT35, None, io) + commands = Commands(io, coder) + + # Sibling dir whose name shares the root's prefix, holding a real file. + sibling = Path(tmp_dname) / "repoXYZ" + sibling.mkdir() + outside_file = sibling / "secret.env" + outside_file.touch() + + commands.cmd_add(str(outside_file.resolve())) + + self.assertNotIn(str(outside_file.resolve()), coder.abs_fnames) + self.assertEqual(len(coder.abs_fnames), 0) + def test_cmd_add_filename_with_special_chars(self): with ChdirTemporaryDirectory(): io = InputOutput(pretty=False, fancy_input=False, yes=False)