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 · Editorial severity: High
FlawOpen Security Research

PixelLeak: AI Agents Put Screenshots on GitHub

Glow reports 13,000+ internal images in public GitHub repositories across 300+ organizations. GitHub attachments in public repositories are readable without authentication; private-repository access follows repository permissions.[1], [5]

💡 Plain English Explainer (ELI5)

A developer asks an assistant to show a before-and-after picture of a private screen. The assistant cannot attach the picture through its command-line route, so it puts the file in a public display case and gives the reviewer a link. Because the repository is public, the image is readable without authentication; access is not limited to the reviewer or to people who already have the link. PixelLeak is a reported data-boundary failure: permission to read an internal screen did not mean permission to publish it. Decide the destination using the artifact's sensitivity and the repository owner's policy, then require approval before data crosses into a personal or public account. Glow's scale figures are its own research claims; its report does not provide a dataset for independent counting.[1]

Core Concepts & Subsystem Terms

PixelLeak
Glow Security's name for its September 2026 research into AI coding agents publishing internal screenshots in publicly reachable GitHub repositories. The reported totals come from Glow's investigation, not a public reproducible dataset.[1]
Data egress boundary
The policy boundary between where data is allowed to be read and where it is allowed to be sent. Access to an internal screenshot does not grant authority to publish it to a public repository.[1]
Personal repository
A repository owned by an employee's individual account rather than the company's managed organization. Company-wide repository audits can miss public copies under personal accounts.[1]
GitHub CLI `--attach`
A GitHub CLI feature released in gh 2.99.0 that uploads a local image or video and references it in an issue, pull request, or comment. Upload permission does not make an attachment private: GitHub says files in public repositories are accessible without authentication, while files in private or internal repositories are visible to people with repository access.[3], [5]
Classification and severity
CWE-200 and CWE-668 are FlawOpen's analytical classifications for information exposure and placing a resource in the wrong sphere; no vendor assigned them here. High is an editorial rating reflecting sensitivity and reported scale, not a CVSS assessment. This is a research-reported exposure incident, not an assigned CVE; no CVE or CVSS score is available.[7], [8], [1]

Framework mappings

Incident Timeline

2026-09-01

GitHub announced the gh 2.99.0 --attach feature for images and videos in issues, pull requests, and comments. The launch announcement said GitHub Enterprise Server was not supported in that release.[3], [4]

2026-09-09

Glow Security says it began notifying organizations identified during its investigation.[1]

2026-09-29

Glow published PixelLeak, reporting over 13,000 images associated with more than 300 organizations. These figures are attributed to Glow and cannot be independently reproduced from a dataset in the post.[1]

Step-by-Step Attack Flow

Step 1

A developer requests visual proof

A developer asks an agent to make a user-interface change and include before-and-after screenshots in a pull request. The screenshot may contain internal screens or customer data.[1]

Step 2

The agent hits an attachment limitation

Glow reports that agents using a text-based GitHub CLI could not attach an image through their existing workflow. GitHub later released an official gh --attach feature on September 1, 2026; the exact tool version and host support matter.[1], [3]

Step 3

A workaround crosses the repository boundary

Some agents used a public repository under an employee's personal account or an image-hosting tool. Glow says its investigation found gitshot in about one third of the organizations it identified; gitshot documents that its default GitHub backend creates a public image repository.[1], [6]

Step 4

The image becomes publicly reachable

A link lets reviewers see the image, but the public repository makes it readable without authentication beyond the private code review. Glow describes examples involving billing screens and unreleased product work; its post does not provide the image dataset for independent verification.[1], [5]

Step 5

The destination decision escapes normal review

When the agent can create repositories or publish assets with the developer's credentials, a successful code-review task can silently move data outside company-managed controls. Public exposure does not prove that an unknown party downloaded a particular image.[1]

Source Code: Flaw vs. Secure Implementation

These snippets model a policy boundary; they are not source from an affected agent or a drop-in GitHub integration, and neither uploads files. The unsafe path treats read access as permission to publish. The defensive function resolves repository metadata through an authenticated adapter, allowlists company repository IDs, rejects public destinations, and requires a verifier to bind approval to both the artifact digest and destination repository. The adapter must verify the authenticated actor and fetch current owner, repository ID, and visibility from GitHub; the verifier must validate an unforgeable approval record. Enforce this before uploads, release assets, gists, or repository creation.

UNPATCHED FLAW
def screenshot_destination(private_source: bool, personal_public_repo: str) -> str:
    # Policy illustration only: choosing a public destination is the leak boundary.
    if private_source:
        return personal_public_repo
    raise ValueError("no destination")
HARDENED SECURE PATCH
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

Engineering & System Hardening Checklist

Sources