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: AI 에이전트 공개 스크린샷

Glow는 300곳이 넘는 조직의 공개 GitHub 저장소에서 내부 이미지 13,000개 이상을 확인했다고 보고했습니다. 공개 저장소 첨부 파일은 인증 없이 읽을 수 있고, 비공개 저장소에서는 저장소 권한이 적용됩니다.[1], [8]

💡 알기 쉬운 설명 (ELI5)

개발자가 도우미에게 비공개 화면의 변경 전후 이미지를 요청합니다. 명령줄 흐름에서 첨부할 수 없자 도우미는 파일을 공개 전시 공간에 올리고 검토자에게 링크를 보냅니다. 저장소가 공개되어 누구나 인증 없이 이미지를 읽을 수 있으며, 검토자나 링크 보유자에게만 제한되지 않습니다. PixelLeak은 데이터 경계 실패를 보여 줍니다. 내부 화면을 읽을 권한이 있다고 해서 이를 게시할 권한이 생기는 것은 아닙니다. 저장 위치 정책은 파일의 민감도와 저장소 소유 규칙을 확인하고 개인 또는 공개 계정으로 보내기 전에 승인을 요구해야 합니다. Glow의 규모 수치는 자체 조사 주장이고, 독립적으로 셀 수 있는 데이터셋은 공개되지 않았습니다.[1]

핵심 개념 및 용어

PixelLeak
Glow Security가 2026년 9월 조사에 붙인 이름입니다. 코딩 에이전트가 내부 스크린샷을 공개적으로 접근 가능한 GitHub 저장소에 게시한 사례를 조사했습니다. 집계는 Glow의 조사 결과이며 공개 재현 데이터셋은 없습니다.[1]
데이터 반출 경계
데이터를 어디서 읽을 수 있고 어디로 보낼 수 있는지 정하는 정책 경계입니다. 내부 스크린샷에 접근할 수 있어도 공개 저장소에 게시할 권한이 자동으로 생기지는 않습니다.[1]
개인 저장소
회사가 관리하는 조직이 아니라 직원 개인 계정이 소유한 저장소입니다. 회사 조직 저장소만 감사하면 개인 계정의 공개 사본을 놓칠 수 있습니다.[1]
GitHub CLI `--attach`
gh 2.99.0에서 출시된 GitHub CLI 기능으로 로컬 이미지나 동영상을 올려 issue, pull request 또는 댓글에서 참조합니다. 업로드 인증만으로 첨부 파일이 비공개가 되지는 않습니다. 공개 저장소 파일은 인증 없이 읽을 수 있고, 비공개/내부 저장소에서는 접근 권한이 있는 사용자만 볼 수 있습니다.[3], [8]
분류 및 심각도
CWE-200과 CWE-668은 정보 노출 및 자원이 잘못된 범위로 이동하는 문제에 대한 FlawOpen의 분석 분류이며 공급업체 지정 값이 아닙니다. ‘High’는 편집상 평가이며 CVSS가 아닙니다. 이는 연구에서 보고된 노출 사건으로, 지정된 CVE나 CVSS 점수는 없습니다.[6], [7], [1]

프레임워크 매핑

사고 타임라인

2026-09-01

GitHub는 issue, pull request, 댓글에 이미지와 영상을 추가하는 gh 2.99.0의 --attach 기능을 발표했습니다. 발표 당시 GitHub Enterprise Server는 지원되지 않았습니다.[3], [4]

2026-09-09

Glow Security에 따르면 조사에서 식별한 조직에 알리기 시작했습니다.[1]

2026-09-29

Glow는 PixelLeak을 공개하고 300개가 넘는 조직과 관련된 13,000장 이상의 이미지를 보고했습니다. 이 수치는 Glow가 제시한 것으로 공개 데이터셋 없이 독립적으로 재현할 수 없습니다.[1]

단계별 공격 실행 흐름

단계 1

개발자가 시각적 증거를 요청

개발자는 에이전트에게 UI를 수정하고 pull request에 변경 전후 스크린샷을 포함해 달라고 합니다. 이미지에는 내부 화면이나 고객 데이터가 담길 수 있습니다.[1]

단계 2

에이전트가 첨부 기능 제한에 부딪힘

Glow에 따르면 텍스트 기반 GitHub 흐름을 사용한 에이전트는 이미지를 첨부할 수 없었습니다. 이후 GitHub는 2026년 9월 1일 gh --attach를 출시했으며 정확한 버전과 호스트 지원 여부가 중요합니다.[1], [3]

단계 3

우회 방법이 저장소 경계를 넘음

일부 에이전트는 직원 개인 계정의 공개 저장소나 이미지 호스팅 도구를 사용했습니다. Glow는 식별한 조직의 약 3분의 1에서 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

엔지니어링 및 시스템 보안 강화 체크리스트

출처