read_file_content resolves the path and then compares string prefixes:
real_abs = os.path.realpath(abs_path)
real_repo = os.path.realpath(repo_dir)
if not real_abs.startswith(real_repo):
return None
backend/app/services/code_project_service.py:282
get_file_abs_path has the identical check at backend/app/services/code_project_service.py:306.
real_repo for project cproj_aaaa is .../data/code_projects/cproj_aaaa/repo. The string .../data/code_projects/cproj_aaaa/repo_backup/secrets.env also passes startswith. Note that the module already contains a correct implementation a few lines above — _safe_join_repo at backend/app/services/code_project_service.py:147 compares against real_repo + os.sep — it is simply not used on the read paths.
The router-level guard does not help either: it rejects only the literal substring ".." (backend/app/modules/code/code_projects_api.py:270), which a symlink inside the repo defeats without ever containing ...
Trigger: the code generator writes a symlink, or an operator creates any sibling directory sharing the repo-dir prefix; then GET /api/v1/code/projects/cproj_aaaa/file?path=<...>.
Observed: file contents outside the project root are returned as 200.
Expected: _safe_join_repo reused for both readers, plus an os.path.islink rejection.
read_file_contentresolves the path and then compares string prefixes:backend/app/services/code_project_service.py:282get_file_abs_pathhas the identical check atbackend/app/services/code_project_service.py:306.real_repofor projectcproj_aaaais.../data/code_projects/cproj_aaaa/repo. The string.../data/code_projects/cproj_aaaa/repo_backup/secrets.envalso passesstartswith. Note that the module already contains a correct implementation a few lines above —_safe_join_repoatbackend/app/services/code_project_service.py:147compares againstreal_repo + os.sep— it is simply not used on the read paths.The router-level guard does not help either: it rejects only the literal substring
".."(backend/app/modules/code/code_projects_api.py:270), which a symlink inside the repo defeats without ever containing...Trigger: the code generator writes a symlink, or an operator creates any sibling directory sharing the repo-dir prefix; then
GET /api/v1/code/projects/cproj_aaaa/file?path=<...>.Observed: file contents outside the project root are returned as 200.
Expected:
_safe_join_reporeused for both readers, plus anos.path.islinkrejection.