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 代理将截图发布到 GitHub

Glow 报告称,300 多家组织的 GitHub 公共仓库中出现了 13,000 多张内部图片。公共仓库附件无需身份验证即可读取;私有仓库附件遵循仓库访问权限。[1], [8]

💡 通俗易懂的原理解析 (ELI5)

开发者让助手提供一张私有界面的修改前后截图。由于命令行流程无法附加图片,助手把文件放进公开展示柜,再把链接发给审阅者。仓库是公开的,因此任何人无需身份验证都能读取图片;访问者并不限于审阅者或已经拿到链接的人。 PixelLeak 是报告中的数据边界失守:能够读取内部屏幕并不意味着有权公开。目的地应根据文件敏感级别和仓库策略决定,向个人或公共账户发送前必须审批。相关数字来自 Glow 的调查,文章没有提供可供独立统计的数据集。[1]

核心概念与专有名词

PixelLeak
Glow Security 对 2026 年 9 月研究的命名,研究对象是将内部截图发布到可公开访问的 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]

框架映射

事件演进时间线

2026-09-01

GitHub 宣布 gh 2.99.0 的 --attach 功能,可在 issue、pull request 和评论中添加图片与视频。公告称该版本不支持 GitHub Enterprise Server。[3], [4]

2026-09-09

据 Glow Security 称,该公司开始通知调查中识别出的组织。[1]

2026-09-29

Glow 发布 PixelLeak,报告称其发现了与 300 多个组织相关的 13,000 多张图片。这些数字来自 Glow;由于没有公开数据集,无法独立复现。[1]

攻击执行流程分解

步骤 1

开发者要求提供视觉证据

开发者要求代理修改界面,并在 pull request 中加入修改前后的截图。图片可能显示内部页面或客户数据。[1]

步骤 2

代理遇到附件功能限制

Glow 称,使用基于文本的 GitHub 流程的代理无法附加图片。GitHub 后于 2026 年 9 月 1 日推出官方 gh --attach;具体版本和主机支持情况很重要。[1], [3]

步骤 3

变通方案跨越仓库边界

一些代理使用员工个人账户下的公共仓库或图片托管工具。Glow 称其识别出的组织中约三分之一使用了 gitshot;该工具文档说明,其默认 GitHub 后端会创建一个公共图片仓库。[1], [5]

步骤 4

图片变成公开可访问

链接可以让审阅者看到图片,但公共仓库也会让代码审查范围之外的人无需身份验证即可读取。Glow 描述了涉及计费界面和未发布产品的案例;文章未提供底层图片数据集以供独立核验。[1], [8]

步骤 5

目的地选择绕过常规审查

如果代理能使用开发者凭据创建仓库或发布文件,完成一次代码审查任务就可能在没有提醒的情况下把数据移出公司控制。图片公开可访问并不能证明未知人员下载过具体图片。[1]

源代码对比:漏洞与安全实现

这些代码片段展示策略边界,并非受影响代理的源代码或可直接使用的 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

工程与系统安全加固清单

参考来源