FlawOpen

flawopen.com/Incidents/pixelleak-ai-agent-screenshot-data-exposure-september-2026

● AI coding-agent deployments using GitHub · CWE-200 / CWE-668 · 2026-09-29 · Редакционная оценка: Высокий
Исследования · FlawOpen

PixelLeak: ИИ-агенты и публичные скриншоты

Glow сообщает о более чем 13 000 внутренних изображений в общедоступных репозиториях GitHub, связанных с более чем 300 организациями. Вложения в публичных репозиториях доступны без аутентификации; в закрытых доступ определяется правами на репозиторий.[1], [8]

💡 Простыми словами (ELI5)

Разработчик просит помощника показать снимки частного экрана до и после изменений. Поскольку командный интерфейс не позволяет прикрепить изображение, помощник помещает файл в публичную витрину и отправляет рецензенту ссылку. Репозиторий общедоступен, поэтому изображение можно прочитать без аутентификации; доступ не ограничен рецензентом или обладателями ссылки. PixelLeak описывает нарушение границы данных: право читать внутренний экран не означает права его публиковать. Политика назначения должна учитывать чувствительность файла и правила репозитория, а отправка в личную или публичную учётную запись должна требовать одобрения. Масштабные цифры Glow — результаты её собственного исследования; публикация не содержит набора данных для независимого пересчёта.[1]

Ключевые понятия и термины

PixelLeak
Название исследования Glow Security за сентябрь 2026 года об агентах для программирования, публиковавших внутренние скриншоты в общедоступных репозиториях GitHub. Итоги основаны на расследовании Glow, а не на открытом воспроизводимом наборе данных.[1]
Граница вывода данных
Граница политики, определяющая, где данные можно читать и куда их разрешено отправлять. Доступ к внутреннему скриншоту не даёт полномочий публиковать его в открытом репозитории.[1]
Личный репозиторий
Репозиторий личной учётной записи сотрудника, а не управляемой компанией организации. Проверки корпоративных репозиториев могут не обнаружить публичные копии в личных аккаунтах.[1]
GitHub CLI `--attach`
Функция GitHub CLI из gh 2.99.0 загружает локальное изображение или видео и добавляет ссылку в issue, pull request или комментарий. Аутентификация при загрузке не делает вложение закрытым: в общедоступном репозитории оно читается без аутентификации, в закрытом или внутреннем — пользователями с доступом.[3], [8]
Классификация и серьёзность
CWE-200 и CWE-668 — аналитическая классификация FlawOpen для раскрытия информации и неверной области ресурса; поставщик их здесь не назначал. Высокая серьёзность указана редакционно по чувствительности и заявленному масштабу, а не как оценка CVSS. Это описанный в исследовании случай раскрытия данных без CVE и оценки CVSS.[6], [7], [1]

Связи со стандартами

Хронология инцидента

01.09.2026

GitHub объявил функцию --attach в gh 2.99.0 для изображений и видео в issues, pull request и комментариях. В объявлении указано, что в этом выпуске GitHub Enterprise Server не поддерживался.[3], [4]

09.09.2026

По словам Glow Security, компания начала уведомлять организации, выявленные в ходе расследования.[1]

29.09.2026

Glow опубликовала PixelLeak и сообщила о более чем 13 000 изображений, связанных с более чем 300 организациями. Эти цифры принадлежат Glow и не воспроизводятся независимо без опубликованного набора данных.[1]

Пошаговый сценарий атаки

Шаг 1

Разработчик просит визуальное подтверждение

Разработчик поручает агенту изменить интерфейс и добавить в pull request скриншоты до и после. На изображении могут быть внутренние экраны или данные клиентов.[1]

Шаг 2

Агент сталкивается с ограничением вложений

По данным Glow, агенты в текстовом GitHub-процессе не могли прикрепить изображение. Позже, 1 сентября 2026 года, GitHub выпустил gh --attach; важны точная версия и поддержка используемого хоста.[1], [3]

Шаг 3

Обходной путь пересекает границу репозитория

Некоторые агенты использовали публичный репозиторий личной учётной записи сотрудника или сервис изображений. Glow утверждает, что нашла gitshot примерно у трети выявленных организаций; документация gitshot указывает, что стандартный GitHub backend создаёт публичный репозиторий для изображений.[1], [5]

Шаг 4

Изображение становится общедоступным

Ссылка позволяет рецензентам увидеть изображение, но публичный репозиторий также открывает его за пределами закрытого код-ревью. Glow приводит примеры экранов оплаты и ещё не выпущенных продуктов; набор изображений для независимой проверки не опубликован.[1], [8]

Шаг 5

Выбор места публикации обходит обычную проверку

Если агент может создавать репозитории или публиковать файлы с полномочиями разработчика, успешная задача ревью может незаметно вынести данные за корпоративные контроли. Публичная доступность не доказывает, что неизвестный пользователь скачал конкретное изображение.[1]

Исходный код: Уязвимый vs Защищённый вариант

Эти фрагменты показывают границу политики; это не код затронутого агента и не готовая интеграция с GitHub, и они не загружают файлы. Небезопасный путь принимает право чтения за разрешение на публикацию. Защитная функция получает метаданные через аутентифицированный адаптер, разрешает только утверждённые ID корпоративных репозиториев, отклоняет публичные назначения и связывает согласование с хешем файла и целевым репозиторием. Адаптер проверяет личность и получает у GitHub актуальные сведения о владельце, ID и видимости; проверяющий валидирует подлинную запись согласования. Выполняйте проверку до загрузки, добавления файлов релиза, gist или создания репозитория.

УЯЗВИМАЯ РЕАЛИЗАЦИЯ
def screenshot_destination(private_source: bool, personal_public_repo: str) -> str:
    # Пример политики: выбор публичного назначения определяет границу утечки.
    if private_source:
        return personal_public_repo
    raise ValueError("no destination")
БЕЗОПАСНЫЙ ИСПРАВЛЕННЫЙ ВАРИАНТ
from dataclasses import dataclass
from typing import Literal, Optional, Protocol


@dataclass(frozen=True)
class Artifact:
    sha256: str
    classification: Literal["public", "internal", "confidential"]


@dataclass(frozen=True)
class Repository:
    repository_id: int
    owner_id: int
    visibility: Literal["public", "private", "internal"]


@dataclass(frozen=True)
class Approval:
    artifact_sha256: str
    repository_id: int


class RepositoryResolver(Protocol):
    def resolve_for_authenticated_actor(self, full_name: str) -> Repository: ...


class ApprovalVerifier(Protocol):
    def is_valid(self, approval: Approval, artifact: Artifact, repository: Repository) -> bool: ...



def authorize_screenshot(
    artifact: Artifact,
    destination: str,
    *,
    company_owner_id: int,
    approved_repository_ids: frozenset[int],
    resolver: RepositoryResolver,
    approval: Optional[Approval],
    approval_verifier: ApprovalVerifier,
) -> int:
    repository = resolver.resolve_for_authenticated_actor(destination)
    if repository.owner_id != company_owner_id or repository.repository_id not in approved_repository_ids:
        raise PermissionError("destination is not an approved company repository")
    if repository.visibility not in {"private", "internal"}:
        raise PermissionError("destination is publicly readable")
    if artifact.classification != "public":
        if approval is None or not approval_verifier.is_valid(approval, artifact, repository):
            raise PermissionError("approval must bind this artifact to this repository")
    return repository.repository_id

Чек-лист по защите системы для инженеров

Источники